來聊聊自己寫的玩具:PHPFuck

PHPFuck 是我前陣子寫的一個簡易 PHP 混淆器,能將任意的 PHP 原始碼轉換成只運用到 ([+.^]) 七種字元的程式,其中大量運用到 PHP 的 weak type 特性。算是一個被 jsfuck 啟發的概念。

這東西意外地成為了我第一個破百星的 repo(本來還以為會是其他亂七八糟的東西先達到這個成就),看在這個情份(?)上,決定還是來認真聊聊它好了 XD

懶人包 / 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 有趣特性的基礎知識

1. Variable Function

RTFM: https://www.php.net/manual/en/functions.variable-functions.php

我們知道,在 PHP 裡是存在 variable function 這種東西的,也就是一個變數後面如果有括號(),那麼 php 便會試著執行與那個變數名相同的函數。舉個例子:

$magic = 'phpinfo';
$magic();

像這樣便會順利執行 phpinfo

至於如果沒有透過變數呢?

'phpinfo'();

這樣也是能正常執行的。

至此,我們知道只要有字串就能執行任意 function,因此確立了一個目標:要能創造出任意的字串

2. Weak Type

Weak type(弱型別),簡單來說是指在不同型別間進行運算時,程式會自動為你進行轉換型別進行運算(隱式轉換)。這邊舉一個簡單的例子

echo "123"+456; // output: 789

這邊可以看出面對字串和數字相加的狀況,PHP 會自動將字串轉為數字進行運算。

而透過這種特性,我們就能達成一些猥瑣的操作了!這也讓我們能順利的生成任意的字串。

運作原理

先整理一下大致上的思路,接著再來一一分析解決的方法:

  1. 把輸入的 code 轉換成 phpfucked 的字串
    1. 處理基本的 ASCII printable 字元
    2. 解決未處理到的字元(如中文等等
  2. 將 phpfucked 的字串拿去 eval 執行

數字組成

在 PHP 中,xor operator 是使用^表示,xor 是什麼我這邊就不解釋了,總之在 PHP 之中,它預設是可以對數字與字串進行運算。 那對於陣列他會怎麼處理呢?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'] 這些東西了!

PS. 在我的程式中,我只有使用 ‘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 的特性就解決了。

看起來大致上會像這樣:

(create_function(
    ...str_getcsv(',"<CODE_HERE>"')
))()

如此一來便能任意執行程式碼了!至此,PHPFuck 算是大功告成。

後記

身為一個 PHP 混淆器,CLI 和 Web 版卻分別是用 Python / JavaScript 寫的,想來真是有點荒謬。

PHP 是世界上最好的語言。

Discussion and feedback