关于疑点,这里是通过Redis PubSub 发送的序列化数据,感谢P牛指点,也就是说,如果拿到了docker网络权限,则可以通过JumpServer向目标设备发送命令,因为他token是固定写在Redis中的,利用中间人攻击即可?未经测试,仅是猜想
简单复现
先简单做下复现,同https://wh0am1i.com/2024/03/30/JumpServer-CVE-2024-29201-CVE-2024-29202/ 中一样配置完成环境并进行传值后,命令成功被执行
在向JumpServer添加playbook,向playbook传值的过程中经过了以下几个过程
- 新建Playbook
- 向Playbook中添加main.yml
- 创建作业
- 执行作业
可以看到,这里经过了以下几个步骤: - 生成一个Playbook ID
- 以PATCH方式向<playbook id="">/file 传值</playbook>
- 将资产ID与Playbook相绑定,获取一个job id,如果这里资产ID错误,则无法绑定
- 执行这个job,并获取一个task id
数据流分析
这个时候来到服务器端进行分析,这里可以看到JumpServer是基于docker进行的,一共有10个容器,如果你是没有进行任何修改的JumpServer服务器,这里可以看到他向外部映射的端口只有80, 2222, 33061, 33062, 63790这四个端口
其余都是通过Docker网桥互通的,并不对外映射,这里可以看到一共存在10个veth分别对应10个应用容器。
我们在整个复现过程中,只向80端口,也就是jms_web传值便完成了整个命令执行,而且根据复现结果,其命令执行却是在jms_celery容器中。
这个时候对此处的br-ab8ac2f1cea3进行tcpdump抓包监听,并重复整个复现过程,观察其中数据包传输流向,究竟是哪个数据包发送给了jms_celery并导致了其远程命令执行。
使用docker inspect确认各个关键容器的IP地址如下:
jms_web:192.168.250.11
jms_core:192.168.250.4
jms_celery:192.168.250.3
jms_redis:192.168.250.10
jms_mysql:192.168.250.5
我们先看jms_web和jms_core之间的通信,这里优先考虑由jms_web向jms_core传值,因为这里调用的是80端口的web服务器进行复现。所以使用ip.addr == 192.168.250.11 && ip.addr == 192.168.250.4进行流量过滤,可以发现这里jms_web将从创建Playbook开始的所有报文都转发给了jms_core,初步判断这里jms_web是反代理的jms_core中8080端口的/api/v1/*中的部分内容。
同时可以发现这里也传输了调用jms_celery时的结果返回
这个时候我们进入jms_core容器查看payload是否在本地落地,这里直接搜索Playbook的ID,可以看到这个文件在此处落地了。
root@jms_core:/opt/jumpserver# find ./ -name fcbdb397-c895-491e-8253-9e9e5f48f020
./data/ops/playbook/fcbdb397-c895-491e-8253-9e9e5f48f020
root@jms_core:/opt/jumpserver# cat ./data/ops/playbook/fcbdb397-c895-491e-8253-9e9e5f48f020/main.yml
[{
"name": "RCE playbook",
"hosts": "all",
"tasks": [
{
"name": "this runs in Celery container",
"shell": "id > /tmp/pwnd",
"\u0064elegate_to": "localhost"
} ],
"vars": {
"ansible_\u0063onnection": "local"
}
}]
这个时候继续分析抓到的pcap包,查看其网络通信内容,寻找这个payload去往celery的路,在分析celery的通信过程中,发现其并未和jms_core直接通信,而是和数据库mysql及redis进行通信。
先对mysql进行内容分析,发现其中存储了ops_playbook的id值,jobs的id值及crontab,及用于ansible的任务规划,没有发现序列化后的yml文件
然后是Redis,其中存储了连接记录,资源详情等内容,但是没有发现序列化后的yml文件
代码分析
从补丁开始分析,补丁中主要对apps/ops/ansible/runner.py进行了修补,并且将原有的PlaybookRunner替换成了SuperPlaybookRunner。其中SuperPlaybookRunner为PlaybookRunner的子类,并且其中增加了一个"LOCAL_CONNECTION_ENABLED": "1"的条件。
同时升级了ansible-core的版本
看一下ansible-core做了哪些改动,将开发者遗留的doc和test删除丢进winmerge进行比较,发现其主要修改了/lib/ansible/plugins/connection/local.py这个文件,增加了一个判断语句,如果没有设置LOCAL_CONNECTION_ENABLED则默认禁用本地链接
可以看到,在3.10.7中,修改了manager.py, job.py中的PlaybookRunner调用,可以发现在job中依旧调用PlaybookRunner,禁用本地连接;在manager中使用SuperPlaybookRunner,启用本地连接
这里可以简单看一下他的任务执行流程
- 获取一个job, 如果是playbook,检查一下危险词
- get_runner,向ansible下发命令self.current_job.playbook.entry
- 看到/apps/ops/ansible/runner.py 中的PlaybookRunner类,在这个类中利用ansible_runner.run运行了这个playbook
可以看到,假如在没有任何过滤的情况下,向该函数传入playbook_path, inventory_path, project_directory就可以进行命令执行,其中playbook_path也就是刚刚main.yml的值,我们这里回到刚刚的3.10.6中,将playbook修改成非Unicode编码的样子,也就是他编码前的样子,将其保存并执行时会发现他触发了check_danger_keywords。[{ "name": "RCE playbook", "hosts": "all", "tasks": [ { "name": "this runs in Celery container", "shell": "id > /tmp/pwnd", "delegate_to": "localhost" } ], "vars": { "ansible_connection": "local" } }]
跟过去看一下check_dangerous_keywords中的函数内容,可以看到他是以正则过滤的方式筛选dangerous_keywords,如果playbook中含有上面的keywords,则返回当前危险字符的位置和文件
到这边我还是想知道他是怎么传值的,所以从JobExecution一步步向上跟进代码,可以看到在JobExecutionSerializer中被调用,将反序列化后的数值填入这个JobExecution类
继续向上,就来到了JobViewSet,这是一个Django调用的前台页面,用于执行job
再从写入流程,也就是此处的playbook跟进
跟进到了API view,也就是上面所说jms_web反代理到jms_core的内容,可以看到其中的patch函数,如果获取到的HTTP方法是PATCH,也就是之前所使用修改main.yml的方法。这个函数在获取到pk和playbook_id并校验之后,将其存储到file_path中,也就是在jms_core中落地
后续跟进到api的\apps\terminal\automations\deploy_applet_host__init__.py,找到了文件解析点
在这里进行了celery命令执行
简单小结
其实到这里已经把漏洞成因分析的七七八八了,以unicode编码的形式绕过了正则过滤,将值传入runner中,使其在localhost运行;补丁中更新了ansible-core的版本,并使用其新特性,将原有的PlaybookRunner区分成两个部分,在manager.py中保留原有的SuperPlaybookRunner,使其允许在localhost执行命令,而在job.py中不允许在localhost执行命令。
CVE-2024-29202
至于CVE-2024-29202,修复补丁中启用了jinja2的SandboxedEnvironment作为NativeEnvironment完成了修复,其执行方式与绕过方式与29201类似,不做过多赘述了
疑点
这里有一个疑点,不知道他是怎么在jms_celery上执行的,yml是在jms_core上的,mysql和redis也未发现序列化后的内容?但是jms_web与jms_celery不通信,jms_core与jms_celery也不通信,jms_celery只和jms_redis与jms_mysql通信。而mysql中存储的是运行结果,Redis中是各种key和session,所以数据流并不完整,只能找到输入点、部分执行点和输出点,很想知道这里是怎么执行到jms_celery中去的,如果有相关解答还请留言,对JumpServer并不是很了解,期待有大手子可以解答。