直接访问web服务器提示need cookie,通过IDA反汇编目标WEB服务程序, F5得到伪C代码如下:
#!cpp haystack = strstr(byte_605240, "Cookie: "); if ( !haystack ) the_exit(2, (__int64)"Need cookie", (__int64)&unk_4020E0, a1); qword_606248 = (__int64)strstr(byte_605240, "HTTP/1.0"); if ( !qword_606248 ) { qword_606248 = (__int64)strstr(byte_605240, "HTTP/1.1"); if ( !qword_606248 ) the_exit(2, (__int64)"Not supported version", (__int64)&unk_4020E0, a1); } if ( strncasecmp(byte_605240, "GET", 3uLL) ) the_exit(2, (__int64)"Not supported method", (__int64)&unk_4020E0, a1); for ( i = 0LL; i < n; ++i ) { if ( *(_BYTE *)(i + 6312512) == 13 || *(_BYTE *)(i + 6312512) == 10 ) *(_BYTE *)(i + 6312512) = 0; } s = strstr(haystack, "auth="); if ( !s ) the_exit(2, (__int64)"Need authentication", (__int64)&unk_4020E0, a1); sa = s + 5; if ( strlen(sa) > 0x40 ) the_exit(2, (__int64)"Invaild authentication length", (__int64)&unk_4020E0, a1); sub_400F12(&var_60, sa); if ( memcmp(&var_60, s2, 0x20uLL) ) the_exit(2, (__int64)"Authentication failed", (__int64)&unk_4020E0, a1);
通过分析得出WEB服务会从客户端发来请求的数据包里查找是否有Cookie关键字,然后再查找Cookie里是否有auth关键字,然后读取auth的值 再经过sub_400F12函数处理,把处理后的结果与 s2的前0x20个字节进行比较,如果相等 则验证成功,否则失败。
跟进400F12函数发现如下代码片段:
#!cpp __int64 __fastcall sub_400F12(_BYTE *a1, _BYTE *a2) { while ( v4 > 4 ) { *v6 = 4 * byte_401FA0[(unsigned __int64)*v7] | ((unsigned __int8)byte_401FA0[(unsigned __int64)v7[1]] >> 4); v6[1] = 16 * byte_401FA0[(unsigned __int64)v7[1]] | ((unsigned __int8)byte_401FA0[(unsigned __int64)v7[2]] >> 2); v8 = v6 + 2; v6 += 3; *v8 = (byte_401FA0[(unsigned __int64)v7[2]] << 6) | byte_401FA0[(unsigned __int64)v7[3]]; v7 += 4; v4 -= 4; } if ( v4 > 1 ) { v9 = v6++; *v9 = 4 * byte_401FA0[(unsigned __int64)*v7] | ((unsigned __int8)byte_401FA0[(unsigned __int64)v7[1]] >> 4); } if ( v4 > 2 ) { v10 = v6++; *v10 = 16 * byte_401FA0[(unsigned __int64)v7[1]] | ((unsigned __int8)byte_401FA0[(unsigned __int64)v7[2]] >> 2); } if ( v4 > 3 ) { v11 = v6++; *v11 = (byte_401FA0[(unsigned __int64)v7[2]] << 6) | byte_401FA0[(unsigned __int64)v7[3]]; } *v6 = 0; return v13 - (-v4 & 3u); }
是很明显的BASE64解密算法代码
接下来看看s2的内容是什么。这段代码里发现
#!cpp sub_4014D3(int a1, unsigned int a2, void *a3) s2 = a3;
那么s2是由外部变量a3传递进来的,我们返回到主函数main里追踪一下a3变量的来历。在主函数里有以下代码片段:
#!cpp sub_401125("../server.cfg", &v12); sub_4014D3(v10, i, &v12);
大概是先通过函数401125得到v12,然后传递给4014D3函数的,这里v12对应上面分析的a3。再看看401125函数的功能:
#!cpp int __fastcall sub_401125(const char *a1, char *a2) { FILE *stream; // [sp+20h] [bp-10h]@1 char *v4; // [sp+28h] [bp-8h]@1 stream = fopen(a1, "r"); fgets(a2, 32, stream); v4 = strpbrk(a2, "/r/n"); if ( v4 ) *v4 = 0; return fclose(stream); }
很明显就是直接读取文件 ../server.cfg的前32个字节的内容到v12里。
到这里,我们就明白了,实际上auth就是文件../server.cfg 前32个字节base64加密后的结果。
可是我们怎么来得到这32个字节的内容呢?
通过仔细阅读伪C代码发现,打印错误信息函数the_exit有如下代码片段:
#!cpp else if ( a1 == 2 ) { sub_401195(v10, 0x194u, (__int64)&unk_4020E0); sprintf(s, "<HTML><BODY><H1>WebServer: %s %s</H1></BODY></HTML>", a2, v9); v6 = strlen(s); sub_4012E7(v10, s, v6); sprintf(s, "SORRY: %s-%s", a2, v9); }
跟进401195 发现:
#!cpp if ( qword_606248 ) v3 = (const char *)qword_606248; else v3 = "HTTP/1.0"; v4 = snprintf(&s, 0x100uLL, "%s %d %s/r/n", v3, a2, a3, a3, __PAIR__(a1, a2)); result = write(fd, &s, v4); v7 = *MK_FP(__FS__, 40LL) ^ v9; return result;
这里snprintf函数将格式化长度为0x100个字节的数据到s,其返回值v4是字符串实际的长度值,也就是说完全是可以大于0x100这个长度的。
接着Write函数将格式化后的字符串 取长度v4返回给客户端,../server.cfg的前32个字节是在主函数里读取的,这里如果我们控制了长度v4完全可能造成内存数据泄露,从而泄露../server.cfg的前32个字节的值,这样就有可能得到auth。
我们先尝试构造超长HTTP数据包试试 访问个超长名字的目录结果如下:
提示找不到cookie,那么webserver是通过函数strstr来查找cookie的 那么cookie放到任何位置都不影响。
我们把cookie放到第一行再尝试:
成功越界访问内存。那么哪部分才是auth呢?
这里我们本地测试一下:
在本地运行webserver 然后来一次内存泄露,看看auth在哪个位置
在本地构造server.cfg内容为32个A,如图:
运行服务,然后发送超长HTTP数据包如图:
在0x26偏移处看到了auth,那么对应三个白帽服务器的auth 应该也在0x26处。
将该处32字节进行base64编码得到
auth=BAjSrP9/AABm7NKs/38AAFgE0qz/fwAAEsjSrP9/AAA=
通过auth验证后,有如下代码:
#!cpp file = (char *)&unk_605244; v8 = strlen((const char *)&unk_605244); v15 = 0LL; while ( v8 > v15 && isspace(file[v15]) ) //去掉目录名前面的空格 { ++v15; ++file; } for ( j = 0LL; ; ++j ) { if ( strlen(file) <= j ) goto LABEL_38; if ( isspace(file[j]) ) break; } file[j] = 0; //FILE目录名里遇到空格便用/x0截断 LABEL_38: v9 = strlen(file); for ( k = 0; k < v9; ++k ) { if ( file[k] == 46 && file[k + 1] == 46 ) //如果目录里出现连续的.. 就提示错误 the_exit(2, (__int64)"Path travel not supported", (__int64)file, a1); }
通过分析,除非能控制file的长度让函数还没检查到..时就停止检查。
File长度v9 =strlen(file), 能控制srlen函数返回值的 就是字符0x00了,也就是file里要出现一个字符0x00,但是还要保证能正常读取文件,文件还必须存在。
这时我们看看后面读取文件的代码
#!cpp filea = file + 1; v10 = open(filea, 0, s2);
读取文件时读取的是filea,如果我们让file[0]=0x0那么就能绕过..检查了。怎么来制造一个/x0字符呢?
在函数前部分有如下代码片段:
#!cpp for ( i = 0LL; i < n; ++i ) { if ( *(_BYTE *)(i + 6312512) == 13 || *(_BYTE *)(i + 6312512) == 10 ) *(_BYTE *)(i + 6312512) = 0; }
它的作用是http数据包里把所有的换行符号0x0d,0x0a替换成0x00,如果我们把让file[0]=0x0d,那么在检查.. 时 file[0]就变成了,0x00,他后面跟../server.cfg后面再跟一个0x0d截断,这样就可以绕过目录跳转检查了,如图:
文件后缀检查有如下代码片段:
#!cpp v11 = 0LL; for ( l = 0LL; ; ++l ) { if ( !off_603160[2 * l] ) goto LABEL_49; v3 = (signed int)strlen(off_603160[2 * l]); v4 = off_603160[2 * l]; v5 = strlen(file); if ( !strncmp(&file[v5 - v3], v4, v3) ) break; } v11 = off_603160[2 * l + 1]; LABEL_49: if ( !v11 ) the_exit(2, (__int64)"Illegal file extension", (__int64)&unk_4020E0, a1); }
代码功能就是从数组603160里读后缀白名单,如果提交的文件后缀在白名单里 就通过,否则就不通过。我们看下603160的文件后缀白名单内容如下:
由于前面我们让file[0]=0x0d 准换后就是0x00所以这里 v5 = strlen(file); v5-v3=-v3
,file指针是个负数,因此file前面-v3开始的字符串要存在文件后缀白名单里才能通过验证。File指针指向的内存前后内容如下:
#!cpp get [0x0d]../server.cfg[0x0d]
file指向第一个0x0d,它前面get是不能控制的,否则http数据包就无法成功被识别,get检查片段:
#!cpp if ( strncasecmp(byte_605240, "GET", 3uLL) ) the_exit(2, (__int64)"Not supported method", (__int64)&unk_4020E0, a1);
能控制的就是t后面那个空格字符,所以我们就要考虑一下后缀有没有get et t开头的字符串了,如果有的话,就尝试构造这样的后缀名。
观察发现只有tz是以t开头的,满足条件。把get后面的空格替换成z 刚好满足条件。于是成功读取到server.cfg的内容,如图:
从server.cfg里成功得到了flag!