PHP Code Reviewing Learning
2020-12-13 03:01
标签:des discuz style class blog code 相关学习资料 目录 1. 前言 PHP是一种被广泛使用的脚本语言,尤其适合于web开发。具有跨平台,容易学习,功能强大等特点,据统计全世界有超过34%的网站有php的应用,包括Yahoo、sina、163、sohu等大型门户网站。而且很多具名的web应用系统(包括bbs,blog,wiki,cms等等)都是使用php开发的,Discuz、phpwind、phpbb、vbb、wordpress、boblog等等。随着web安全的热点升级,php应用程序的代码安全问题也逐步兴盛起来,越来越多的安全人员投入到这个领域,越来越多的应用程序代码漏洞被披露。 2. 传统的代码审计技术 WEB应用程序漏洞查找基本上是围绕两个元素展开:变量与函数。也就是说一漏洞的利用必须把你提交的恶意代码通过变量经过n次变量转换传递,最终传递给目标函数执行,还记得MS那句经典的名言吗?"一切输入都是有害的"。 3. PHP版本与应用代码审计 到目前为止,PHP主要有3个版本:php4、php5、php6,使用比例大致如下: 由于php缺少自动升级的机制,导致目前PHP版本并存,也导致很多存在漏洞没有被修补。这些有漏洞的函数也是我们进行WEB应用程序代码审计的重点对象,也是我们字典重要来源。 4. 其他的因素与应用代码审计 5. 扩展我们的字典 5.1 变量本身的key 上面的代码就提取了变量本身的key显示出来,单纯对于上面的代码,如果我们提交URL: 那么就导致一个xss的漏洞,扩展一下如果这个key提交给include()等函数或者sql查询呢?:) 5.2 变量覆盖 很多的WEB应用都使用上面的方式,如Discuz!4.1的WAP部分的代码 以及DEDECMS的common.inc.php CMS中的这些代码模块对PHP的本地变量注册进行了模拟实现,自然也引入了同样的变量覆盖安全问题。 0x1: parse_str()变量覆盖漏洞 该函数一样可以覆盖数组变量,上面的代码是通过$_SERVER‘QUERY_STRING‘来提取变量的,对于指定了变量名的我们可以通过注射"="来实现覆盖其他的变量: 上面的代码通过提交$var来实现对$var1的覆盖。 0x2: import_request_variables()变量覆盖漏洞 这个函数可能导致的漏洞如下: 5.3 magic_quotes_gpc与代码安全 当打开时,所有的 ‘(单引号),"(双引号),\(反斜线)和 NULL 字符都会被自动加上一个反斜线进行转义。还有很多函数有类似的作用 如:addslashes()、mysql_escape_string()、mysql_real_escape_string()等。 但是,PHP中存在某些地方是不受magic_quotes_gpc保护的,认识到这点很重要,因为从安全控制的最佳实践来说,最好的做法就是将某类安全处理代码块封装到一个API中,并在程序中所有涉及到这类风险的位置应用这个API,理论上说,magic_quotes_gpc也应该是要这样,但事实上却不是这样,PHP中不受magic_quotes_gpc保护的变量有: 总的来说,magic_quotes_gpc存在两个主要的问题 所以在PHP5.4之后,PHP就停止了对magic_quotes_gpc的支持,而鼓励开发者遵循最佳安全开发实践来对输入变量进行处理。 0x1: 变量的编码与解码 变量和编码本身没有明显的漏洞,它带来的问题是会隐藏攻击者的payload意图,导致WAF、IDS等防御策略失效。 0x2: 魔术引号/转义带来的新的安全问题 这给我们引进了一个非常有用的符号"\","\"符号不仅仅是转义符号,在WIN系统下也是目录转跳的符号(只截取"\"后面的内容)。这个特点可能导致php应用程序里产生非常有意思的漏洞: 5.4 代码注射 0x1: PHP中可能导致代码注射的函数 例如: 0x2: 变量函数与双引号 我们再看如下代码: 5.5 PHP自身函数漏洞及缺陷 0x1: PHP函数的溢出漏洞 0x2: session_destroy()删除文件漏洞 当我们提交构造cookie:PHPSESSID=/../1.php,相当于unlink(‘sess_/../1.php‘)这样就通过注射../转跳目录删除任意文件了。很多著名的程序某些版本都受影响如phpmyadmin,sablog,phpwind3等等。 0x3: 随机函数 1. 随机数密文空间长度问题: rand() VS mt_rand() 可以看出rand()最大的随机数是32767,这个很容易被我们暴力破解。 当我们的程序使用rand处理session时,攻击者很容易暴力破解出你的session,但是对于mt_rand是很难单纯的暴力的。 我们发现,这个新生成的$passwd,是直接调用了microtime()后,取其MD5值的前6位。由于MD5是单向的哈希函数,因此只需遍历microtime()的值,再按照同样的算法(这就是算法逆向的思想),即可猜解出$passwd的值。 因此只需要获取到服务器的系统时间,就可以以此时间作为"基数",按次序递增,即可猜解出新生成的密码。因此这个算法是存在非常严重的设计缺陷的,程序员预想的随机生成密码,其实并未随机。 (思考: 攻击者能利用这个microtime()的弱随机性漏洞进行基于时间"基数"的穷举的一个最重要的前提就是攻击者要获取到服务器的系统时间,也就是在发起攻击前要获取到尽可能靠近关键点的时间,比如说在生成cookie的那一瞬间会取一次microtime(),我们攻击者的目的就是要"穷举"出cookie生成的那一瞬间的microtime(),为了达到这个目的,我们就必须要尽可能的获取到尽可能靠近那个取值点的时间,才能有效的进行"时间"穷举,否则如果时间间隔太大,穷举就会很没效率,还可能触发警报) 如果调用时不带可选参数,本函数以 "msec sec" 的格式返回一个字符串,其中 sec 是自 Unix 纪元(0:00:00 January 1, 1970 GMT)起到现在的秒数,msec 是微秒部分。字符串的两部分都是以秒为单位返回的。 我们发现,后面的"秒数部分"基本是一样的(要达到这点需要攻击者能够做到在关键的附近获取到microtime)。我们要做的就是不断的穷举前面的毫秒部分。 2) 随即发生器种子问题: mt_srand()/srand()-weak seeding(by Stefan Esser) 伪随机数是由数学算法实现的,它真正随机的地方在于"种子(seed)"。种子一旦确定后,再通过同一个伪随机数算法计算出来的随机数,其值是固定,多次计算所得值的顺序也是固定的(也就是说,只要种子seed是相同的,之后产生的伪随机数序列就是相同的)。 我们可以直接调用mt_rand(),系统会自动播种。但是有的时候,程序猿为了和以前的PHP版本兼容,PHP代码中经常会这样写 这种播种的写法其实是由缺陷的,且不说time()是可以被攻击者获知的,使用microtime()获得的种子范围其实也不是很大。 变化的范围在0~1000000之间,猜解100万次即可遍历所有的种子。 如果是在同一进程中(apache不能重启),则同一个seed(这个是关键前提)每次通过mt_rand()生成的值都是固定的。 多次访问得到的结果都是一样的,也就是说,当seed确定时,1~N次通过mt_rand()产生的值都没有发生变化。 建立在这个基础上,就可以得到一种针对随机数种子的可行的攻击方式: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. 怎么进一步寻找新的字典
针对这样一个状况,很多应用程序的官方都成立了安全部门,或者雇佣安全人员进行代码审计,因此出现了很多自动化商业化的代码审计工具。也就是这样的形势导致了一个局面:大公司的产品安全系数大大的提高,那些很明显的漏洞基本灭绝了,那些大家都知道的审计技术都无用武之地了。
我们面对很多工具以及大牛扫描过n遍的代码,有很多的安全人员有点悲观,而有的官方安全人员也非常的放心自己的代码,但是不要忘记了"没有绝对的安全",我们应该去寻找新的途径挖掘新的漏洞。本文就给介绍了一些非传统的技术经验和大家分享。
这句话只强调了变量输入,很多程序员把"输入"理解为只是gpc[$_GET,$_POST,$_COOKIE],但是变量在传递过程产生了n多的变化。导致很多过滤只是个"纸老虎"!我们换句话来描叙下代码安全:"一切进入函数的变量是有害的"。
PHP代码审计技术用的最多也是目前的主力方法:静态分析,主要也是通过查找容易导致安全漏洞的危险函数,常用的如grep,findstr等搜索工具,很多自动化工具也是使用正则来搜索这些函数。下面列举一些常用的函数,也就是下文说的字典。但是目前基本已有的字典很难找到漏洞,所以我们需要扩展我们的字典,这些字典也是本文主要探讨的。
其他的方法有:通过修改PHP源代码来分析变量流程,或者hook危险的函数来实现对应用程序代码的审核,但是这些也依靠了我们上面提到的字典。
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版本的问题,比较重要的还有操作系统类型(主要是两大阵营win/*nix),WEB服务端软件(主要是iis/apache两大类型)等因素。这是由于不同的系统不同的WEB SERVER有着不同的安全特点或特性,下文有些部分会涉及。
所以我们在做某个公司WEB应用代码审计时,应该了解他们使用的系统,WEB服务端软件,PHP版本等信息。
下面将详细介绍一些非传统PHP应用代码审计一些漏洞类型和利用技巧。
说到变量的提交很多人只是看到了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";
}
?>
http://localhost/test/key.php?=1&bbb=2
很多的漏洞查找者都知道extract()这个函数在指定参数为EXTR_OVERWRITE或者没有指定函数可以导致变量覆盖,但是还有很多其他情况导致变量覆盖的如:遍历初始化变量
请看如下代码:php
//var.php?a=fuck
$a=‘hi‘;
foreach($_GET as $key => $value)
{
$$key = $value;
}
print $a;
?>
$chs = ‘‘;
if($_POST && $charset != ‘utf-8‘)
{
$chs = new Chinese(‘UTF-8‘, $charset);
foreach($_POST as $key => $value)
{
$$key = $chs->Convert($value);
}
unset($chs);
...
foreach(Array(‘_GET‘,‘_POST‘,‘_COOKIE‘) as $_request)
{
foreach($$_request as $_k => $_v)
{
if($_k == ‘nvarname‘)
{
${$_k} = $_v;
}
else
{
${$_k} = _RunMagicQuotes($_v);
}
}
}
php
//var.php?var=new
$var = ‘init‘;
parse_str($_SERVER[‘QUERY_STRING‘]);
print $var;
?>
访问:
http://localhost/test/var.php?var=new
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
http://cn2.php.net/manual/zh/function.parse-str.php
将 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
首先,我们需要明白的是magic_quotes_gpc的版本情况PHP 5.3.0之前有效
PHP 5.3.0起废弃
PHP 5.4.0起移除,即不管任何设置均无效
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)");1. 宽字节错误导致的注入
2. 覆盖度不完整,没有对程序中所有的输入变量都应用安全处理
一个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()
首先我们看下魔术引号的处理机制:1. \-->\2. ‘-->\‘
3. "-->\"
4. null-->\0
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/*‘
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));
对于单引号和双引号的区别,我们需要仔细理解,例如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方式加载进来,这样,如果被加载的文件是变量函数(例如${${...}}这种类型的),这就容易被人注射变量函数进而代码执行了。
大家还记得Stefan Esser大牛的Month of PHP Bugs项目么,其中比较有名的要算是unserialize(),代码如下:
unserialize(stripslashes($HTTP_COOKIE_VARS[$cookiename . ‘_data‘]);
在以往的PHP版本里,很多函数都曾经出现过溢出漏洞,所以我们在审计应用程序漏洞的时候不要忘记了测试目标使用的PHP版本信息http://www.php-security.org/
测试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);
}
?>
总体来说,PHP、以及其他的语言中,和随机数有关的漏洞有以下两种:1. 随机数密文空间长度问题
2. 随即发生器种子问题
php
//on windows
print mt_getrandmax(); //2147483647
echo "";
print getrandmax();// 32767
?>
php
$a= md5(rand());
for($i=0;$i32767;$i++)
{
if(md5($i) ==$a )
{
print $i."-->ok!!
";
exit;
}
else
{
print $i."
";
}
}
?>
当然,凡是也不是绝对的,我们说mt_rand()抗穷举性更强也是基于攻击者完全不具有对目标随机系统先验知识的情况下而言的。我们来思考下面这个场景:
比如下面的代码,其逻辑是用户取回密码时,会由系统随机生成一个新的密码,并发送到用户的邮箱:function sendPSW()
{
....
$messenger = &$this->system->loadModel(‘system/messenger‘);
echo microtime() . "
";
$passwd = substr(md5(print_r(microtime(), true)), 0, 6);
}
PHP中的microtime()有两个值合并而成,一个是微秒数,一个是系统当前秒数。http://www.w3school.com.cn/php/func_date_microtime.asp
在这个案例中,生成密码的前一行,直接调用了microtime()并返回在当前页面上,这又使得攻击者以非常低的成本获得了服务器时间;且两次调用microtime()的时间间隔非常短,因此必然是在同一秒内,攻击者只需要猜解微秒数即可。
这点在发送攻击前一定要注意,因为每种攻击一般都会有一些必要的成立条件的。http://www.w3school.com.cn/php/func_date_microtime.asp
0.68454800 1382964876
0.68459400 1382964876
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);
?>
在PHP 4.2.0之前的版本中,是需要通过srand()或mt_srand()给rand()、mt_rand()"播种"的。
在PHP 4.2.0之后的版本中,不在需要事先通过srand()、mt_srand()来"播种"。
mt_srand();
mt_srand((double) microtime() * 100000);
mt_srand((double) microtime() * 1000000);
mt_srand((double) microtime() * 10000000);
0 double) microtime() 1
----->
0 double) microtime() * 1000000 1000000
在PHP 4.2.0之后的版本中,如果没有通过播种函数指定seed,而直接调用mt_rand(),则系统会分配一个默认的种子(默认不是指一个定值,这个值也是随机的)。在32位系统上默认的播种的种子最大值是2^32,因此最多只需要尝试2^32次就可以破解seed。php
mt_srand(1);
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
?>
1244335972
15217923
1546885062
2002651684
2135443977
1865258162
1509498899
21454231701) 通过穷举方法猜解出种子的值
2) 通过mt_srand()对猜解出的种子值进行播种
3) 通过还原程序逻辑,计算出对用的mt_rand()产生的伪随机数的值
php
mt_srand((double) microtime() * 1000000);
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
?>
每次访问都会得到不同的随机数值,这是因为种子每次都会发生变化。
假设攻击者已知第一个随机数的值:154176006(这在实际情况中很常见,即攻击者只能获得伪随机序列中的一部分的值,要去猜测剩下的其他值),如何猜解出剩下的几个随机数呢?只需要猜解出当前用的种子即可。
php
if($seed = get_seed())
{
echo "seed is: " . $seed . "\n";
mt_srand($seed);
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
echo mt_rand() . "
";
}
//逆向算法的逻辑,猜解出种子值
function get_seed()
{
for($i = 0; $i 1000000; $i++)
{
mt_srand($i);
//
上一篇:JS明确指定函数的接受者