DTale代码审计-从身份认证绕过到RCE

D-Tale 是 Flask 后端和 React 前端的组合,为您提供了一种查看和分析 Pandas 数据结构的简便方法,允许用户方便地浏览和分析数据,而无需编写复杂的代码。Dtale 可以在 Jupyter Notebook 中或者独立的网页中运行,使得分析过程更加直观和高效。该系统存在身份验证绕过和RCE漏洞

D-Tale 是 Flask 后端和 React 前端的组合,为您提供了一种查看和分析 Pandas 数据结构的简便方法,允许用户方便地浏览和分析数据,而无需编写复杂的代码。Dtale 可以在 Jupyter Notebook 中或者独立的网页中运行,使得分析过程更加直观和高效。该系统存在身份验证绕过和RCE漏洞

1、身份认证绕过

Dtale 提供了身份验证系统,由于Dtale的web服务基于Flask的。在 Flask 中,SECRET_KEY 是一个非常重要的配置项,它用于对会话(session)数据进行加密和签名,以防止未经授权的篡改。如果攻击者知道了应用的 SECRET_KEY,他们就能够伪造合法的会话并绕过应用的身份验证或其他重要的安全机制。

image-20241030154833293

通过查看源码,发现SECRET_KEY是被硬编码的,在 Flask 中,会话数据默认存储在客户端,通过一个名为 session 的 cookie 发送给客户端。在存储之前,Flask 会对会话数据进行序列化并加密,然后使用 SECRET_KEY 进行签名。这样我们就可以通过SECRET_KEY伪造session进而绕过认证系统

image-20241030155029335

Flask Session 的组成结构主要由三部分构成,第一部分为 Session Data ,即会话数据。第二部分为 Timestamp ,即时间戳。第三部分为 Cryptographic Hash ,即加密哈希。

要伪造cookie,首先要知道session的格式,对于已经登录的系统,我们可以对其第一部分的 Session Data进行解密,它实际为base64编码后的结果,然后对其中的相关字段进行修改后再进行签名加密。对于不能登录的系统,可以分析其源码,以下为Dtale登录部分的逻辑

image-20241030171613440

通过/login路由,会首先通过authenticate方法判断用户名密码是否正确,如果正确,然后使用session 来存储登录状态和用户名。所以session的格式为:{'username':'asd','logged_in':'true'}

然后使用flask-session-cookie-manager工具来伪造session

image-20241030171929607

最后替换session即可成功登录系统

image-20241030172042715

2、远程命令执行

在Pandas中,query 方法用于基于表达式对 DataFrame 进行筛选。它允许以更直观和简洁的方式编写查询条件。但实际上,query()方法内部实现依赖于Python的eval()函数,eval() 函数用来执行一个字符串表达式,并返回表达式的值。

理论上我们可以执行任意python代码,但经过实验发现并不行,并报错ValueError: "import" is not a supported function

image-20241028153336458

这是因为query会首先会将其中的字符串转换为抽象语法树,由于__import__并不是当前上下文中本地变量里的,所以在访问时会报错UndefinedVariableError,进而进入下面的异常捕获的流程

image-20241024145417150

FuncNode中会判断该变量是否在MATHOPS中,在MATHOPS中定义了一些常见的numpy库筛选和计算的函数,显然__import__没在其中。

image-20241024145521029

image-20241024145618549

既然无法直接执行,那可以尝试反射的方法去调用__import__方法,进而包含任意库从而RCE

在query中可以使用age > 30这种基本用法外,还可以使用@ 用于引用函数外部的变量,也就是说,当你需要在 query 表达式中使用当前作用域中的变量时,可以用 @ 来访问。例如下面这个例子

import pandas as pd

data = {'age': [25, 30, 45, 35],
        'name': ['Alice', 'Bob', 'Charlie', 'David']}
df = pd.DataFrame(data)

# 定义一个外部变量
age_threshold = 30

# 在 query 中使用外部变量
result = df.query('age > @age_threshold')
print(result)

这样就可以灵活的使用程序中的变量进行筛选条件。

image-20241028151805453

既然可以使用@引用函数外部的变量,我们就可以通过pandas库中反射调用基类,进而调用__import__。这里可以搜索builtins,因为builtinspython的内建模块,且builtins中具有__import__方法

image-20241031201524715

image-20241029154827941

通过搜索在pandas库中有两处都引用了builtins,我们挑第一个来构造链,其路径在core/common.py下,所以构造链为

@pd.core.common.builtins.__import__("os").system("calc")

接下来就是寻找如何触发query进而RCE。

进入后台后,可以可视化的分析Pandas 数据结构,但是这里我们使用筛选选择自定义过滤器时,这里会提示Custom Filtering is currently disabled.,并提示需要在启动代码中加入对应的配置项enable_custom_filters=True才可以使用。

image-20241030175016911

首先查看对应的代码,搜索当前路由/popup,可以看到代码最终调用了base_render_template对模板进行渲染

@dtale.route("/popup/<popup_type>")
@dtale.route("/popup/<popup_type>/<data_id>")
def view_popup(popup_type, data_id=None):
    """
    :class:`flask:flask.Flask` route which serves up a base jinja template for any popup, additionally forwards any
    request parameters as input to template.

    :param popup_type: type of popup to be opened. Possible values: charts, correlations, describe, histogram, instances
    :type popup_type: str
    :param data_id: integer string identifier for a D-Tale process's data
    :type data_id: str
    :return: HTML
    """
    ...
    return base_render_template(
        "dtale/popup.html",
        data_id,
        title=title,
        popup_title=popup_title,
        js_prefix=popup_type,
        grid_links=grid_links,
        back_to_data=text("Back To Data"),
    )

base_render_template下或获取当前配置中的各种参数,并将参数带入渲染模板

image-20241031113130273

image-20241031113233809

所以说这里所有的配置参数的值最后都是渲染在了前端页面上,那我们就可以直接抓包修改返回包即可绕过。

image-20241031135944133

但是当我们执行自定义的Filter时还是会被提示 not enabled!,通过查看该路由的后端代码,发现在后端也对enable_custom_filters的值进行了验证。

image-20241031140050119

image-20241031140933568

但是我们找到了真正执行filter的地方时通过run_query方法进而在该方法内用query执行传递的参数的。所以可以在这个文件内搜索run_query看看那些地方还调用了该方法

image-20241101170755929

最后找到这样一处,在这个方法下并没有二次验证base_render_template的值,直接从get请求中获取query的值带入run_query方法。看到此路由信息为/chart-data,请求http://127.0.0.1:40000/dtale/chart-data/1?query=@pd.core.frame.com.builtins.__import__(%22os%22).system(%22calc%22)即可触发。

image-20241101171104904

3、修复方法

在3.13.1中修复了身份认证绕过漏洞,将SECRET_KEY的值设置成了随机数

image-20241031202258040

在3.14.1中修复了RCE漏洞,增加了对base_render_template的验证

image-20241031202626535

  • 发表于 2025-02-10 09:49:12
  • 阅读 ( 24243 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
中铁13层打工人
中铁13层打工人

79 篇文章

站长统计