Good Old Days

Description

problem description

Solution

Before the solution I must say it was very cool to see the Windows XP screen and Winamp again, kudos!

Intro

Network Traffic Analysis

OK, lets start to solve this challenge.

After browsing around, most of the links/files in the GUI are missing/not working, but the command prompt is working :-)

The dir command will show us that there are two interesting files in the current directory:

Reading the flag isn't allowed of course (this is a challenge, not a Windows Command Line 101 course :-) )

C:\Documents and Settings\Pwner\Documents>type flag.txt
Error: access denied

Reading the good_old_days.exe file is allowed, but it looks like this is an ELF file (The first WSL maybe?).

C:\Documents and Settings\Pwner\Documents>type good_old_days.exe 
 ELF <binary gibberish>

CMD

From the the network traffic we can see that the type command triggers a post request to /cmd with some payload, and the response is the good_old_days.exe ELF file.

CMD

Find The Bug

We can see that this is an ELF file with symbols (Thanks!).

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4[32]; // [rsp+0h] [rbp-290h] BYREF
  char v5[600]; // [rsp+20h] [rbp-270h] BYREF
  unsigned __int64 v6; // [rsp+278h] [rbp-18h]

  v6 = __readfsqword(0x28u);
  httplib::Server::Server((httplib::Server *)v5);
  httplib::Server::set_mount_point((httplib::Server *)v5, "/", "./www");
  std::function<void ()(httplib::Request const&,httplib::Response &)>::function<main::{lambda(httplib::Request const&,httplib::Response &)#1},void,void>((std::_Function_base *)v4);
  httplib::Server::Get((__int64)v5, (__int64)"/hi", (__int64)v4);
  std::function<void ()(httplib::Request const&,httplib::Response &)>::~function(v4);
  std::function<void ()(httplib::Request const&,httplib::Response &)>::function<main::{lambda(httplib::Request const&,httplib::Response &)#2},void,void>(v4);
  httplib::Server::Post(v5, "/cmd", v4);
  std::function<void ()(httplib::Request const&,httplib::Response &)>::~function(v4);
  httplib::Server::listen((httplib::Server *)v5, "0.0.0.0", 8080, 0);
  httplib::Server::~Server((httplib::Server *)v5);
  return 0;
}

Analyzing the ELF, we can see that this is an HTTP server with two APIs:

Let's take a look at the execute_commands function:

  payload_data = (const payload_header *)std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::data(a2);
  payload_size = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(a2);
  if ( payload_size <= 0xF )
  {
    ...
  }
  if ( payload_data->version_number != 0x10000 )
  {
    ...
  }
  cmd_ptr = (const cmd_header *)payload_data->cmds;
  cmds_end = (char *)payload_data + payload_size;
  while ( cmd_ptr != (const cmd_header *)cmds_end )
  {
    if ( (unsigned __int64)(cmds_end - (const char *)cmd_ptr) <= 0xF )
    {
      ...
    }
    if ( cmd_ptr->cmd_size_qwords <= 1uLL )
    {
      ...
    }
    if ( cmd_ptr->cmd_size_qwords > 0x80uLL )
    {
      ...
    }
    if ( cmds_end - (const char *)cmd_ptr < (unsigned __int64)(8 * cmd_ptr->cmd_size_qwords) )
    {
      ...
    }
    cmd_type = cmd_ptr->cmd_type;
    if ( cmd_ptr->cmd_type == CMD_DIR )
    {
      if ( cmd_ptr->cmd_size_qwords != 2 )
      {
        ...
      }
    }
    else
    {
      if ( !cmd_type || (unsigned __int64)(cmd_type - 2) > 7 )
      {
        ...
      }
      if ( cmd_ptr->cmd_size_qwords == 2 )
      {
        ...
      }
    }
    cmd_ptr = (const cmd_header *)((char *)cmd_ptr + 8 * cmd_ptr->cmd_size_qwords);
  }

We can understand the payload structure from the binary:

enum CMDS{
    CMD_DIR          = 1;
    CMD_ECHO         = 2;
    CMD_TYPE         = 3;
    CMD_ERASE        = 4;
    CMD_MOVE         = 5;
    CMD_COPY         = 6;
    CMD_SET_SRC_NAME  = 7;
    CMD_PING_IPV4    = 8;
    CMD_PING_IPV6    = 9;
};

struct cmd_header{
    enum CMDS cmd_type;
    uint64_t  cmd_size_in_qwords; // including the header size
    uint8_t   cmd_data[];         // cmd_size_in_qwords * sizeof(uint64_t)
};

struct payload_header{
    uint64_t          version;
    uint64_t          session_cookie
    struct cmd_header cmds[];         //dynamic size
}

The function checks the following things:

  1. The payload size is at least 2 qwords.
  2. The version number is 0x10000.
  3. For each cmd:
  4. The cmd_ptr->cmd_size_qwords is at least 2 qwords.
  5. The cmd_ptr->cmd_size_qwords is not bigger than 0x80 qwords.
  6. cmd_ptr - cmds_end (the remaining size) is not less than cmd_ptr->cmd_size_qwords * 8.
  7. If cmd_ptr->cmd_type is CMD_DIR then cmd_ptr->cmd_size_qwords is equal 2.
  8. cmd_ptr->cmd_type is a valid value (between CMD_DIR(1) and CMD_PING_IPV6(9)) and cmd_ptr->cmd_size_qwords is bigger then 2.

Looks OK, let's continue:

__int64 __fastcall is_admin_token(unsigned __int64 a1)
{
  unsigned int v1; // ebx
  char *v3; // [rsp+18h] [rbp-48h]
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char> >_0 v4; // [rsp+20h] [rbp-40h] BYREF
  unsigned __int64 v5; // [rsp+48h] [rbp-18h]

  v5 = __readfsqword(0x28u);
  std::__cxx11::to_string((std::__cxx11 *)&v4, a1);
  v3 = getenv("ADMIN_TOKEN");
  if ( v3 )
    v1 = std::operator==<char>(&v4, v3);
  else
    v1 = std::operator==<char>(&v4, "1");
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v4);
  return v1;
}
  get_session_path[abi:cxx11](&v19, payload_data->session_cookie);
  if ( (unsigned __int8)is_admin_token(payload_data->session_cookie) )
    is_admin = 1;

The function checks if we are sending the admin token by checking against the environment variable ADMIN_TOKEN: If we send the admin token, is_admin will be equal to 1.

_data_start = (const cmd_header *)payload_data->cmds;
LABEL_28:
  while ( _data_start != cmds_end )
  {
    _cmd_size = 8 * _data_start->cmd_size_qwords - 16;
    _memcpy_fwd(_cmd_data, _data_start->cmd_data, _cmd_size);
    _cmd_data[_cmd_size] = 0;
    switch ( _data_start->cmd_type )
    {
      case CMD_DIR:
        cmd_dir((__int64)&v19, a1);
        ++_data_start;
        break;
      case CMD_ECHO:
        cmd_echo(_cmd_data, a1);
        _data_start = (const cmd_header *)((char *)_data_start + _cmd_size + 16);
        break;
      case CMD_TYPE:
        ::cmd_type(&v19, _cmd_data, a1, is_admin);
        _data_start = (const cmd_header *)((char *)_data_start + _cmd_size + 16);
        break;
      case CMD_ERASE:
        cmd_erase(&v19, _cmd_data, a1, is_admin);
        _data_start = (const cmd_header *)((char *)_data_start + _cmd_size + 16);
        break;
      case CMD_MOVE:
        cmd_move(&v19, _cmd_data, a1, is_admin, &v20);
        _data_start = (const cmd_header *)((char *)_data_start + _cmd_size + 16);
        break;
      case CMD_COPY:
        cmd_copy(&v19, _cmd_data, a1, is_admin, &v20);
        _data_start = (const cmd_header *)((char *)_data_start + _cmd_size + 16);
        break;
      case CMD_SET_SRC_NAME:
        cmd_set_src_name(&v19, _cmd_data, a1, is_admin, &v20);
        _data_start = (const cmd_header *)((char *)_data_start + _cmd_size + 16);
        break;
      case CMD_PING_IPV4:
        cmd_ping(_cmd_data, _cmd_size >> 2, a1);
        _data_start = (const cmd_header *)((char *)_data_start + 4 * (_cmd_size >> 2) + 16);
        break;
      case CMD_PING_IPV6:
        cmd_ping_ipv6(_cmd_data, _cmd_size >> 4, a1);
        _data_start += (_cmd_size >> 4) + 1;
        break;
      default:
        goto LABEL_28;
    }
  }

The function calculates the _cmd_size (in bytes, without the header) and copies the command to a temporary buffer - _cmd_data.

    _cmd_size = 8 * _data_start->cmd_size_qwords - 16;
    _memcpy_fwd(_cmd_data, _data_start->cmd_data, _cmd_size);
    _cmd_data[_cmd_size] = 0;

After executing the command and incrementing the _data_start pointer by the command size (that already passed validation) the code continues to the next command.

      case CMD_PING_IPV6:
        cmd_ping_ipv6(_cmd_data, _cmd_size >> 4, a1);
        _data_start += (_cmd_size >> 4) + 1;
        break;

Let's take a look at the Ping IPv6 function. The execute_command function will send this function the number of IPv6 addresses as an argument. An IPv6 address is 16 byte long, so _cmd_size >> 4 (i.e. _cmd_size/16) is the number of IPv6 address in the command data. After the function cmd_ping_ipv6 finishes, the _data_start pointer is incremented by _cmd_size >> 4 + sizeof(cmd_header) to the next command.

The problem is that _cmd_size can be equal to 8 bytes (1 qword of data or 3 qwords including the header). This value will pass the validation at the beginning of the function and cause the cmd_ping_ipv6 function to receive _cmd_size >> 4 (i.e. 0) as number of IPv6 address. That alone is Ok, since no full address is in the command payload. However, when the function will incrment the _data_start pointer, the incrementation will be only by sizeof(cmd_header) (i.e. _data_start += (8 >> 4) + 1), which will cause the _data_start pointer to point to the _data_start->cmd_data as the next command. This means that this location is still part of the last command that is not validate as a command at the beginning of the function.

Exploit

Let's look at the cmd_type function:

unsigned __int64 __fastcall cmd_type(__int64 a1, const char *cmd_data, std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char> >_0 *a3, char is_admin)
{
  std::runtime_error *v4; // rbx
  const std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char> >_0 *v5; // rax
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char> >_0 v9; // [rsp+20h] [rbp-80h] BYREF
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char> >_0 v10; // [rsp+40h] [rbp-60h] BYREF
  char v11[40]; // [rsp+60h] [rbp-40h] BYREF
  unsigned __int64 v12; // [rsp+88h] [rbp-18h]

  v12 = __readfsqword(0x28u);
  validate_filename(cmd_data);
  if ( is_admin != 1 && !strcmp(cmd_data, "flag.txt") )
  {
    v4 = (std::runtime_error *)_cxxabiv1::__cxa_allocate_exception(0x10uLL);
    std::runtime_error::runtime_error(v4, "access denied");
    _cxxabiv1::__cxa_throw(v4, &`typeinfo for'std::runtime_error, (void (*)(void *))std::runtime_error::~runtime_error);
  }
  std::operator+<char>(&v9, a1, "/");
  std::operator+<char>(&v10, &v9, cmd_data);
  readfile(v11, &v10);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v10);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v9);
  if ( (unsigned __int8)std::optional<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::has_value(v11) )
  {
    v5 = (const std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char> >_0 *)std::optional<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::value(v11);
    std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=(a3, v5);
  }
  else
  {
    std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=(
      a3,
      "The system cannot find the file specified.\n");
  }
  std::optional<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~optional(v11);
  return v12 - __readfsqword(0x28u);
}

OK, to read flag.txt we need to make sure that the is_admin byte will be equal to 1.

Let's look at the stack:

  char _cmd_data[1025]; // [rsp+A0h] [rbp-430h] BYREF
  unsigned __int8 is_admin; // [rsp+4A1h] [rbp-2Fh]

So we need to overflow _cmd_data and override is_admin using the bug we saw before.

We create the following full payload.

During the validation phase, the execute_command function will validate the following commands and the validation will pass:

validation

However, during the execution the execute_command function will execute the following commands:

Exec

  1. The first command is a dir command with _data_start->cmd_size_qwords equal to 0x02.
  2. The second command is a ping_ipv6 command with _data_start->cmd_size_qwords equal to 0x03.
  3. Due to the bug, the next command is also a ping_ipv6 with _data_start->cmd_size_qwords equal to 0x03.
  4. Due to the bug the next command is a ping_ipv4 with _data_start->cmd_size_qwords equal to 0x02.
  5. The next command is a dir command with _data_start->cmd_size_qwords equal to 0x83.
  6. This command will overflow the _cmd_data and will write 1 to is_admin.
  7. The type command moves the pointer to the next command regardless of the command size, the program assumes that the dir command size is sizeof(cmd_header).
  8. The next command is a type flag.txt command with _data_start->cmd_size_qwords equal to 0x03.
  9. The next command is a type ! command with _data_start->cmd_size_qwords equal to 0x50.
  10. The next command is a type ! command with _data_start->cmd_size_qwords equal to 0x41.

We send the payload and get the response:

 Volume in drive C has no label.
 Volume Serial Number is 9A7E-22F6.

 Directory of C:\Documents and Settings\Pwner\Documents

 <DIR>          .
 <DIR>          ..
             54 flag.txt
       10679376 good_old_days.exe
 Volume in drive C has no label.
 Volume Serial Number is 9A7E-22F6.

 Directory of C:\Documents and Settings\Pwner\Documents

 <DIR>          .
 <DIR>          ..
             54 flag.txt
       10679376 good_old_days.exe
BSidesTLV2021{FCKGW-RHQQ2-YXRKT-8TG6W-2B7Q8-h4ck3rman}The system cannot find the file specified.
The system cannot find the file specified.

And we have a flag: BSidesTLV2021{FCKGW-RHQQ2-YXRKT-8TG6W-2B7Q8-h4ck3rman}