Good Old Days
- Category: Pwn
- 350 points
- Solved by JCTF Team
Description

Solution
Before the solution I must say it was very cool to see the Windows XP screen and Winamp again, kudos!
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:
flag.txtgood_old_days.exe
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 deniedReading 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>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.
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:
- GET
/hithat returnsHello World! - POST
/cmdthat registers the functionexecute_commandsas a callback function to parse, validate and execute the POST request payload.
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:
- The payload size is at least 2 qwords.
- The version number is
0x10000. - For each cmd:
- The
cmd_ptr->cmd_size_qwordsis at least 2 qwords. - The
cmd_ptr->cmd_size_qwordsis not bigger than0x80qwords. cmd_ptr - cmds_end(the remaining size) is not less thancmd_ptr->cmd_size_qwords * 8.- If
cmd_ptr->cmd_typeisCMD_DIRthencmd_ptr->cmd_size_qwordsis equal 2. cmd_ptr->cmd_typeis a valid value (betweenCMD_DIR(1)andCMD_PING_IPV6(9)) andcmd_ptr->cmd_size_qwordsis bigger then2.
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:
However, during the execution the execute_command function will execute the following commands:
- The first command is a
dircommand with_data_start->cmd_size_qwordsequal to0x02. - The second command is a
ping_ipv6command with_data_start->cmd_size_qwordsequal to0x03. - Due to the bug, the next command is also a
ping_ipv6with_data_start->cmd_size_qwordsequal to0x03. - Due to the bug the next command is a
ping_ipv4with_data_start->cmd_size_qwordsequal to0x02. - The next command is a
dircommand with_data_start->cmd_size_qwordsequal to0x83. - This command will overflow the
_cmd_dataand will write1tois_admin. - The
typecommand moves the pointer to the next command regardless of the command size, the program assumes that thedircommand size issizeof(cmd_header). - The next command is a
type flag.txtcommand with_data_start->cmd_size_qwordsequal to0x03. - The next command is a
type !command with_data_start->cmd_size_qwordsequal to0x50. - The next command is a
type !command with_data_start->cmd_size_qwordsequal to0x41.
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}