from:http://blog.gosecure.ca/2016/04/27/binary-webshell-through-opcache-in-php-7/
在 PHP 7.0 發布之初,就有不少 PHP 開發人員對其性能提升方面非常關注。在引入 OPcache 后,PHP的性能的確有了很大的提升,之后,很多開發人員都開始采用 OPcache 作為 PHP 應用的加速器。OPcache 帶來良好性能的同時也帶來了新的安全隱患,下面的內容是 GoSecure 博客發表的一篇針對 PHP 7.0 的 OPcache 執行 PHP 代碼的技術博文。
本文會介紹一種新的在 PHP7 中利用默認的 OPcache 引擎實施攻擊的方式。利用此攻擊向量,攻擊者可以繞過“Web 目錄禁止文件讀寫”的限制 ,也可以執行他自己的惡意代碼。
OPcache 是 PHP 7.0 中內建的緩存引擎。它通過編譯 PHP 腳本文件為字節碼,并將字節碼放到內存中。
OPcache 緩存文件格式請看這里。
同時,它在文件系統中也提供了緩存文件。
在 PHP.ini 中配置如下,你需要指定一個緩存目錄:
opcache.file_cache=/tmp/opcache
在指定的目錄中,OPcache 存儲了已編譯的 PHP 腳本文件,這些緩存文件被放置在和 Web 目錄一致的目錄結構中。如,編譯后的 /var/www/index.php 文件的緩存會被存儲在 /tmp/opcache/[system_id]/var/www/index.php.bin 中。
system_id 是當前 PHP 版本號,Zend 擴展版本號以及各個數據類型大小的 MD5 哈希值。在最新版的 Ubuntu(16.04)中,system_id 是通過當前 Zend 和 PHP 的版本號計算出來的,其值為 81d80d78c6ef96b89afaadc7ffc5d7ea。這個哈希值很有可能被用來確保多個安裝版本中二進制緩存文件的兼容性。當 OPcache 在第一次緩存文件時,上述目錄就會被創建。
在本文的后面,我們會看到每一個 OPcache 緩存文件的文件頭里面都存儲了 system_id。
有意思的是,運行 Web 服務的用戶對 OPcache 緩存目錄(如:/tmp/opcache/)里面的所有子目錄以及文件都具有寫權限。
#!shell
$ ls /tmp/opcache/
drwx------ 4 www-data www-data 4096 Apr 26 09:16 81d80d78c6ef96b89afaadc7ffc5d7ea
正如你所看到的,www-data 用戶對 OPcache 緩存目錄有寫權限,因此,我們可以通過使用一個已經編譯過的 webshell 的緩存文件替換 OPcache 緩存目錄中已有的緩存文件來達到執行惡意代碼的目的。
要利用 OPcache 執行代碼,我們需要先找到 OPcache 的緩存目錄(如:/tmp/opcache/[system_id])以及 Web 目錄(如:/var/www/)。
假設,目標站點已經存在一個執行 phpinfo() 函數的文件了。通過這個文件,我們可以獲得 OPcache 緩存目錄, Web 目錄,以及計算system_id 所需的幾個字段值。我寫了一個腳本,可以利用 phpinfo() 計算出 system_id。
另外還要注意,目標站點必須存在一個文件上傳漏洞。
假設 php.ini 配置 opcache 的選項如下:
opcache.validate_timestamp = 0 ; PHP 7 的默認值為 1
opcache.file_cache_only = 1 ; PHP 7 的默認值為 0
opcache.file_cache = /tmp/opcache
此時,我們可以利用上傳漏洞將文件上傳到 Web 目錄,但是發現 Web 目錄沒有讀寫權限。這個時候,就可以通過替換 /tmp/opcache/[system_id]/var/www/index.php.bin 為一個 webshell的二進制緩存文件運行 webshell。
在本地創建 webshell 文件 index.php ,代碼如下:
#!php
<?php
system($_GET['cmd']);
?>
在 PHP.ini 文件中設置 opcache.file_cache 為你所想要指定的緩存目錄
運行 PHP 服務器(php -S 127.0.0.1:8080) ,然后向 index.php 發送請求(wget 127.0.0.1:8080),觸發緩存引擎進行文件緩存。
打開你所設置的緩存目錄,index.php.bin 文件即為編譯后的 webshell 二進制緩存文件。
修改 index.php.bin 文件頭里的 system_id 為目標站點的system_id。在文件頭里的簽名部分的后面就是system_id的值。
通過上傳漏洞將修改后的 index.php.bin 上傳至 /tmp/opcache/[system_id]/var/www/index.php.bin ,覆蓋掉原來的 index.php.bin
重新訪問 index.php ,此時就運行了我們的 webshell
針對這種攻擊方式,在 php.ini 至少有兩種配置方式可以防御此類攻擊。
如果內存緩存方式的優先級高于文件緩存,那么重寫后的 OPcache 文件(webshell)是不會被執行的。但是,當 Web 服務器重啟后,就可以繞過此限制。因為,當服務器重啟之后,內存中的緩存為空,此時,OPcache 會使用文件緩存的數據填充內存緩存的數據,這樣,webshell 就可以被執行了。
但是這個方法比較雞肋,需要服務器重啟。那有沒有辦法不需要服務器重啟就能執行 webshell 呢?
后來,我發現在諸如 WordPress 等這類框架里面,有許多過時不用的文件依舊在發布的版本中能夠訪問。如: registration-functions.php
由于這些文件過時了,所以這些文件在 Web 服務器運行時是不會被加載的,這也就意味著這些文件沒有任何文件或內存的緩存內容。這種情況下,通過上傳 webshell 的二進制緩存文件為 registration-functions.php.bin ,之后請求訪問 /wp-includes/registration-functions.php ,此時 OPcache 就會加載我們所上傳的 registration-functions.php.bin 緩存文件。
如果服務器啟用了時間戳校驗,OPcache 會將被請求訪問的 php 源文件的時間戳與對應的緩存文件的時間戳進行對比校驗。如果兩個時間戳不匹配,緩存文件將被丟棄,并且重新生成一份新的緩存文件。要想繞過此限制,攻擊者必須知道目標源文件的時間戳。
如上面所說的,在 WordPress 這類框架里面,很多源文件的時間戳在解壓 zip 或 tar 包的時候都是不會變的。
注意觀察上圖,你會發現有些文件從2012年之后從沒有被修改過,如:registration-functions.php 和 registration.php 。因此,這些文件在 WordPress 的多個版本中都是一樣的。知道了時間戳,攻擊者就可以繞過 validate_timestamps 限制,成功覆蓋緩存文件,執行 webshell。二進制緩存文件的時間戳在 34字節偏移處。
OPcache 這種新的攻擊向量提供了一些繞過限制的攻擊方式。但是它并非一種通用的 PHP 漏洞。隨著 PHP 7.0 的普及率不斷提升,你將很有必要審計你的代碼,避免出現上傳漏洞。并且檢查可能出現的危險配置項。