PHP代码审计中的一些Tips
1.eregi
此函数可以被%00截断
比如下面这个例子,可以使用$b=”%001111”
1 | //%00好像算一个字节 |
此外,如果传入数组,函数会报错并返回NULL。
2.assert
PHP中的assert可以用来执行PHP函数,进而进行getshell等操作,比如我们利用如下代码进行目录扫描
1 | <?php |
payload s=print_r(scandir('./'));
3.md5&sha1
PHP中的md5和sha1函数存在两个问题,第一是他们处理数组都返回null;第二在弱类型条件下他们会认为如下的返回值相同
1 | QNKCDZO |
注意:如果使用了md5并且是强相等,那么找到数据对应md5相同的值即可,在此给出一组某强网杯使用过的数据
1 | $Param1="\x4d\xc9\x68\xff\x0e\xe3\x5c\x20\x95\x72\xd4\x77\x7b\x72\x15\x87\xd3\x6f\xa7\xb2\x1b\xdc\x56\xb7\x4a\x3d\xc0\x78\x3e\x7b\x95\x18\xaf\xbf\xa2\x00\xa8\x28\x4b\xf3\x6e\x8e\x4b\x55\xb3\x5f\x42\x75\x93\xd8\x49\x67\x6d\xa0\xd1\x55\x5d\x83\x60\xfb\x5f\x07\xfe\xa2"; |
此外我们观察定义可以得到另外一点,通过设置raw_output参数的值为true,我们可以达到一个\,从而进行sql注入
string md5 ( string $str [, bool $raw_output = false ] )
str
原始字符串。raw_output
如果可选的 raw_output 被设置为 TRUE,那么 MD5 报文摘要将以16字节长度的原始二进制格式返回。
1 | php > var_dump(md5(128,true)); |
参考: SQL injection with raw MD5 hashes
4.strcmp
注:5.3之前版本的php存在如下问题
当这个函数接受到了不符合的类型,这个函数将发生错误并返回0,因而可以使用数组绕过验证
1 | <?php |
5.ereg
ereg()函数只能处理字符,如果传入数组将返回null
6.strpos
strpos()的参数不能够是数组,所以处理数组返回的是null
strpos()与PHP的自动类型转换结合也会出在哪问题
如下:
1 | var_dump(strpos('abcd','a')); # 0 |
并且由于PHP的自动类型转换的关系,0和false是相等的
1 | var_dump(0==false); # true |
例题:
1 | class Login { |
传入的username和password的首位字符是<或者是>就可以绕过限制,那么最后的pyaload就是:
1 | username=<"><injected-tag%20property="&password=<"><injected-tag%20property=" |
最终传入到$this->login($xmlElement)的$xmlElement值是<xml><user="<"><injected-tag property=""/><pass="<"><injected-tag property=""/></xml>
这样就可以进行注入了。
7.is_numeric
is_numeric()函数对于空字符%00,无论是%00放在前后都可以判断为非数值,而%20空格字符只能放在数值后。
此外,is_numeric()函数在判断是否是数字时会忽略字符串开头的’ ‘、’\t’、’\n’、’\r’、’\v’、’\f’。而’.’可以出现在任意位置,E、e能出现在参数中间,仍可以被判断为数字。也就是说is_numeric(“\r\n\t 0.1e2”) >> TRUE
8.ord
ord()函数返回字符串的首个字符的 ASCII 值
例如下面这道题目,我们可以用16进制绕过限制
1 | <?php |
9.科学计数法
1 | strlen($_GET['password']) < 8 && $_GET['password'] > 9999999 |
10.in_array
语法:in_array(search,array,type)
参数 | 描述 |
---|---|
search | 必需。规定要在数组搜索的值。 |
array | 必需。规定要搜索的数组。 |
type | 可选。如果设置该参数为 true,则检查搜索的数据与数组的值的类型是否相同。 |
注意:in_array()的第三个参数在默认情况下是false,因此 PHP 会尝试将文件名自动转换为整数再进行判断,导致该判断可被绕过。
例如如下代码在13 行存在任意文件上传漏洞。 在 12 行代码通过 in_array()
来判断文件名是否为整数,可是未将 in_array()
的第三个参数设置为 true 。in_array()
的第三个参数在默认情况下是false,因此 PHP 会尝试将文件名自动转换为整数再进行判断,导致该判断可被绕过。比如使用文件名为 5vulnspy.php 的文件将可以成功通过 in_array($this->file['name'], $this->whitelist)
判断,从而将恶意的 PHP 文件上传到服务器。
1 | class Challenge { |
测试
1 | $myarray = range(1,24); |
注:array_search()与in_array()也是一样的问题。
11.filter_var
filter_var()的URL过滤非常的弱,只是单纯的从形式上检测并没有检测协议。测试如下:
1 | var_dump(filter_var('vulnspy.com', FILTER_VALIDATE_URL)); # false |
这种情况下可以采用如下payloadjavascript://comment%250aalert(1)
来触发XSS
注:%250a即%0a表示换行符,上面的payload会被换行,并且//表示注释。最终触发后将得到如下形式
1 | javascript://comment |
12.class_exist
以class_exist()为例的下列函数会在在PHP 5.4以下版本中存在任意文件包含漏洞
1 | call_user_func() |
注:class_exists()会检查是否存在对应的类,当调用class_exists()函数时会触发用户定义的autoload()函数,用于加载找不到的类(这个特性是因为class_exists函数中的$autoload参数,默认是true造成的)。所以如果我们输入../../../../etc/passwd是,就会调用class_exists(),这样就会触发autoload(),这样就是一个任意文件包含的漏洞了。
注: PHP5~5.3(包含5.3版本)版本 之间才可以使用路径穿越符号”../“。
此外,还存在一个blind xxe的漏洞,由于存在class_exists(),所以我们可以调用PHP的内置函数,并且通过$controller = new $controllerName($data);进行实例化。借助与PHP中的SimpleXMLElement类来完成XXE攻击。
xxe漏洞实例参考:
我是如何黑掉“Pornhub”来寻求乐趣和赢得10000$的奖金
参考:
stackoverflow:class_exists&autoload:
13.mail
mail()中的第五个参数可以-X的方式写入webshell。
payload:`example@example.com -OQueueDirectory=/tmp -X/var/www/html/rce.php`
这个PoC的功能是在Web目录中生成一个PHP webshell。该文件(rce.php)包含受到PHP代码污染的日志信息
escapeshellarg()和filter_var()不安全的问题参考在PHP应用程序开发中不正当使用mail()函数引发的血案
escapeshellarg和escapeshellcmd联合使用从而造成的安全问题参考PHP escapeshellarg()+escapeshellcmd() 之殇
14.正则表达式可能存在问题
(1)
如本意想将非a-z、.、-、全部替换为空,但是正则表达式写成了`[^a-z.-]`,其中没有对-进行转义,那么-表示一个列表,例如[1-9]表示的数字1到9,但是如果[1-9]表示就是字母1、-和9。所以[^a-z.-_]表示的就是非ascii表中的序号为46至122的字母替换为空。那么此时的../…/就不会被匹配,就可以进行目录穿越,从而造成任意文件删除了。
(2)在反序列化漏洞中对于preg_match('/O:\d:/', $data)
这样的正则可以采用在对象长度前添加一个+号,即o:14->o:+14来进行绕过。
15.parse_str
parse_str()可以在参数可控的情况下可以造成变量覆盖漏洞
例如:
1 | $var = parse_url($_SERVER['HTTP_REFERER']); |
16.preg_replace
preg_replace() /e 模式可以执行任意代码,例子如下
1 | header("Content-Type: text/plain"); |
preg_replace的参数含义参考 PHP手册–preg_replace
在此处我们可以看到有两处的值是我们可以操控的,但是只有在’strtolower(“\1”)’这个位置的参数才可以执行代码,所以关键就在这儿。 \1是具有特殊含义的,在这儿就是就是指定第一个子匹配项,也即${phpinfo()},进而达到执行代码的目的
参考文章:
17.str_replace
str_replace()函数是单次替换而不是多次替换,因而可以通过双写敏感词汇过滤,例如:
1 | str_replace('../', '', $language); |
18.header
使用header()进行跳转的时候没有使用exit()或者是die(),导致后续的代码任然可以执行。如果后面存在危险函数,那么将会触发漏洞。
例如:
1 | extract($_POST); |
此处就可以POST一个pi=phpinfo()
来借助assert()函数触发代码执行漏洞
19.intval
特性1:
intval()函数执行成功时返回 变量的10进制值,失败时返回 0。空的 array 返回 0,非空的 array 返回 1。
特性2:
整数溢出、向下取整和整形判断存在问题
(1)整数溢出
32位系统最大的带符号范围为-2147483648 到 2147483647,64位最大的是 9223372036854775807。因此,在32位系统上 intval(‘1000000000000’) 会返回 2147483647
(2)向下取整
intval(10.99999)会返回10,intval和int等取整都是’截断’取整,并不是四舍五入
(3)intval函数进去取整时,是直到遇上数字或者正负号才开始进行转换,之后在遇到非数字或者结束符号(\0)时结束转换
特性3:
intval()函数会忽略’’ ‘\n’、’\r’、’\t’、’\v’、’\0’ ,也就是说intval(“\r\n\t 12”) >> 12
20.htmlentities
htmlentities默认情况下不会对单引号进行转义。
21.addslashes
在进行了addslashes之后进行了截断,在一些情况下就有可能能够获得一个引号。
比如:
1 | function sanitizeInput($input, $length = 20) { |
此处输出的刚好是带有一个\,而’则因为长度限制被截断,从而可以出发SQL注入漏洞
22.小特性
(1)php自身在解析请求的时候,如果参数名字中包含空格、.、[这几个字符,会将他们转换成_。但是通过$_SERVER['REQUEST_URI']
方式获得的参数并不会进行转换。
(2)$_REQUEST是直接从GET,POST 和 COOKIE中取值,不是他们的引用。即使后续GET,POST 和 COOKIE发生了变化,也不会影响$_REQUEST的结果
参考:
(3)PHP中的""
是可以执行代码的,因而在payload中常采用"<?php phpinfo();>"
23. ++
PHP中的自增符号++在如下情况中不会有任何意义
1 | $test=123; echo $test++; # 123 |
因此像下面代码所示的一样,就可能回产生变量覆盖漏洞
1 | foreach ($input as $field => $count) { |
提示:当然如果++$count形式的话,也是可以存在变量覆盖的,因为在进行++操作时会进行隐式类型转换,如果能够转换成功,则会进行加法操作;如果不能转换成功,则将最后一个字符进行加法操作。
示例:
1 | $test = 123; echo ++$test; // 124 |
24.openssl_verify
依据openssl_verify()的定义有
如果签名正确返回 1, 签名错误返回 0, 内部发生错误则返回-1.
如果单独采用如下形式的判断就会出现问题,因为if判断只有遇到0或者是false返回的才是false。
1 | if (openssl_verify($data, $signature, $pub)) { |
25.stripcslashes
stripcslashes函数
返回反转义后的字符串。可识别类似 C 语言的 \n,\r,… 八进制以及十六进制的描述。
1 | var_dump(stripcslashes('0\073\163\154\145\145\160\0405\073')); // 0;sleep 5; |
因而对于下面这种形式我们可以采用将命令转换为八进制的形式进行绕过正则判断并触发命令执行
1 | function createThumbnail() { |
26.set_error_handler
若错误配置此函数,将会造成信息泄露进而造成漏洞产生,比如:
1 | set_error_handler(function ($no, $str, $file, $line) { |
这里的设置就相当于
1 | error_reporting(E_ALL); |
这种配置将会泄露所有的错误信息
27.declare与array_walk
针对PHP7版本
PHP7中引入了declare(strict_types=1);这种声明方式,在进行函数调用的时候会进行参数类型检查。如果参数类型不匹配则函数不会被调用。1
2
3
4
5
6
7
8declare(strict_types=1);
function addnum(int $a,int $b) {
return $a+$b;
}
$result = addnum(1,2);
var_dump($result); // 输出3
$result = addnum('1','2');
var_dump($result); //出现Fatal error: Uncaught TypeError,Argument 1 passed to addnum() must be of the type integer, string given,程序出错,参数的数据类型不匹配
但是通过array_walk()调用的函数会忽略掉严格模式还是按照之前的php的类型转换的方式调用函数。1
2
3
4
5
6
7declare(strict_types=1);
function addnum(int &$value) {
$value = $value+1;
}
$input = array('3a','4b');
array_walk($input,addnum);
var_dump($input);//array(4,5)
因此利用array_walk()的这种特性,我们可以传入任意字符进去,进而触发相应的漏洞。
28.ldap_escape
string ldap_escape ( string $value [, string $ignore [, int $flags ]] )
value
The value to escape.
ignore
Characters to ignore when escaping.
flags
The context the escaped string will be used in: LDAP_ESCAPE_FILTER for filters to be used with ldap_search(), or LDAP_ESCAPE_DN for DNs.
当使用ldap_search()时需要选择LDAP_ESCAPE_FILTER过滤字符串,但是如果选择LDAP_ESCAPE_DN将会导致过滤无效
29.strip_tags && preg_replace
例如:
1 | $textMsg = trim(strip_tags(preg_replace('/<(head|title|style|script)[^>]*>.*?<\/\\1>/s','',$message))); |
目的很明显–去掉所有的标签和内容。首先使用preg_replace
过滤掉标签、标签内容、标签属性,接着又使用strip_tags
去掉其余的html和php标记。
正常输入没问题–如:<head>evil</head>
,得到的结果是空,即全部都被过滤了。
结果很悲惨–如果攻击者输入<head>evil</headend>
或者<he<>ad>evil</head>
之类,就会导致evil字符串逃逸,攻击者利用evil字符串再结合上下文说不定就能够造成漏洞。
30.escapeshellarg && escapeshellcmd
escapeshellarg 将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号
escapeshellcmd 会对&#;|*?~<>^()[]{}$\, \x0A 和 \xFF进行转义,’和 “仅在不配对儿的时候被转义
需要注意的是escapeshellarg和escapeshellcmd在win平台和linux平台的表现是不一样的。他们两者造成的漏洞也主要是在Linux平台下。接下来主要是说明在Linux平台下的情况
1 | $msg = "123'456"; |
当两者混合使用时,就会出现问题。代码如下:
1 | $parameter1 = escapeshellarg($parameter) |
假设我们传入的$parameter为172.17.0.2' -v -d a=1
,那么经过escapeshellarg之后变为'172.17.0.2'\'' -v -d a=1'
。之后经过escapeshellcmd变为'172.17.0.2'\\'' -v -d a=1\'
,此时\的存在后面得’不会被转义,所以后面的两个’’变为了空白字符。那么最后实际的命令为curl 172.17.0.2\ -v -d a=1'
,成功地逃逸了单引号。
这两个函数联合使用之后可以造成单引号逃逸,这样就很有可能会造成漏洞,利用的方式就需要看具体的应用场景了。
31.addslashes && basename
basename的主要用法是:
给出一个包含有指向一个文件的全路径的字符串,basename()函数返回基本的文件名。如果是在windows环境下,路径中的斜线(/)和反斜线()都可以用作目录分割符,在其他环境下是斜线(/)
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15在 win平台下
$mypath1 = 'C:/Users/monkey/1.txt';
$name1 = basename($mypath1);
var_dump($name1); // 1.txt
$mypath2 = 'C:\Users\monkey\2.txt';
$name2 = basename($mypath2);
var_dump($name2); // 2.txt
在Linux平台下
$mypath1 = 'C:/Users/monkey/1.txt';
$name1 = basename($mypath1);
var_dump($name1); // 1.txt
$mypath2 = 'C:\Users\monkey\2.txt';
$name2 = basename($mypath2);
var_dump($name2); // C:\Users\monkey\2.txt
注:
- 不一定是需要addslashes,只需要是进行了转义即可
- 此方式的利用需要在win平台下。因为在win平台下,\/都可以作为basename的分隔符,但是在Linux平台下只有/可以作为分隔符,而addslashes会增加一个\。所以只能在win平台下使用。
漏洞演示如下:1
2
3
4
5$filename = "123'456.png";
$filename = addslashes($filename);
var_dump($filename); //结果是 123\'456.png
$filename = basename($filename);
var_dump($filename); // 结果是 '456.png
通过例子可以看到,成功地逃逸了反斜线,单引号也保留了。
如果存在如下的代码:
1 | // 对输入进行转义 |
此时,如果我们输入的image的参数为123' and if(1.sleep(3),0)#
,最后的imagename的值为' and if(1.sleep(3),0)#
,sql语句为UPDATE table SET image = '' or if(1.sleep(3),0)#'where id=1
形成了一个盲注。
32.explode && preg_replace
注:这种漏洞常见于文件上传
explode()用法:
array explode ( string $delimiter , string $string [, int $limit ] ),此函数返回由字符串组成的数组,每个元素都是 string 的一个子串,它们被字符串 delimiter 作为边界点分割出来。
示例:
1 | $pizza = "piece1 piece2 piece3 piece4 piece5 piece6"; |
这两个函数造成的漏洞其实就是一个任意文件上传,由于preg_replace()过滤了特殊字符,导致能够逃逸出php这种后缀,而explode()用以取文件名,最后取得的就是错误的文件后缀。
29~32的漏洞实例&参考:
33.PHP可变变量与变量执行
在花括号内的代码是可以执行的
1 | //这种写法在php5.4.45以下的版本中都是无法执行的,但是在之后的版本都是可行 |
在任何php的版本中都可以执行的方法:
1 | "${ phpinfo()}"; 第一个字符为空格) |
原理:空格,tab,注释,回车是各种语法分析引擎中常见的分割字符,@是PHP语法的一个特殊的容错符号,所以可变变量内的花括号有这么一个规则,需要判断花括号内的内容是否为真正的代码,条件即是文本的第一个字符串是否为PHP语法解析引擎的分割字符和特殊的语法符号!
参考:
34.PHP String Offset
关于字符串offset取值特性的一段描述[详见[2]]:
String access and modification by character
Characters within strings may be accessed and modified by specifying the
zero-based offset of the desired character after the string using square array
brackets, as in $str[42]. Think of a string as an array of characters for this
purpose. The functions substr() and substr_replace() can be used when you want
to extract or replace more than 1 character.Note: Strings may also be accessed using braces, as in $str{42}, for the same purpose.
说白了就是php中的字符串也可以像数组一样进行取值。比如:
1 | $test = "hello world"; |
但是它的自动转换问题决定了下面的输出也是h:
1 | //pass被自动转换为0 |
漏洞示例:
1 | //下面这段代码是在在phpspy2006中用于判断登录时所使用的代码 |
这样的验证逻辑如果利用上述的特性就很容易地就可以被绕过。$admin没有被初始定义为数组类型,那么当我们用字符串提交时phpsyp.php?admin=1abc时,php会取字符串1xxx的第一位,成功绕过if的条件判断。
与之类似的还有php4fun中的第五题
参考:
35.switch
如果switch是数字类型的case的判断时,switch会将其中的参数转换为int类型。如下:
1 | $i ="2abc"; |
36.file_put_contents&&unlink
此处指代file_put_contents、copy、file_get_contents等读取写入操作与unlink、file_exists等删除判断文件函数之间对于路径处理的差异导致的删除绕过
示例:
1 | <?php |
以下援引自P牛:
查看php源码,其实我们能发现,php读取、写入文件,都会调用php_stream_open_wrapper_ex来打开流,而判断文件存在、重命名、删除文件等操作则无需打开文件流。
我们跟一跟php_stream_open_wrapper_ex就会发现,其实最后会使用tsrm_realpath函数来将filename给标准化成一个绝对路径。而文件删除等操作则不会,这就是二者的区别。
所以,如果我们传入的是文件名中包含一个不存在的路径,写入的时候因为会处理掉“../”等相对路径,所以不会出错;判断、删除的时候因为不会处理,所以就会出现“No such file or directory”的错误。
因此linux可以通过xxxxx/../test.php、test.php/. windows可以通过test.php:test test.ph<来绕过文件删除
此外发现还可以使用伪协议php://filter/resource=1.php在file_ge_contents、copy等中读取文件内容,却可以绕过文件删除
37.浮点数问题
当小数小于10^-16后,PHP对于小数的判断就出现问题了,PHP7也是如此
1 | var_dump(1.000000000000000 == 1) >> TRUE |
38.filter_var&parse_url=>ssrf
参考:
39.file_get_contents
通过file_get_contents获取网页内容并返回到客户端有可能造成xss
例如如下代码
1 | if(filter_var($argv[1], FILTER_VALIDATE_URL)) { |
虽然通过filter_var函数对url的格式进行检查,并且使用正则对url的host进行限定
但是可以通过data://baidu.com/plain;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pgo=
页面会将<script>alert(1)</script>
返回给客户端,就有可能造成xss
40.敏感配置项
1.register_globals
php版本小于5.4时存在
当该配置项为ON时,会把用户通过GET、POST提交的参数自动注册成全局变量。当代码中存在有未初始化的变量时,可能会导致变量覆盖的问题
注:其中参数覆盖的顺序受到配置文件中variables_order的参数影响,默认是EGPCS。按顺序,右边的参数来源会覆盖左边的的参数来源
2.allow_url_include
php版本大于5.2默认为off
当该配置项为ON时,可以通过include、require等函数进行远程文件包含
类似的还有allow_url_fopen,这个参数配置为on的时候可以使用file_get_contents函数打开url
allow_url_include和allow_url_fopen当两个配置项都为ON的时候,可以直接使用url进行远程包含,当include为ON,fopen为OFF时,只能通过php伪协议进行包含
3.magic_quato_gpc
php版本小于5.4存在
此配置项为ON的时候会对GET、POST、COOKIE变量中的单引号(‘)、双引号(“)、反斜杠(\)、空字符(NULL)前添加反斜杠进行转义。
注意:这个配置并不会对SERVER变量里的特殊字符进行转义,因此可能会导致referer、client-ip存在注入等漏洞
4.magic_quato_runtime
php版本小于5.4存在
这个配置和magic_quato_gpc的区别就在于runtime是对从数据库或者文件中取出的数据进行转义,因此只对例如file()、fgets()、fread()、mysql_fetch_array()等很多对数据库查询和文件读取的函数产生影响
5.magic_quato_sybase
php版本小于5.4存在
这个配置和magic_quato_gpc 的区别在于,sybase只会转义空字符,把单引号转为双引号,并且这个配置如果为ON会覆盖gpc的配置
6.open_basedir
这个配置用来设置限定php程序只能访问哪些目录。在windows下,多个目录用分号(;)分割,linux下用冒号(:)进行分割。注意的是配置的目录需要用斜杠(/)进行封尾,否则就变成了前缀匹配。例如,配置/var/test,那么/var/test和/var/test123都是可以进行访问的,如果指定一个确定的目录就要写成/var/test/
参考:《PHP代码审计小结》
41.parse_url
parse_url
函数在解析 url 时存在的bug,通过:////x.php?key=value
的方式可以使其返回False。
1 | zeroyu@zeros ~ uname -a |
参考:《跨次元CTF》
42.$
$$key = $value;中key有两个$,这会获取到的数组键名作为变量,数组中的键值就成了变量的值
1 | <!--foreach($_GET as $key => $value){ |
43.extract
可能存在变量覆盖漏洞
1 | <?php |
例如:
1 |
|
payload
1 | ?flag=&gift= |
44.create_function
不用创建新函数来达到执行的目的,直接使用如下代码达到RCE
1 | create_function('', $_GET['code']); |
具体场景中的绕过问题
1 | 1. 如果可控在第一个参数,需要闭合圆括号和大括号:create_function('){}phpinfo();//', ''); |