我有在 SITCON 2021 發表一場以 PHPFuck 為主題的短講,有興趣的可以看看
PHP 是世界上最棒的語言,但我做了一個比它更棒的! | Slide
PHPFuck 是我前陣子寫的一個簡易 PHP 混淆器,能將任意的 PHP 原始碼轉換成只運用到 ([+.^])
七種字元的程式碼,其中大量運用到 PHP 的 weak type 特性,是一個受到 JSFuck 啟發的概念。
這東西意外地成為了我第一個破百星的專案(本來還以為會是其他亂七八糟的東西先達到這個成就),看在這個情份(?)上,還是來寫一篇文認真聊聊它好了
懶人包 / TL;DR
'ArrayArray' => [].[]
0 => []^[]
1 => []^[[]]
2 => ([]^[[]]) + ([]^[[]])
NULL => [][[]]
'p' => 'A'^'1' => 'ArrayArray'[0] ^ (1).NULL
'🐱' => IntlChar::chr(0x1f431) => ['IntlChar', 'chr'](0x1f431)
['cat', 'meow'] => 'str_getcsv'('cat,meow')
eval(CODE) => 'create_function'('', CODE)()
背景知識
首先,我們需要來複習一下關於 PHP 奇妙特性的一些知識。
Variable Function
RTFM: https://www.php.net/manual/en/functions.variable-functions.php
所謂的「variable function」,指的是能將任意的變數(variable)當成函式呼叫,舉例而言,如下的程式碼便能順利執行 phpinfo()
。
$magic = 'phpinfo';
$magic();
但我們一定需要變數嗎?其實不然,直接對一個字串進行呼叫也是可行的:
'phpinfo'();
看到這裡,我們已經知道了只要有字串就能執行任意 function,因此確立了一個目標:要能創造出任意的字串!
Weak Type
Weak type(弱型別),簡單來說是指在不同型別間進行運算時,程式會自動為你進行轉換型別進行運算(隱式轉換)。這邊舉一個簡單的例子
echo "123" + 456; // output: 789
這邊可以看出面對字串和數字相加的狀況,PHP 會自動將字串轉為數字進行運算。
而透過這種特性就能達成一些猥瑣的操作,讓我們能順利的生成任意的字串了!
PHPFuck 實作
先整理一下大致上的思路,接著再來一一分析解決的方法:
- 把輸入的 code 轉換成 PHPFuck-ed 的字串
- 處理基本的 ASCII printable 字元
- 解決未處理到的字元(如中文等等)
- 將 PHPFuck-ed 的字串拿去 eval 執行
生成數字
在 PHP 中,xor operator 是使用 ^
表示,xor 是什麼我這邊應該就不用解釋了,總之在 PHP 之中,它是可以對數字與字串進行運算的。
那 xor 會怎麼處理陣列呢?PHP 中,空的陣列轉為數字會是 0,而任意非空陣列轉為數字都會是 1:
php > var_dump((int)[]);
int(0)
php > var_dump((int)["meow"]);
int(1)
php > var_dump((int)[9,4,8,7]);
int(1)
php > var_dump((int)[NULL]);
int(1)
因此我們可以透過這點,搭配 PHP 中弱型別自動轉型的特性進行操作。PHP 在陣列間進行 xor 運算時,空與非空 xor 會回傳 1,而空與空、非空與非空 xor 則會回傳 0,藉此我們可以構造出 0 與 1 了。
var_dump([]^[]); // int(0)
var_dump([]^[[]]); // int(1)
既然我們已經獲得 1 了,我們只要累加上去(1+1+1...),自然就可以生成任意數字了,目標達成!
生成基本字元
究竟在 PHP 中,可以憑空生出哪些字元呢?這邊列舉了一些我找到的東西。
php > var_dump((string)[]);
string(10) "Array"
php > var_dump((string)(0/0));
string(3) "NAN"
php > var_dump((string)(1/0));
string(3) "INF"
var_dump((string)(10000000000000000000));
string(7) "1.0E+19"
根據上面的測試,我們可以獲得 ['A', 'a', 'E', 'F', 'I', 'N', 'r', 'y', '+']
這些字元;另外,我們透過前一步產生數字的成果,直接把那些數字轉型成字元,就能獲得 ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
這些東西了!
P.S. 在我的程式中,由於字元限制所以只有使用 'Array' 與數字而已。
等等,問題來了,我並沒有 (string)
可以使用,那要怎麼辦呢?其實,在 PHP 中,要強制把任意數值轉型成字串除了使用 (string)$variable
以外,還有一個更暴力的方法:只要用 .
operator(用於串接字串)對它操作就好了,由於可用字元的限制,所以我採用了這種方法。
生成任意 ASCII 字元
這邊又牽扯到 PHP 的一個有趣特性了:我們可以使用 xor operator 對字串間進行運算。
php > echo "123" ^ "KEY";
zwj
透過這種特性,我們可以僅透過 Aray0123456789
這 14 個字元,暴力排列組合產生出所有 ASCII 字元!
詳細列表可以看程式原始碼,這邊就不舉例了
生成所有字元
對啦,能產生了所有 ASCII 字元是聽起來還不錯,但世界上不只有 ASCII,像是 Unicode 有 1,114,112 個字元欸,你要怎麼處理啊?
沒關係,我們不一定要直接暴力湊出那個字元嘛,PHP 有一個內建的類別 IntlChar ,就是用來處理 unicode 的;以我們需要的例子來說,透過 IntlChar::chr(0x1f431)
我們就能輕鬆產出 🐱 了。不過,前面 IntlChar::chr
這部分要怎麼處理呢?其實它可以被轉換成 ['IntlChar', 'chr']
([類別名稱, 方法](...)
)的形式來表達,那我們應該就順利完成這部分了吧?
——並沒有。
這邊又來了一個新的挑戰:這個陣列有兩個元素,直覺上顯然需要用逗號將它們隔開,但是,逗號並不在我們的可用字元 ([+.^])
中啊!有沒有不需要逗號也能產生陣列的方法呢?我翻了許久的文件,終於找到了 str_getcsv
這個函數,完全符合我們目前的需求!簡單來說,像這樣呼叫 str_getcsv('cat,meow')
,它就會回傳 ['cat', 'meow']
這樣的陣列。
所以在這邊,便能用這種方法拿到我們想要的字元了:
("str_getcsv"("IntlChar,chr"))(0x1f431);
執行!
好的,我們前面已經順利湊出任意字串、數字了,現在剩下的問題就是該怎麼執行呢?
說到執行程式碼,如果有稍微接觸過 PHP 的人第一個想法應該就是 eval
吧?嗯,不錯的想法,只可惜在我們的場景下是行不通的,因為它不能被拿來當作 variable function 用。直接來看看官方文件,它很直接的說了:
Note: Because this is a language construct and not a function, it cannot be called using variable functions.
好吧,那還有什麼方法呢?試試 assert
如何,它也是能用來執行任意程式碼的吧?Well yes, but actually no. 告訴大家一個令人難過的消息:在 PHP 7.1 以後的版本,assert 也不支援被拿來當 variable function 用了。
在最後,我找到了 create_function
,這是用來產生一個匿名函數(anonymous function)的函數,用法是這樣的 create_function( string $args , string $code )
。看來這邊又遇到逗號了是吧,不過別擔心,這邊使用 unpacking 的特性就解決了,簡單來說,PHP 能透過 ...
將陣列展開成一個函數的參數。
看起來大致上會像這樣:
(create_function(
...str_getcsv(',"<CODE_HERE>"')
))()
如此一來便能任意執行程式碼了!至此,PHPFuck 算是大功告成。
後記
身為一個 PHP 混淆器,CLI 和 Web 版卻分別是用 Python / JavaScript 寫的,想來真是有點荒謬。
在完成之後我的 PHPFuck 就沒怎麼在更新了,不過後續衍伸出了不少其他更精進的版本。
像是 @arxenix 找到了只需要五種字元就可以達成的方法,並把它出在了 UIUCTF 2021
同時 @lebr0nli 也去除了我 PHPFuck 裡使用到的 +
號,改良出了一個只需要六種字元的版本
最後的最後——PHP 是世界上最好的語言。