SSTI学习
1,什么是模板
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。
2,什么是SSTI
SSTI(Server-Side Template Injection)——服务器端模板注入。比如python中的flask、php的thinkphp、java的spring等框架一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。
漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。其影响范围主要取决于模版引擎的复杂性。
看了下例子,形式上有点像做题题型,根据题型去选择对应的方法。内容上感觉像是xss
先跟着wp做一题看看是什么情况
[HNCTF 2022 WEEK2]ez_SSTI
非常干净,源代码里也没有提示。但题目上给这题打的Flask框架
尝试传参
用变量标识符包裹,发现可以识别 然后就是将payload中注入python函数
?name={{config.__class__.__init__.__globals__[%27os%27].popen(%27cat%20flag%27).read()}}
3,SSTI的攻击方式
SSTI由于不同的模板,其语法也会有所不同吗,后文以twig为例。
如果用此模板去攻击其他模板,通常是不行的。
1,检测是否存在SSTI
如上面的例子一样,当我们传入正常的name参数时,整个网页会显示正常。看起来就像平常的get传参拼接一样,但是当我们用特定的模板语法构成的payload被成功解析时,那么就可以判断这里存在SSTI注入。如果没有可能就是其他模板或者不存在SSTI。
比如在源码中看到类似
<h1>hello {{name}}</h1>
之类的,传入一个name参数能被成显示,如果传入{{2*2}},能被成功解析为4,那八九不离十就是SSTI注入了
2,发起攻击
在找到注入点后,就能像sql那样发起攻击了。原理就是利用该模板的函数进行文件的增删读改。然后因为模板会将盖变量输出,所以我们也就能够看见结果了。
由于模板有多种,每种payload都是不同的。而且随着php版本不同,模板的更新,会出现新的方法,同时之前的旧方法可能会失效。需要在过程中不断积累。
4,Flask框架下的攻击
由于有很多的模板,这里就以Python的Flask框架为例子,学习SSTI。
这种漏洞多半是由于字符转义不严格,代码直接将用户输入输出,通过构造恶意语句执行命令。
@app.route('/test2', defaults={'name': 'kobe'})
@app.route('/test2/<name>')#动态路由获取name参数
def test2(name):data='''<html><body>{{str}}</body></html>'''return render_template_string(data,str=name)
以上述代码为例,创建了一个名为test2的路由,并且是动态路由,通过路由获取name的参数
在flask中,用{{}}来标识变量。然后返回重新渲染过的data数据,并标记变量str是name
因为渲染时,特殊字符会被转义,所以这样就不会被当作变量解析。
但如果我们直接将变量拼接到数据中就会造成恶意输入被解析
@app.route('/test1',methods=['GET','POST'])#get传参获取参数
def test1():name = request.args.get('name',default='kobe')data='''<html><head></head><body>hello {}</body></html>'''.format(name)return render_template_string(data)
比如我们用format格式化字符串 what_can_i_say!! 这样被成功解析后就能用继承关系找环境中的可以用魔术方法拿flag
__class__:查找当前类型所属对象
__base__:查找上一级父类
__mro__:查找当前类对象的所有继承类
__subclasses__():查找父类下的所有子类
__init__:查看类是否重载(程序在运行时就已经加载好了这个模块到内存中),如果出现wrapper说明没有重载
__globals__:函数会以字典的形式返回当前对象的全部全局变量
主要流程时找os._wrap_close,然后再全局变量中找:
__buitins__:对所有内置标识符直接访问
eval():内置函数,计算字符串表达式的值
popen():执行一个shell以运行命令
先获取一个类 然后一直访问它的父类,也就是一直套__base__,知道object(object应该是所有类的父类,再访问object的所有子类就能找到有用的方法)
然后把找到的所有类,用任何办法,找到os._wrap_close这个方法
然后以数组的形式访问 然后看查该方法是否重载,如果没有重载就需要换一个方法,因该是
只要返回出地址就代表已经重载了
然后再看查当前对象的全局变量 然后可以用浏览器的搜索框,看是否存在这三种全局变量
__buitins__,eval(),popen()
然后传入payload,就可以任意命令执行了
{{"".__class__.__base__.__subclasses__()[159].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}也可以直接用popen{{"".__class__.__base__.__subclasses__()[159].__init__.__globals__['popen']('ls').read()}}
当然SSTI还有很多模板,可以用如下图示判断是否是这几种主流模板之一