Code-audit-Challenge 代码审计练习笔记

php代码审计能力还是不大行,很有必要单独系统的拿出来练习一下。github上有一个很好的项目code-audit-challenge,那么趁着寒假就跟着这个一步步的来吧,边刷边记录。

challenge1

这道题来源于php Bug#69892。从题目逻辑上看,仅仅是接收cookie输入user,再与users一个个的比较,若相同则设置uid,当uid为0时为admin用户。 经过尝试只有5号用户密码hash可以解密为hund,但如果我们希望登录admin用户,那么就得使用*Bug#69892*了。大致来说这个bug,是当运行在64位系统中时,数组中的键0x100000000=2^32=4294967296 为unsigned long 类型 且等同于0,意味着在比较`input===user` 时能通过,而取出input[0]为null,强转换成0,加上0后使得uid仍然为0,这样就通过管理员登陆了。

challenge2

考察php弱类型与函数is_numeric的特性。
从逻辑上看,本题接收time参数,先使用is_numeric判断time是否为数字,再要求7776000>time>5184000,若这些都满足则sleep((int)time)。可以看到如果满足time大小要求的话则无法等待如此长的sleep时间。
我们知道is_numeric() 函数可以支持识别科学计数法与十六进制,而int不支持。那么根据php弱类型,如果是一个科学计数法形式2e10 在强制int转换后,就变成了2(当成字符串识别,截取第一个字母前的数字)。那么我们就可以构造一个处于规定大小之间的科学计数法就行了,如下
?time=0.7e7

challenge3

index.php

xxxxx/option.php

代码逻辑很简单,通常用作修改配置文件的地方。接收转义后的参数option,将它用正则替换到配置文件中去。
虽然进过转义了,但仍然有很多种解法。
第一种方法?option=aaa';%0aphpinfo();//
这样输入后配置文件中的代码会变为

如果此时,我们再替换一次,输入?option=aaa,那么此时匹配替换掉的是aaa\ 将转义用的反斜杠去掉了,那么下面的代码又能执行了。
第二种方法 ?option=aaa\';phpinfo();//
首先理解为什么要转义,转义是在字符串中将特殊字符前面加\消除它的特殊性.(比如想输入怕连着后面被识别成变量,那么直接输入\\就能直接匹配成option=’aaa\‘;phpinfo();//‘;

challenge5

考察无字母数字和一些符号的webshell,这类的webshell是利用特殊数据结构或字符构造一个字母,然后利用php特性遍历整个字母表。而且php函数可以通过$str1='assert';$str1('phpinfo();')方式调用,这样就构成了以上的webshell。经尝试上述方式适用于php5,php7在构造webshell的过程中会报错。
exp看这里

challenge6

经典的命令注入绕过,可以看到几乎所有隔开两个命令的字符都过滤了,不过还可以用%0a换行绕过。

challenge7

这题很简单,利用的是php弱类型,数字加e开头的字符串==比较时会默认当做科学计数法形式使用。正好这道题中md5(“admin1674227342”)又是’0e’开头的,按照科学计数法那么它的值为0,所有直接输入一个32位0e开头的不与上述MD5值相同就行了。

challenge8

利用了一个小tricks,反引号在mysql中主要是用作强调字段名或者表名与关键字区分,例如存在一个名为like的字段,那么在使用sql语句时就一定得用反引号括起来与关键字like区分起来。
而在这里利用的是desc table_name1 table_name2 只要table_name1存在,这个语句就不会报错,因此就可以绕过这里的限制。

challenge9

这是jarvisoj的一道原题,不过这道题得代入原始情境才能解出。(原题题可以扫出phpinfo文件。)
从魔法函数可以看出这是一个反序列化漏洞,
ini_set('session.serialize_handler', 'php')看到这个就想到了这个上的例子,题目设置序列化处理器不同于phpinfo中的php_serialize,由此就产生了漏洞。原因是未正确处理\’|\’,如果以php_serilize方式存入,比如我们构造出”|” 伪造的序列化值存入,但之后解析又是用的php处理器的话,那么将会反序列化伪造的数据(\’|\’之前当作键名,\’|\’之后当作键值)。
此时也可以看到phpinfo()中的session.upload_progress.enabled打开,php会记录上传文件的进度,在上传时会将其信息保存在$_SESSION中添加一组数据。所以可以通过Session Upload Progress来设置session。
通过以下的html构造post和上传的同时请求,以希望注入序列化内容

更详细的思路看这里

challenge10

php弱类型绕过。当$a为php://input,$data可以通过php://input来接受post数据。
$id传一个字符进去,满足!$_GET['id']为假。与数字进行比较时会被转换为0,满足$id==0
$b,要求长度大于5,其次要求满足eregi的要求和首字母不为4。可以设置$b%00111111,这样,substr()会发生截断,在匹配时时进行eregi(“111”,”1114”)满足,同时%00对strlen不会发生截断。
payload:

challenge11

这道题只要是1718年玩过ctf的人都见到过,一个很有趣的tricks。
可见前面是正常的连接数据库,然后接收password参数,使用md5后的值拼接sql语句判断是否为admin用户密码。这一切都看起来很正常,但是细心地同学能发现md5函数使用了第二个参数并且为true,这意味着md5函数将以原始16 字符二进制格式输出。当强转换成字符串时,就会将raw格式的数据转换成ascii值。这意味着,如果存在一个md5值raw格式转换成字符串时正好为'or'XXXXX 那么只要是'or'开头都能将sql语句where后面变成pass=''or'XXXX 那么是一定成立的,这样就完成了绕过。
当然这样的md5前的字符串别人已经帮我们找好了ffifdyop

challenge12

 

很简单的命令注入,可以看到eval里面的代码是字符串拼接进去的,直接闭合函数就行啦。
?hello=123);phpinfo();%23

 

 

challenge13


 

可以看到,目的是通过重重限制使得v1、v2、v3值为1。

v1处所涉及到的知识点是php弱类型比较时,会将2017a这样的以数字开头的字符串强制转换为2017。

v2处没有什么知识点,看懂代码就行。

v3处strcmp在比较数组时会返回null,eregi比较时存在%00截断。

payload:?foo={"bar1":"12345a","bar2":[[1],2,3,4,5]}&cat[1][]=1&cat[0]=xxxhtctf2016&dog=xxx%00

challenge14

sql注入题,首先没有转义单引号,过滤了很多关键词。一般情况下先判断账号后判断密码的注入登录我们可以在username处使用union注入,使得后面的密码正确。但是这道题过滤了union等关键字,使得无法union注入。

这里,这道题可以使用with rollup注入。with rollup 常常跟在group by 后面使用,用作汇总group by 的字段(会多返回一条数据),而如果group by pwd,那么多返回的汇总数据中pwd字段对应的数据为null。那么如果我们输入pwd为空,就能通过弱类型比较与null相等。

payload:uname=admin' group by pwd with rollup limit 2 offset 1 -- -&pwd=

challenge15


简单tricks,在php中md5和sha1等哈希函数在处理数组时会返回null。

payload:?name[]=1&password[]=2

challenge16

显然这里的练习排布顺序并不是很合理,这道题也是比较简单的注入。

没有任何过滤,password被md5。直接union注入即可。

payload:user=qwer' union select 'c4ca4238a0b923820dcc509a6f75849b'-- -&pass=1

challenge17

 

看到$$可以很明显是一道变量覆盖题。在对$_POST的foreach操作后,flag预设变量值会被输入的flag值覆盖,这说明我们必须要在flag变量值被改变前,将flag值转移到其他已有且可打印变量中。在这道题中我们可以选择变量_200。GET数据为_200=flag,POST数据为flag=1

challenge18

可以看到,本题未转义特殊字符,只是将html特殊字符编码。这样的话因为反斜杠并没有被编码,所以可以通过反斜杠注入。

payload:?username=admin\&password=or 1-- -

challenge20

 

可以看到这是一道sql注入的题,但是注入点为不常见的UPDATE句型中的table处。虽然我们不需要引号闭合即可注入,但是如代码所示我们可以看到sql语句不是处于一行,无法用单行注释符号注释掉后面的内容。所以注入限制在了update语句中。

但是显然,从打印出了mysql报错语句可以看出,这道题利用的是报错注入。

必须在update语句中找到子查询语句才能使用报错注入。经查阅,update的table也可以进行连接操作。即update t1 join t2 set t1.uname=t2.uname ;

那么我们就可以构造以下payload:?table=users join (select updatexml(1,concat(0x7e,(select group_concat(uname,pwd) from users)),1) as user ) tt on tt.user=uname

challenge21

目标:读取位于上一层目录的file_list.php。

strpos(str1,str2)返回字符串str2在字符串str1中的位置。而题目中比较采用的是弱类型比较!=,当str2在str1开头时返回的位置为0,那么弱类型比较就等于false。

payload:download.php?f=file_list/../../file_list.php

challenge22

这道题环境不好搭建,仅从代码和writeup分析。

首先代码分为两个文件,一个index.php和query.php。在index.php中可以通过curl发送查询数据给query.php,而且在最下面如果知道了key值可以直接命令执行。从query.php中我们可以猜测是sql注入题,可能可以注入出key值。可以看到,我们的输入内容被addslashes了而且还有重重过滤。但是,在query.php中,将输入值进行了parse_str,而parse_str函数在解析字符串的同时还会urldecode,这说明我们可以通过二次urldecode绕过addslashes。

最后就是关键字绕过了。

challenge23

wechall.net上的一道题,可以看到代码上面有个变量覆盖$$k = $v;

下面判断是否是admin用户登录,直接覆盖login变量就行。

payload:http://www.wechall.net/challenge/training/php/globals/globals.php?login[0]=admin&login[1]=2

challenge24

也是wechall.net上的题。题目以及代码注释得很清楚,一个投票小程序,每个人到100时会reset,而我们需要投某个人到111票才算过关。一开始以为是条件竞争题,后来才发现是注入题。

首先可以看到虽然使用了mysql_real_escape_string函数,但实际上,插入投票信息的sql语句并不需要引号闭合,而是反引号。所以我们可以直接用反引号闭合sql语句,并将一个人票数设为111,而限制投票函数noesc_stop100是先检查票数是否为111再判断是否大于100,所以直接设为111就可以了。

payload: barack`=111 -- -

challenge25

来自wechall.net,从代码可以看到,从GET接收参数eval,经过过滤参数输入到eval中,本体目标是让这个函数返回True。

最后的return是强类型比较,而在过滤掉引号的情况下,无法直接输入eval='1337'。然而在php手册中除了用引号表示字符串外,还有两种:

  • heredoc 语法结构
  • nowdoc 语法结构

heredoc语法:

其中q为任意标识符,前后意味着将字符串包围起来,而且还要注意换行。

所以payload为:index.php?eval=<<<q%0A1337%0Aq;%0A

challenge26

 

代码很简单,接收参数,不允许1-9之间的数字,而在最后弱类型等于3735929054。

弱类型等于,那么我们可以将它转换进制(php弱类型支持16进制、8进制等)。转换成16进制后,我们发现结果为0xdeadc0de没有1-9之间的数字。

challenge27

index.php

flag.php:

正则匹配只能输入字母数字的组合,所以无法闭合eval里的内容。从$$可以看到,我们可以看到任何变量的值,所以可以直接从$GLOBSLS中看到flag。

challenge28

很简单,md5(数组)会返回null,就绕过了。

challenge29

0ctf的一道web题,关于php随机数安全的。这道题逻辑很简单,输出一个rand和md5后的5个之后的rand,让我们预测那5个rand。

看了很多种解法,有本地爆破seeds来预测的https://github.com/p4-team/ctf/tree/master/2016-03-12-0ctf/rand_2

还有直接根据算法预测后面的随机数http://www.vuln.cn/6004

有关php伪随机数函数rand()和mt_rand()的预测在ctf题中也挺常见的,具体内容可以看这。由这篇博客我们可以看出,我们只要先访问超过30多次,那么就有足够的量来预测后面的随机数,当然,我们在里面也说过,生成的随机数会随机加上1,所以需要跑多次才能跑通。顺便在这里提醒一下复现这道题的同学,真实测试php5.6+apache2在保持连接的状态下访问101次不会重复为随机数函数种下种子,可到了php7.2+apache2时变成了8次,这意味着如果是php7.2将无法复现本题。

payload:

challenge30

题目来自Secuinside CTF 2017 Mathboy7

几经周转把这道题搞清楚了,这道题难以找到合适的writeup,只发现了一篇韩文写的

这道题原本mysql存在宽字节注入,但是单从代码中无法显示出来。现在虽然绕过addlashes可以注入了,但是从逻辑上看,本题要求最后的当id==’admin’时拿到flag,上面的writeup指出原题中user表是没有任何用户,这说明需要我们自己使用union注入出一行来。如下面所示:

id=%df%5c&pw=union select 'admin',true,true

虽然union和select都没有被过滤,但是明显,我们不能输入单引号,也不能输入数字(即无法使用16进制表示)。那么如何表示’admin’字符串呢?

首先,mysql中存在预设好的值比如pi()(圆周率)、version()user()等,经过运算能变成想要的数字,如下(来源)

 

而使用conv([10-36],10,36) 就能将上述数字转成对应字符,然后用concat将字符拼接。

当然,我们这道题并不能使用conv,所以得依靠别的方法了。

上面的韩文writeup是通过encrypt和password两个加密函数,通过加密上面数字生成随机序列,用mid函数将我们想要的字符串攫取出来。

首先,他发现password(pi())的第36个字符开始为’AD**‘,而encrypt第二个参数salt(只取前两位,后面的不算),用作加密的盐且会返回到加密后的数据中(比如encrypt(‘1234’,’gggg’),返回的加密后的数据前两位一定是’gg’)。这样

mid(password(pi()),floor(pi()*pi()*floor(pi()))+ceil(pi()+pi()))就等于’AD’开头的字符串。此时还差后面的min,可以尝试不断地用脚本自由组合encrypt,直到生成的加密字符串存在以m开头或者mi或者min,按照这种方法凑下去。

或者我还有一种方法,直接在encrypt生成的加密数据中找admin中的字母,找到便使用mid截取下来,最后使用concat函数连接就行啦。最后payload:

mid(encrypt(ceil(pi()*pi())*ceil(pi()*pi())*ceil(pi()*pi())*ceil(pi()*pi())*floor(pi())+ceil(pi()*pi())*ceil(pi()*pi())*ceil(pi()*pi())*ceil(pi())+ceil(pi()*pi())*ceil(pi()*pi())*floor(pi()*pi())+ceil(pi()*pi())*(floor(pi()*pi())-true),mid(password(pi()),floor(pi()*pi()*floor(pi()))+ceil(pi()+pi()))),true, (ceil(pi())+true))

challenge31

有点点绕,复现时也发现了urldecode的知识的模糊点。

以上代码主要是可以将以http://127.0.0.0开头的网页读取并写入任意文件中,包括了php文件。

很重要的是这里后面将console.log($path update successed!)打印了出来,这说明$path变量的内容可以完全打印到网页上。所以,如果这时我们将<?php phpinfo(); ?>作为path输入,$url为任意以http://127.0.0.1开头的值。那么返回的页面是含有<?php phpinfo(); ?>的。此时我们再以此页面为url写入到一个$path中,那么这个path如果是php文件,就可以执行上面的代码。

在这个过程中,对于urldecode我有了更深的理解。对于#?&=@+/:;%<>等字符在url中传输时,由于它们有特殊的作用(比如&分割参数、?代表参数开始、=分割参数与值、%用作url编码)。这意味着如果url中真的存在这类字符时,我们就得对他们进行编码(就是常见的%编码)。

回到本题,首先页面会返回含有php代码的url为:http://127.0.0.1/code-audit-challenge/cha31.php?path=<? php phpinfo(); ?>&file=http://127.0.0.1/code-audit-challenge/cha31.php

上面的url为了不造成混淆,最好把path中的问号和空格编码,就可以输入浏览器了。

将上述页面写入我们指定的php文件中的payload未编码状态为:?file=http://127.0.0.1/code-audit-challenge/cha31.php?path=<? php phpinfo(); ?>&file=http://127.0.0.1/code-audit-challenge/&path=hhh.php。我们可以看到,第一个file=直到第二个file=中间的数据都为第一个file的数据,其内容是要交给file_get_contents()读取的。这意味着我们得将其进行urlencode。

最终payload:

?file=http://127.0.0.1/code-audit-challenge%2fcha31.php%3fpath%3d%3c%3f%2520php%2520%2520phpinfo()%253b%2520%3f%3e%26file%3dhttp%3a%2f%2f127.0.0.1%2fcode-audit-challenge%2f&path=hhh.php

challenge32

challenge33

这道题来自于 WooYun Puzzle#3 。

本题逻辑是利用rand函数生成了6位的secret_key和csrf_token放在了session里,用户需要输入act和key,其中key是以secret_key为salt的hash值,act是函数名。至此,我们需要获取随机数secret_key,才能执行自己输入的函数,但是执行的函数无参数,后面的函数执行获取flag.php中的内容仅靠上述代码是无法完成的,这里仅复现secret_key猜解过程。

首先可以知道rand()函数伪随机序列是可以预测的,不清楚的可以看这篇文章。我们现在已知csrf_token,虽然secret_key的生成在csrf_token生成前,原来我们都是预测rand()函数后面生成的随机数,但是反向预测也是一样的。那么只要收集足够的csrf_token就可以反向预测出原来的secret_key值出来。

原理都在上述文章中展示出来了,下面是演示脚本

payload:

 

 

2 thoughts on “Code-audit-Challenge 代码审计练习笔记

发表评论

电子邮件地址不会被公开。 必填项已用*标注