玩转Hacker101 CTF(五)

hi,大家好,我又来放writeup啦!经过一个周末的头脑风暴,我终于拿到了第十四题的flag,所以接着第一篇第二篇第三篇还有第四篇的进度,这次和大家一起学习Hacker101 CTF的第十二、十三、十四题:

废话不多说,上题!

 

第十二题TempImage

这道题难度适中,考察文件上传漏洞,打开主页,大大的上传点:

点开:

文件上传漏洞的姿势很多,防护措施也五花八门,我整理了一下,画成了下面的脑图:

可能有遗漏的,欢迎在评论区补充。

检测文件上传点有无问题,我的习惯是直接做个图片马,抓上传包,因为这样最节省时间,先用copy做个图片马:

注意php.php中的内容先不要急着放一句话,先放个phpinfo()试试,成功了再放webshell,把上传包抓下来,送到Repeater模块:

放包:

返回了302跳转:

Follow redirection,跟踪跳转:

返回状态200,应该已经传上去了,在浏览器上访问一下跳转后的地址:

图片正常,为了判断后台有无修改图片内容(例如删除脚本标签头、图片重渲染等),需要将这场图片下载下来,与原始图片作对比:

简单的对比使用windows自带的comp命令就好:

可以看到文件内容没有修改。接下来探测文件后缀有无限制,重新上传shell.png抓包,或者直接在刚才抓下的包中修改文件名为shell.php,放包:

可见文件虽然上传上去了,但后缀名却还是png,这是为什么呢?仔细看了一下上传的包,发现最下面有猫腻:

看,这里又出现了一个filename,如果我们改变它,再上传会怎么样呢,shell.png改为shell.php,放包:

看,成功的上传了php后缀的文件,是不是大功告成了呢?在浏览器里访问一下上传上去的php文件,本以为会出现phpinfo的页面,然而并没有:

明明访问的是php文件,返回的却是图片,说明我们传上去的php文件没有解析,出现这种状况的原因一般是在files这个目录下有个.htaccess文件,其中配置了该目录下的所有文件都不可作为脚本解析,实现这个目标的.htaccess配置有多种,下面是其中一种:

php_flag engine off

总之,就是files目录下的文件不可以当作php脚本执行,那么就要思考其他路子,试想,虽然files目录下的文件不可以当作php脚本执行,但是其他目录比方说网站根目录总可以的吧,那么有没有办法把文件传到网站根目录下呢?我们现在可以控制filename了,如果filename中带上../路径会发生什么呢,修改上传包中filename参数的值为:../shell.php:

看,也许是提示我们思路正确,第一个flag已经出来了。仔细观察一下报错:

<br />
<b>Warning</b>:  move_uploaded_file(files/6c992b5b4654d3da3eb9e414afbc38e2_../shell.php): failed to open stream: No such file or directory in <b>/app/doUpload.php</b> on line <b>7</b><br />
<br />
<b>Warning</b>:  move_uploaded_file(): Unable to move '/tmp/phpfeEYvz' to 'files/6c992b5b4654d3da3eb9e414afbc38e2_../shell.php' in <b>/app/doUpload.php</b> on line <b>7</b><br />
ERROR: Upload failed<br>^FLAG^ebe27115de38566ea08f91ef913b3416edcfb3301ec9e958695bac47277aeba7$FLAG$

注意其中的路径files/6c992b5b4654d3da3eb9e414afbc38e2_../shell.php,显然,如果要把shell.php上传到与files目录的上级目录,我们应该把filename的值改为:

/../../shell.php,再次发包:

成功!浏览器访问一下:

OK,成功执行,接下来只需要把图片马中的payload改为一句话木马,重复上述过程,用菜刀连接即可:

在index.php找到了第二个flag:

顺带看了一下files目录,里面果然有.htaccess,内容和我想得差不多:

 

第十三题H1 Thermostat

很easy的android抓包分析及逆向,也就是签到题的难度,简单过一下,

打开主页,提示apk正在生成:

稍后刷新,出现apk下载链接:

拖到模拟器安装打开,我用的是mumu模拟器,注意修改模拟器的wifi代理地址为10.0.2.2

配置fiddler抓包

第一个flag就在请求头中,接下来反编译apk,我用的工具是gda,翻了翻源码,找到第二个flag,发现第一个flag也在里面,刚刚完全不需要抓包的,真是简单到家了啊!

 

第十四题Model E1337 v2 – Hardened Rolling Code Lock

这道题是Expert难度,是第十一题的加强版本,我花了一个周末死了无数脑细胞才解决掉它,来一起看一看,打开主页:

与十一题一模一样,依然是个猜数字游戏,输入code,返回期望值,如果输入的code与期望值相同,则拿到flag:

得先想办法找到后台源码,我用的是爆破的办法,字典用的是SecLists中的

SecLists-masterDiscoveryWeb-Contentraft-medium-words.txt,最好放在vps上用wfuzz爆破,

wfuzz -w path/raft-medium-words.txt --hc 404 http://xx.xx.xx.xx/xx/FUZZ

本地用burpsuite爆破很慢,再给大家安利一下:vps+wfuzz+seclists的组合真香。

很快出了结果:

源码所在的页面是rng,其实如果仔细回想一下第十一题,猜也能猜到,因为第十一题中的源码逻辑文件就是rng.py。

来读源码:

import random

def setup(seed):
    global state
    state = 0
    for i in range(16):
        cur = seed & 3
        seed >>= 2
        state = (state << 4) | ((state & 3) ^ cur)
        state |= cur << 2

def next(bits):
    global state

    ret = 0
    for i in range(bits):
        ret <<= 1
        ret |= state & 1
        for k in range(3):
            state = (state << 1) ^ (state >> 61)
            state &= 0xFFFFFFFFFFFFFFFF
            state ^= 0xFFFFFFFFFFFFFFFF

            for j in range(0, 64, 4):
                cur = (state >> j) & 0xF
                cur = (cur >> 3) | ((cur >> 2) & 2) | ((cur << 3) & 8) | ((cur << 2) & 4)
                state ^= cur << j

    return ret

setup((random.randrange(0x10000) << 48) | (random.randrange(0x10000) << 32) | (random.randrange(0x10000) << 16) | random.randrange(0x10000))

这道题的代码与第十一题有三处不同:

一是setup函数的调用,十一题是

setup((random.randrange(0x10000)<<16) | random.randrange(0x10000))

也就是seed的范围在2的32次方内,这对个人计算机来说是一个可以接受的值,所以可以使用爆破的办法,而这里

setup((random.randrange(0x10000) << 48) | (random.randrange(0x10000) << 32) | (random.randrange(0x10000) << 16) | random.randrange(0x10000))

显然,seed的范围在2的64方内,这对个人计算机来说是个天文数字了,所以这里直接爆破行不通,需要分析代码寻找窍门。

仔细观察setup函数中的下列代码片段:

......
for i in range(16):
        cur = seed & 3
        seed >>= 2
        ......

现在seed已经是64bit的一个数了,那么cur = seed & 3代表取出seed的最低2bit位给cur,然后seed >>= 2代表着seed右移2bit位,这个过程在循环
for i in range(16)中,也就是说每次循环seed的最低两位赋予了cur,用简单的话描述这个循环对seed的影响就是:seed的最低2*16即32个bit位参与了运算,而且在setup函数的其他地方没有用到seed,所以说只有seed的低32位对setup函数有影响,再换句话说就是:setup(0xFFFFFFFF11111111)和setup(0x11111111)对整个脚本生成期望值的影响是一样的!

为了确认这个结论,我又写了个脚本来跟踪seed的每一位,看由setup函数生成的state的每一位状态:

seed = ['b0','b1','b2','b3','b4','b5','b6','b7','b8','b9','b10','b11','b12','b13','b14','b15','b16','b17','b18','b19','b20','b21','b22','b23','b24','b25','b26','b27','b28','b29','b30','b31','b32','b33','b34','b35','b36','b37','b38','b39','b40','b41','b42','b43','b44','b45','b46','b47','b48','b49','b50','b51','b52','b53','b54','b55','b56','b57','b58','b59','b60','b61','b62','b63']

state = []

def myxor(a1,a2):
    global state 

    arrlen = max(len(a1),len(a2))
    if len(a1) > len(a2):
        temp = ['']*(len(a1)-len(a2))
        temp.extend(a2)
        a2 = temp
    else:
        temp = ['']*(len(a2)-len(a1))
        temp.extend(a1)
        a1 = temp

    result = []
    for i in range(arrlen):
        if a1[i] == '':
            result.append(a2[i])
        elif a2[i] == '':
            result.append(a1[i])
        else:
            result.append('(' + a1[i] + ' XOR ' + a2[i] + ')')
    return result

def myor(a1,a2):
    global state

    arrlen = max(len(a1),len(a2))
    if len(a1) > len(a2):
        temp = ['']*(len(a1)-len(a2))
        temp.extend(a2)
        a2 = temp
    else:
        temp = ['']*(len(a2)-len(a1))
        temp.extend(a1)
        a1 = temp

    result = []
    for i in range(arrlen):
        if a1[i] == '':
            result.append(a2[i])
        elif a2[i] == '':
            result.append(a1[i])
        else:
            result.append('(' + a1[i] + ' OR ' + a2[i] + ')')
    return result

for x in range(16):
    cur = seed[-2:]
    seed = seed[:-2]
    temp1 = state[:]
    temp1.extend(['','','',''])
    temp2 = state[-2:]
    temp2 = myxor(temp2,cur)
    state = myor(temp1,temp2)
    cur.extend(['',''])
    state = myor(state,cur)

print("init_state:",state)

最后的输出:

init_state: ['b62', 'b63', 'b62', 'b63', 'b60', 'b61', '(b62 XOR b60)', '(b63 XOR b61)', 'b58', 'b59', '((b62 XOR b60) XOR b58)', '((b63 XOR b61) XOR b59)', 'b56', 'b57', '(((b62 XOR b60) XOR b58) XOR b56)', '(((b63 XOR b61) XOR b59) XOR b57)', 'b54', 'b55', '((((b62 XOR b60) XOR b58) XOR b56) XOR b54)', '((((b63 XOR b61) XOR b59) XOR b57) XOR b55)', 'b52', 'b53', '(((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52)', '(((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR
b53)', 'b50', 'b51', '((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50)', '((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51)', 'b48', 'b49', '(((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48)', '(((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49)', 'b46', 'b47', '((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46)', '((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47)', 'b44', 'b45', '(((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44)', '(((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45)', 'b42', 'b43', '((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42)', '((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43)', 'b40', 'b41', '(((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40)', '(((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41)', 'b38', 'b39', '((((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40) XOR b38)', '((((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41) XOR b39)', 'b36', 'b37', '(((((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40) XOR b38) XOR b36)', '(((((((((((((b63 XOR b61) XOR b59) XOR b57)
XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41) XOR b39) XOR b37)', 'b34', 'b35', '((((((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40) XOR b38) XOR
b36) XOR b34)', '((((((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41) XOR b39) XOR b37) XOR b35)', 'b32', 'b33', '(((((((((((((((b62 XOR b60) XOR b58) XOR b56) XOR b54) XOR
b52) XOR b50) XOR b48) XOR b46) XOR b44) XOR b42) XOR b40) XOR b38) XOR b36) XOR b34) XOR b32)', '(((((((((((((((b63 XOR b61) XOR b59) XOR b57) XOR b55) XOR b53) XOR b51) XOR b49) XOR b47) XOR b45) XOR b43) XOR b41) XOR b39) XOR b37) XOR b35) XOR b33)']

可以看到,由setup函数改变的state只受seed中b32~b63这32个bit位的影响,seed的高32bit位相当于被丢弃未参与运算。

所以我们依旧可以爆破seed来解这道题,因为我们只需要爆破其低32bit位,最多也就是2的32次方中可能性。

但简单的爆破还是不能满足我的需求,因为在第十一题中,我所使用的爆破代码跑了2个小时才得到结果,而在这题中,还有两处不同,分别是第十九行加了个循环以及bits变为了64而不是26:

这样一方面进一步混淆了state,另一方面也会导致我爆破的计算量x3x64/26,如果简单套用上一题的爆破代码,大概要十四五个小时,这是让人无法接受的,所以我决定再分析一下代码,看能否找到弱点优化我的爆破代码。

注意第15~18行代码:

ret = 0
for i in range(bits):
    ret <<= 1
    ret |= state & 1

每次调用next(64)时ret的初始值为0,bits为64,那么循环中的代码就可以表述为:

ret左移1bit位,state的最低位放在ret的最低位上,如此循环64次。并且下文中ret的值没有再被修改。所以如果把setup()调用后state的状态记为S0,其最低位bit位记为S0[63],然后看第16~27行代码

    for i in range(bits):
        ret <<= 1
        ret |= state & 1
        for k in range(3):
            state = (state << 1) ^ (state >> 61)
            state &= 0xFFFFFFFFFFFFFFFF
            state ^= 0xFFFFFFFFFFFFFFFF

            for j in range(0, 64, 4):
                cur = (state >> j) & 0xF
                cur = (cur >> 3) | ((cur >> 2) & 2) | ((cur << 3) & 8) | ((cur << 2) & 4)
                state ^= cur << j

将i=0这次循环结束时state的状态记为S1,其最低bit位记为S1[63],将i=2这次循环结束时state的状态记为S2,其最低bit位记为S2[63],以此类推,那么第一个ret就可以表示为:

S0[63],S1[63],...S63[63]

同样的,第二个ret可以表示为:

S64[63],S65[63],...S127[63]

而ret我们是知道的,那么在爆破过程中,我们验证一个seed能否产生我们想要的ret值时,就不需要将ret完全计算出来再比较,我们只需要计算中间过程的state,将它相应的位与ret相应的位进行比较可以了,这样可以大大减少我们的计算量,依照这个思路完成的代码如下:

#include <stdio.h>

unsigned long long state = 0;
unsigned long long ret0 = 8097447744684720271; //todo 
unsigned long long ret1 = 9416998937824950119; //todo

void setup(unsigned long long seed) {
    state = 0;
    unsigned long long cur = 0;
    for(unsigned i = 0; i < 16; i++) {
        cur = seed & 3;
        seed >>= 2;
        state = (state << 4) | ((state & 3ll) ^ cur);
        state |= cur << 2;
    }
}

void modifyState() {
    for (unsigned m = 0; m < 3; m++) {
        state = (state << 1) ^ (state >> 61);
        state &= 0xFFFFFFFFFFFFFFFF;
        state ^= 0xFFFFFFFFFFFFFFFF;

        for (unsigned j = 0; j < 64; j += 4) {
            unsigned long long cur = (state >> j) & 0xF;
            cur = (cur >> 3) | ((cur >> 2) & 2) | ((cur << 3) & 8) | ((cur << 2) & 4);
            state ^= cur << j;
        }
    }
}

unsigned long long next(unsigned bits) {
    unsigned long long ret = 0;
    for (unsigned i = 0; i < bits; i++) {
        ret <<= 1;
        ret |= state & 1;
        for (unsigned m = 0; m < 3; m++) {
            state = (state << 1) ^ (state >> 61);
            state &= 0xFFFFFFFFFFFFFFFF;
            state ^= 0xFFFFFFFFFFFFFFFF;

            for (unsigned j = 0; j < 64; j += 4) {
                unsigned long long cur = (state >> j) & 0xF;
                cur = (cur >> 3) | ((cur >> 2) & 2) | ((cur << 3) & 8) | ((cur << 2) & 4);
                state ^= cur << j;
            }
        }
    }
    return ret;
}

unsigned int check(unsigned long long ret,unsigned int j){
    if(((ret >> j) & 1) == (state & 1)){
        modifyState();
        if(j > 0){
            check(ret,j-1);
        }else{
            return 1;
        }
    }else{
        return 2;
    }
}

int main(int argc, char* argv[]) {
    unsigned long long seed = 1;
    unsigned printtimes = 0;
    while (seed) {
        if(seed / 0x1000000 == printtimes){
            printf("now seed is :%I64xn",seed);
            printtimes++;
        }
        setup(seed);
        if(check(ret0,63) == 1){
            printf("ret0 matched,now state is:%I64x and seed is :%I64xn",state,seed);
            if(check(ret1,63) == 1){
                printf("ret1 matched,now state is:%I64x and seed is :%I64xn",state,seed);
                printf("And next ret is :%I64x",next(64));
                break;
            }
        }
        seed++;
    }
    while (getchar() != 'e') { //防止跑出结果一声不响的退出

    }
}

注意在VS中编译这段代码时,要选择Debug x64模式,release模式可能会跑不出来,我猜可能是编译器代码优化出了问题,如果你知道怎么解决,欢迎告诉我。
用上面编译出来的exe跑,十几分钟就出了结果,我的CPU还是很一般的货:

看,seed的值在范围0~0xFFFFFFFF中算是靠中间了,如果你还想更快,可以使用多线程,估计五、六分钟就能出结果,不过要注意给state加上访问锁。
跑出来的值是16进制的,转为10进制提交,就拿到了flag:

(完)