前言

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版本的问题,比较重要的还有操作系统类型(主要是两大阵营win/*nix),WEB服务端软件(主要是iis/apache两大类型)等因素。这是由于不同的系统不同的WEB SERVER有着不同的安全特点或特性,下文有些部分会涉及。

所以我们在做某个公司WEB应用代码审计时,应该了解他们使用的系统,WEB服务端软件,PHP版本等信息。

扩展我们的字典

下面将详细介绍一些非传统PHP应用代码审计一些漏洞类型和利用技巧。

变量本身的key

说到变量的提交很多人只是看到了GET/POST/COOKIE等提交的变量的值,但是忘记了有的程序把变量本身的key也当变量提取给函数处理。

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

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

key.php?<script>alert(1);script>=1&bbb=2

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


漏洞审计策略
PHP版本要求:无
系统要求:无
审计策略:通读代码


变量覆盖

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

遍历初始化变量

请看如下代码:

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

很多的WEB应用都使用上面的方式(注意循环不一定是foreach),如Discuz!4.1的WAP部分的代码:

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


漏洞审计策略
PHP版本要求:无
系统要求:无
审计策略:通读代码


parse_str()变量覆盖漏洞

//var.php?var=new
$var
='init';
parse_str
($_SERVER['QUERY_STRING']);
print $var;

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

//var.php?var=1&a[1]=var1%3d222
$var1
='init';
parse_str
($a[$_GET['var']]);
print $var1;

上面的代码通过提交$var来实现对$var1的覆盖。


漏洞审计策略(parse_str)
PHP版本要求:无
系统要求:无
审计策略:查找字符parse_str



漏洞审计策略(mb_parse_str)
PHP版本要求:php4<4.4.7 php5<5.2.2
系统要求:无
审计策略:查找字符mb_parse_str


import_request_variables()变量覆盖漏洞

//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!';


漏洞审计策略(import_request_variables)
PHP版本要求:php4<4.4.1 php5<5.2.2
系统要求:无
审计策略:查找字符import_request_variables


PHP5 Globals

从严格意义上来说这个不可以算是PHP的漏洞,只能算是一个特性,测试代码:


// register_globals =ON
//foo.php?GLOBALS[foobar]=HELLO
php 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];

如果熟悉WEB2.0的***的同学,很容易想到上面的代码我们可以利用这个特性进行crsf***。


漏洞审计策略
PHP版本要求:无
系统要求:无
审计策略:通读代码


magic_quotes_gpc与代码安全


什么是magic_quotes_gpc

当打开时,所有的 '(单引号),"(双引号),\(反斜线)和 NULL 字符都会被自动加上一个反斜线进行转义。还有很多函数有类似的作用 如:addslashes()、mysql_escape_string()、mysql_real_escape_string()等,另外还有parse_str()后的变量也受magic_quotes_gpc的影响。目前大多数的主机都打开了这个选项,并且很多程序员也注意使用上面那些函数去过滤变量,这看上去很安全。很多漏洞查找者或者工具遇到些函数过滤后的变量直接就放弃,但是就在他们放弃的同时也放过很多致命的安全漏洞。 :)

哪些地方没有魔术引号的保护

1) $_SERVER变量

PHP5的$_SERVER变量缺少magic_quotes_gpc的保护,导致近年来X-Forwarded-For的漏洞猛暴,所以很多程序员考虑过滤X-Forwarded-For,但是其他的变量呢?


漏洞审计策略($_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等等)


变量的编码与解码

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

这个类型的主要函数有:

1) stripslashes() 这个其实就是一个decode-addslashes()

2) 其他字符串转换函数:


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版本要求:无
系统要求:无
审计策略:查找对应的编码函数


二次***

详细见附录[1]

1)数据库出来的变量没有进行过滤

2)数据库的转义符号:

  • mysql/oracle转义符号同样是\(我们提交'通过魔术引号变化为\',当我们update进入数据库时,通过转义变为')

  • mssql的转义字符为'(所以我们提交'通过魔术引号变化为\',mssql会把它当为一个字符串直接处理,所以魔术引号对于mssql的注射没有任何意义)

从这里我们可以思考得到一个结论:一切进入函数的变量都是有害的,另外利用二次***我们可以实现一个webrootkit,把我们的恶意构造直接放到数据库里。我们应当把这样的代码看成一个vul?


漏洞审计策略
PHP版本要求:无
系统要求:无
审计策略:通读代码


魔术引号带来的新的安全问题

首先我们看下魔术引号的处理机制:

[\-->\\,'-->\',"-->\",null-->\0]

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

1)得到原字符(',\,",null])

$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)得到“\”字符

$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='
and1=1/*'


漏洞审计策略
PHP版本要求:无
系统要求:无
审计策略:查找字符串处理函数如substr或者通读代码


变量key与魔术引号

我们最在这一节的开头就提到了变量key,PHP的魔术引号对它有什么影响呢?

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

1)当magic_quotes_gpc = On时,在php5.24下测试显示:

aaaa\'aaa
bb\'b

从上面结果可以看出来,在设置了magic_quotes_gpc = On下,变量key受魔术引号影响。但是在php4和php<5.2.1的版本中,不处理数组第一维变量的key,测试代码如下:

php
//key.php?aaaa'aaa[bb']=1
print_R
($_GET);
?>

结果显示:

Array([aaaa'aaa] => Array ( [bb\'] => 1 ) ) 

数组第一维变量的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等)


变量函数与双引号

对于单引号和双引号的区别,很多程序员深有体会,示例代码:

echo "$a\n";
echo
'$a\n';

我们再看如下代码:

//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文件里,这样很容易被人注射变量函数。

漏洞审计策略
PHP版本要求:无
系统要求:无
审计策略:通读代码


PHP自身函数漏洞及缺陷


PHP函数的溢出漏洞

大家还记得Stefan Esser大牛的Month of PHP Bugs(MOPB见附录2)项目么,其中比较有名的要算是unserialize(),代码如下:

unserialize(stripslashes($HTTP_COOKIE_VARS[$cookiename .'_data']);

在以往的PHP版本里,很多函数都曾经出现过溢出漏洞,所以我们在审计应用程序漏洞的时候不要忘记了测试目标使用的PHP版本信息。


漏洞审计策略
PHP版本要求:对应fix的版本
系统要求:
审计策略:查找对应函数名


PHP函数的其他漏洞

Stefan Esser大牛发现的漏洞:unset()--Zend_Hash_Del_Key_Or_Index Vulnerability 比如phpwind早期的serarch.php里的代码:

unset($uids);
......
$query
=$db->query("SELECT uid FROM pw_members WHERE username LIKE '$pwuser'");
while($member=$db->fetch_array($query)){
       $uids
.= $member['uid'].',';
}
$uids
? $uids=substr($uids,0,-1): $sqlwhere.=' AND 0 ';
........
$query
= $db->query("SELECT DISTINCT t.tid FROM $sqltable WHERE $sqlwhere $orderby $limit");


漏洞审计策略
PHP版本要求:php4<4.3 php5<5.14
系统要求:无
审计策略:查找unset


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等等。


漏洞审计策略
PHP版本要求:具体不详
系统要求:无
审计策略:查找session_destroy


随机函数

1) rand() VS mt_rand()

php
//on windows
print mt_getrandmax();//2147483647
print getrandmax();// 32767
?>

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

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

当我们的程序使用rand处理session时,***者很容易暴力破解出你的session,但是对于mt_rand是很难单纯的暴力的。


漏洞审计策略
PHP版本要求:无
系统要求:无
审计策略:查找rand