D-Tale 是 Flask 后端和 React 前端的组合,为您提供了一种查看和分析 Pandas 数据结构的简便方法,允许用户方便地浏览和分析数据,而无需编写复杂的代码。Dtale 可以在 Jupyter Notebook 中或者独立的网页中运行,使得分析过程更加直观和高效。该系统存在身份验证绕过和RCE漏洞
Dtale 提供了身份验证系统,由于Dtale的web服务基于Flask的。在 Flask 中,SECRET_KEY
是一个非常重要的配置项,它用于对会话(session)数据进行加密和签名,以防止未经授权的篡改。如果攻击者知道了应用的 SECRET_KEY
,他们就能够伪造合法的会话并绕过应用的身份验证或其他重要的安全机制。
通过查看源码,发现SECRET_KEY
是被硬编码的,在 Flask 中,会话数据默认存储在客户端,通过一个名为 session
的 cookie 发送给客户端。在存储之前,Flask 会对会话数据进行序列化并加密,然后使用 SECRET_KEY
进行签名。这样我们就可以通过SECRET_KEY
伪造session进而绕过认证系统
Flask Session 的组成结构主要由三部分构成,第一部分为 Session Data ,即会话数据。第二部分为 Timestamp ,即时间戳。第三部分为 Cryptographic Hash ,即加密哈希。
要伪造cookie,首先要知道session的格式,对于已经登录的系统,我们可以对其第一部分的 Session Data进行解密,它实际为base64编码后的结果,然后对其中的相关字段进行修改后再进行签名加密。对于不能登录的系统,可以分析其源码,以下为Dtale登录部分的逻辑
通过/login
路由,会首先通过authenticate
方法判断用户名密码是否正确,如果正确,然后使用session
来存储登录状态和用户名。所以session的格式为:{'username':'asd','logged_in':'true'}
。
然后使用flask-session-cookie-manager工具来伪造session
最后替换session即可成功登录系统
在Pandas中,query
方法用于基于表达式对 DataFrame 进行筛选。它允许以更直观和简洁的方式编写查询条件。但实际上,query()
方法内部实现依赖于Python的eval()
函数,eval()
函数用来执行一个字符串表达式,并返回表达式的值。
理论上我们可以执行任意python代码,但经过实验发现并不行,并报错ValueError: "import" is not a supported function
这是因为query
会首先会将其中的字符串转换为抽象语法树,由于__import__
并不是当前上下文中本地变量里的,所以在访问时会报错UndefinedVariableError
,进而进入下面的异常捕获的流程
在FuncNode
中会判断该变量是否在MATHOPS
中,在MATHOPS
中定义了一些常见的numpy库筛选和计算的函数,显然__import__
没在其中。
既然无法直接执行,那可以尝试反射的方法去调用__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)
这样就可以灵活的使用程序中的变量进行筛选条件。
既然可以使用@
引用函数外部的变量,我们就可以通过pandas
库中反射调用基类,进而调用__import__
。这里可以搜索builtins
,因为builtins
是python的内建模块,且builtins
中具有__import__
方法
通过搜索在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
才可以使用。
首先查看对应的代码,搜索当前路由/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
下或获取当前配置中的各种参数,并将参数带入渲染模板
所以说这里所有的配置参数的值最后都是渲染在了前端页面上,那我们就可以直接抓包修改返回包即可绕过。
但是当我们执行自定义的Filter时还是会被提示 not enabled!,通过查看该路由的后端代码,发现在后端也对enable_custom_filters
的值进行了验证。
但是我们找到了真正执行filter的地方时通过run_query
方法进而在该方法内用query
执行传递的参数的。所以可以在这个文件内搜索run_query
看看那些地方还调用了该方法
最后找到这样一处,在这个方法下并没有二次验证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)
即可触发。
在3.13.1中修复了身份认证绕过漏洞,将SECRET_KEY
的值设置成了随机数
在3.14.1中修复了RCE漏洞,增加了对base_render_template
的验证
79 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!