PHP-Audit-Labs-Day13-特定场合下addslashes函数的绕过

PHP-Audit-Labs

项目地址:https://www.ripstech.com/php-security-calendar-2017/

Turkey Baster

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
class LoginManager {
private $em;
private $user;
private $password;

public function __construct($user, $password) {
$this->em = DoctrineManager::getEntityManager();
$this->user = $user;
$this->password = $password;
}

public function isValid() {
$user = $this->sanitizeInput($this->user);
$pass = $this->sanitizeInput($this->password);

$queryBuilder = $this->em->createQueryBuilder()
->select("COUNT(p)")
->from("User", "u")
->where("user = '$user' AND password = '$pass'");
$query = $queryBuilder->getQuery();
return boolval($query->getSingleScalarResult());
}

public function sanitizeInput($input, $length = 20) {
$input = addslashes($input);
if (strlen($input) > $length) {
$input = substr($input, 0, $length);
}
return $input;
}
}

$auth = new LoginManager($_POST['user'], $_POST['passwd']);
if (!$auth->isValid()) {
exit;
}

这是一道典型的用户登录程序,从代码来看,考察的应该是通过 SQL注入 绕过登陆验证。代码 第33行 ,通过 POST 方式传入 userpasswd 两个参数,通过 isValid() 来判断登陆是否合法。我们跟进一下 isValid() 这个函数,该函数主要功能代码在 第12行-第22行 ,我们看到 13行14行 调用 sanitizeInput() 针对 userpassword 进行相关处理。

跟进一下 sanitizeInput() ,主要功能代码在 第24行-第29行 ,这里针对输入的数据调用 addslashes 函数进行处理,然后再针对处理后的内容进行长度的判断,如果长度大于20,就只截取前20个字符。 addslashes 函数定义如下:

addslashes — 使用反斜线引用字符串

1
2
> string addslashes ( string $str )
>

作用:在单引号(')、双引号(")、反斜线(\)与 NUL( NULL 字符)字符之前加上反斜线。

我们来看个例子:

img

那这题已经过滤了单引号,正常情况下是没有注入了,那为什么还能导致注入了,原因实际上出在了 substr 函数,我们先看这个函数的定义:

substr — 返回字符串的子串

1
2
> string substr ( string $string , int $start [, int $length ] )
>

作用:返回字符串 stringstartlength 参数指定的子字符串。

我们来看个例子:

img

那么再回到这里,我们知道反斜杠可以取消特殊字符的用法,而注入想要通过单引号闭合,在这道题里势必会引入反斜杠。所以我们能否在反斜杠与单引号之间截断掉,只留一个反斜杠呢?答案是可以,我们看个以下这个例子。

img

在这个例子中,我们直接使用题目代码中的过滤代码,并且成功在反斜杠和单引号之间截断了,那我们把这个payload带入到题目代码中,拼接一下 第17行-第19行 代码中的sql语句。

1
select count(p) from user u where user = '1234567890123456789\' AND password = '$pass'

这里的sql语句由于反斜杠的原因, user = '1234567890123456789' 最后这个单引号便失去了它的作用。这里我们让 pass=or 1=1# ,那么最后的sql语句如下:

1
select count(p) from user where user = '1234567890123456789\' AND password = 'or 1=1#'

这时候在此SQL语句中, user 值为 1234567890123456789' AND password = ,因此我们可以保证带入数据库执行的结果为 True ,然后就能够顺利地通过验证。

所以这题最后的 payload 如下所示:

1
user=1234567890123456789'&passwd=or 1=1#

CTF

index.php

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
43
44
45
46
47
48
49
50
51
52
<?php
require 'db.inc.php';
function dhtmlspecialchars($string) {
if (is_array($string)) {
foreach ($string as $key => $val) {
$string[$key] = dhtmlspecialchars($val);
}
}
else {
$string = str_replace(array('&', '"', '<', '>', '(', ')'), array('&amp;', '&quot;', '&lt;', '&gt;', '(', ')'), $string);
if (strpos($string, '&amp;#') !== false) {
$string = preg_replace('/&amp;((#(\d{3,5}|x[a-fA-F0-9]{4}));)/', '&\\1', $string);
}
}
return $string;
}
function dowith_sql($str) {
$check = preg_match('/select|insert|update|delete|\'|\/\*|\*|\.\.\/|\.\/|union|into|load_file|outfile/is', $str);
if ($check) {
echo "非法字符!";
exit();
}
return $str;
}
// 经过第一个waf处理
foreach ($_REQUEST as $key => $value) {
$_REQUEST[$key] = dowith_sql($value);
}
// 经过第二个WAF处理
$request_uri = explode("?", $_SERVER['REQUEST_URI']);
if (isset($request_uri[1])) {
$rewrite_url = explode("&", $request_uri[1]);
foreach ($rewrite_url as $key => $value) {
$_value = explode("=", $value);
if (isset($_value[1])) {
$_REQUEST[$_value[0]] = dhtmlspecialchars(addslashes($_value[1]));
}
}
}
// 业务处理
if (isset($_REQUEST['submit'])) {
$user_id = $_REQUEST['i_d'];
$sql = "select * from ctf.users where id=$user_id";
$result=mysql_query($sql);
while($row = mysql_fetch_array($result))
{
echo "<tr>";
echo "<td>" . $row['name'] . "</td>";
echo "</tr>";
}
}
?>

db.inc.php

1
2
3
4
5
6
7
<?php
$mysql_server_name="localhost";
$mysql_database="ctf"; /** 数据库的名称 */
$mysql_username="root"; /** MySQL数据库用户名 */
$mysql_password="root"; /** MySQL数据库密码 */
$conn = mysql_connect($mysql_server_name, $mysql_username,$mysql_password,'utf-8');
?>

ctf.sql

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
# Host: localhost  (Version: 5.5.53)
# Date: 2018-08-18 21:42:20
# Generator: MySQL-Front 5.3 (Build 4.234)

/*!40101 SET NAMES utf8 */;

#
# Structure for table "users"
#

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`pass` varchar(255) DEFAULT NULL,
`flag` varchar(255) DEFAULT NULL,
PRIMARY KEY (`Id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

#
# Data for table "users"
#

/*!40000 ALTER TABLE `users` DISABLE KEYS */;
INSERT INTO `users` VALUES (1,'admin','qwer!@#zxca','hrctf{R3qu3st_Is_1nterEst1ng}');
/*!40000 ALTER TABLE `users` ENABLE KEYS */;

在做Day13之前,我们先来了解一些需要用到的基础知识。

撸大师曾经提到过一个php特性: http://wooyun.org/bugs/wooyun-2010-064792
php自身在解析请求的时候,如果参数名字中包含” “、”.”、”[“这几个字符,会将他们转换成下划线。

  • 对于传入的非法的 $_GET 数组参数名,PHP会将他们替换成 下划线 。经过fuzz,有以下这些字符:

img

  • 当我们使用HPP(HTTP参数污染)传入多个相同参数给服务器时,PHP只会接收到后者的值。(这一特性和中间件有关系)

img

  • 通过 $_SERVER['REQUEST_URI'] 方式获得的参数,并不会对参数中的某些特殊字符进行替换。

img

这里的代码中有两个waf。

第一个WAF在代码 第29行-第30行 ,这里面采用了 dowith_sql() 函数,跟进一下 dowith_sql() 函数,该函数主要功能代码在 第19-第26行 ,如果 $_REQUEST 数组中的数据存在 select|insert|update|delete 等敏感关键字或者是字符,则直接 exit() 。如果不存在,则原字符串返回。

img

而第二个WAF在代码 第33行-第39行 ,这部分代码通过 $_SERVER['REQUEST_URI'] 的方式获取参数,然后使用 explode 函数针对 & 进行分割,获取到每个参数的参数名和参数值。然后针对每个参数值调用 dhtmlspecialchars() 函数进行过滤。

img

跟进一下 dhtmlspecialchars() 函数,发现其相关功能代码在 第3行-第14行 ,这个函数主要功能是针对 '&', '"', '<', '>', '(', ')' 等特殊字符进行过滤替换,最后返回替换后的内容。从 第44行和第45行 的代码中,我们可以看到这题的参数都是通过 REQUEST 方式获取。我们可以先来看个例子:

img

第一次 $_REQUEST 仅仅只会输出 i_d=2 的原因是因为php自动将 i.d 替换成了 i_d 。而根据我们前面说的第二个特性,PHP取最后一个参数对应的值,因此第一次 $_REQUEST 输出的是2。

第二次 $_REQUEST 会输出 i_d=select&i.d=2 是因为 $_SERVER['REQUEST_URI'] 并不会对特殊的符号进行替换,因此结果会原封不动的输出。所以这题的payload可以根据下面这个思维导图进行构造:

img

  • 我们通过页面请求 i_d=padyload&i.d=123
  • 当数据流到达第一个WAF时,php会将参数中的某些特殊符号替换为下划线。因此便得到了两个 i_d ,所以此时的payload变成了 i_d=payload&i_d=123
  • 前面我们介绍了,如果参数相同的情况下,默认 第二个参数传入的值 会覆盖 第一个参数传入的值 。因此此时在第一个WAF中 i_d=123 ,不存在其他特殊的字符,因此绕过了第一个WAF。
  • 当数据流到达进入到第二个WAF时,由于代码是通过 $_SERVER['REQUEST_URI'] 取参数,而我们前面开头的第三个知识点已经介绍过了 $_SERVER['REQUEST_URI'] 是不会将参数中的特殊符号进行转换,因此这里的 i.d 参数并不会被替换为 i_d ,所以此时正常来说 i.di_d 都能经过第二个WAF。
  • 第二个WAF中有一个 dhtmlspecialchars() 函数,这里需要绕过它,其实很好绕过。绕过之后 i_d=payload&i.d=123 便会进入到业务层代码中,执行SQL语句,由于这里的SQL语句采用拼接的方式,因此存在SQL注入。

因此最后payload如下:

1
http://127.0.0.1/index.php?submit=&i_d=-1/**/union/**/select/**/1,flag,3,4/**/from/**/ctf.users&i.d=123

img

转载:

https://xz.aliyun.com/t/3178

https://bugs.leavesongs.com/php/%E8%B4%B7%E9%BD%90%E4%B9%90%E7%B3%BB%E7%BB%9F%E6%9C%80%E6%96%B0%E7%89%88sql%E6%B3%A8%E5%85%A5%EF%BC%88%E6%97%A0%E9%9C%80%E7%99%BB%E5%BD%95%E7%BB%95%E8%BF%87waf%E5%8F%AFunion-select%E8%B7%A8%E8%A1%A8%E6%9F%A5%E8%AF%A2%EF%BC%89/

-------------本文结束感谢您的阅读-------------