# 極之番『漩渦』教學題文件
## 觀察
首先,先觀察目前現有的東西,Dockerfile。
```Dockerfile=
FROM php:7.4.33-apache
RUN chown -R www-data:www-data /var/www/html && \
chmod -R 555 /var/www/html
ARG FLAG
RUN echo $FLAG > /flag_`cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1`
USER www-data
```
透過這個 Dockerfile,我們可以得知了幾件事情。
- Line1 這題的 php 版本為 7.4.33
- 可以透過此 [網站](https://3v4l.org/#v7.4.33) 來測試不同版本 php 行為
- Line7 通常最終會讓我們能 RCE,也告訴了我們 flag 的位置在根目錄
## stage1
```php=
<?php
include('config.php');
echo '<h1>👻 Stage 1 / 4</h1>';
$A = $_GET['A'];
$B = $_GET['B'];
highlight_file(__FILE__);
echo '<hr>';
if (isset($A) && isset($B))
if ($A != $B)
if (strcmp($A, $B) == 0)
if (md5($A) === md5($B))
echo "<a href=$stage2>Go to stage2</a>";
else die('ERROR: MD5(A) != MD5(B)');
else die('ERROR: strcmp(A, B) != 0');
else die('ERROR: A == B');
else die('ERROR: A, B should be given');
```
可以發現在 Line13 會需要讓 A != B,且在 Line13 會需要 `strcmp($A, $B) == 0`,代表了若 A 和 B 皆為字串時需要兩者相同,到這裡正常行為下就不太可能發生。我們繼續往下看,他還需要讓兩者的 md5 hash 值相同。
這題用到的是 php 中 array parameter 的技巧,php 中很多函式在處理到傳入參數為 array 時,會報 warning,但仍會回傳 NULL。
另外一個會用到的技巧是弱型別比較的技巧,在 php 中使用 == 來比較兩值時,將進行弱型別轉換,下圖範例。


綜合以上兩種特性,傳入 A 和 B 不相同的兩個 array,就能導致通過 Line12 `A!=B` 的檢查,接下來 Line13 由於 `strcmp($A, $B)` 為 `NULL`,因此這個弱型別比較會是 true,最後 Line14 `md5($A)` 與 `md5($B)` 兩者皆為 NULL,因此 `md5($A) === md5($B)` 為 true
payload:
`stage1.php?A[]=1&B[]=3`
## stage2
```php=
<?php
include('config.php');
echo '<h1>👻 Stage 2 / 4</h1>';
$A = $_GET['A'];
$B = $_GET['B'];
highlight_file(__FILE__);
echo '<hr>';
if (isset($A) && isset($B))
if ($A !== $B){
$is_same = md5($A) == 0 and md5($B) === 0;
if ($is_same)
echo (md5($B) ? "QQ1" : md5($A) == 0 ? "<a href=$stage3?page=swirl.php>Go to stage3</a>" : "QQ2");
else die('ERROR: $is_same is false');
}
else die('ERROR: A, B should be given');
```
這題比起 stage1 多用到了一個知識點,在 php 中, = 的運算優先度是高於 and 的,若有以下 expression 時,結果會是 true。因此,在 Line13 我們只需要讓前面的為 true 即可。

在 Line15 我們會用到的知識點是,三元運算子的錯誤使用,我們先假設 `md5($B)` 是 false,`md5($A) == 0` 是 true
```
false ? "QQ1" : true ? "<a href=$stage3?page=swirl.php>Go to stage3</a>" : "QQ2"
```
會先處理前面的 `false ? "QQ1" : true`,因此運算式變這樣。
```
true ? "<a href=$stage3?page=swirl.php>Go to stage3</a>" : "QQ2"
```
因此,答案就會正常的被印出。
## stage3
```php=
<?php
include('config.php');
echo '<h1>👻 Stage 3 / 4</h1>';
$page = $_GET['page'];
highlight_file(__FILE__);
echo '<hr>';
if (isset($page)) {
$path = strtolower($_GET['page']);
// filter \ _ /
if (preg_match("/\\_|\//", $path)) {
echo "<p>bad hecker detect! </p>";
}else{
$path = str_replace("..\\", "../", $path);
$path = str_replace("..", ".", $path);
echo $path;
echo '<hr>';
echo file_get_contents("./page/".$path);
}
} else die('ERROR: page should be given');
```
這題的考點是 preg_match 的錯誤使用,雖然看起來傳入了 `/\\_|\//` 但其實 `\` 並不能正常被過濾,第一次的 `\` 被跳脫後,傳入 php 內的 regular expression parser 時只剩下一個 `\` 被拿來跳過 `_`,因此整個 preg_match 變成只有過濾 `_` `/`。
然後下方 Line16 又很好心的將 `..\` 轉為 `../`,我們就有機會 path traversal,要注意 Line17 將 `..` 轉為 `.` 因此我們需要將 payload 的 `..` 變為 `....` 才能將整體變為 `../`。
能 path traversal 後,觀察到一直有個檔案我們很好奇,就是 `config.php`,我們第一個先去看他,即可看到通往 stage4 的檔案名稱了(不要不小心輸入 fake flag lol)。
## stage4
```php=
<?php
echo '<h1>👻 Stage 4 / 4</h1>';
highlight_file(__FILE__);
echo '<hr>';
extract($_POST);
if (isset($👀))
include($👀);
else die('ERROR: 👀 should be given');
```
在 Line9 有個任意 include,但沒有$👀被賦值的地方。
這題用到了 [extract](https://www.php.net/manual/en/function.extract.php),而且參數用了 $_POST 因此讓我們能操作 POST 參數,來覆蓋任意變數可以拿來蓋 $👀。
最後就是要 include 什麼,關於這個 LFI2RCE 我們有很多 [招](https://blog.stevenyu.tw/2022/05/07/advanced-local-file-inclusion-2-rce-in-2022/),這邊就選擇 php filter chain,payload 可以參考 [這個](https://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT),即可成功 RCE。
## 結論
這題用到了都是 CTF 中 php 的 feature,也是很久之前就在 CTF 中就出現的利用方式,這次題目把他們出在一題讓大家回味一下(? 希望各位玩的開心 ouob