來聊聊自己寫的玩具:PHPFuck

我有在 SITCON 2021 發表一場以 PHPFuck 為主題的短講,有興趣的可以看看
PHP 是世界上最棒的語言,但我做了一個比它更棒的!‌‌ | Slide

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

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

GitHub - splitline/PHPFuck: PHPFuck: ([+.^]) / Using only 7 different characters to write and execute php.
PHPFuck: ([+.^]) / Using only 7 different characters to write and execute php. - GitHub - splitline/PHPFuck: PHPFuck: ([+.^]) / Using only 7 different characters to write and execute php.
GitHub 原始碼

懶人包 / 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 實作

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

  1. 把輸入的 code 轉換成 PHPFuck-ed 的字串
    • 處理基本的 ASCII printable 字元
    • 解決未處理到的字元(如中文等等)
  2. 將 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

7 is too many · Issue #6 · splitline/PHPFuck
You can do it in 5 :)

同時 @lebr0nli 也去除了我 PHPFuck 裡使用到的 + 號,改良出了一個只需要六種字元的版本

GitHub - lebr0nli/PHPFun: Write and execute PHP with only 6 different characters: ([.^])
Write and execute PHP with only 6 different characters: ([.^]) - GitHub - lebr0nli/PHPFun: Write and execute PHP with only 6 different characters: ([.^])

最後的最後——PHP 是世界上最好的語言。

Show Comments