Skip to content

Instantly share code, notes, and snippets.

@ky28059
Last active September 23, 2024 00:53
Show Gist options
  • Save ky28059/f18c4f60f000b3f94c5aef427ce8b9aa to your computer and use it in GitHub Desktop.
Save ky28059/f18c4f60f000b3f94c5aef427ce8b9aa to your computer and use it in GitHub Desktop.

PatriotCTF 2024 — Really Only Echo

Hey, I have made a terminal that only uses echo, can you find the flag?

nc chal.competitivecyber.club 3333

We're given a Python server that looks like this:

#!/usr/bin/python3

import os,pwd,re
import socketserver, signal
import subprocess

listen = 3333

blacklist = os.popen("ls /bin").read().split("\n")
blacklist.remove("echo")
#print(blacklist)

def filter_check(command):
    user_input = command
    parsed = command.split()
    #Must begin with echo
    if not "echo" in parsed:
        return False
    else:
        if ">" in parsed:
            #print("HEY! No moving things around.")
            req.sendall(b"HEY! No moving things around.\n\n")
            return False
        else:
            parsed = command.replace("$", " ").replace("(", " ").replace(")", " ").replace("|"," ").replace("&", " ").replace(";"," ").replace("<"," ").replace(">"," ").replace("`"," ").split()
            #print(parsed)
            for i in range(len(parsed)):
                if parsed[i] in blacklist:
                    return False
            return True

def backend(req):
    req.sendall(b'This is shell made to use only the echo command.\n')
    while True:
        #print("\nThis is shell made to use only the echo command.")
        req.sendall(b'Please input command: ')
        user_input = req.recv(4096).strip(b'\n').decode()
        print(user_input)
        #Check input
        if user_input:
            if filter_check(user_input):
                output = os.popen(user_input).read()
                req.sendall((output + '\n').encode())
            else:
                #print("Those commands don't work.")
                req.sendall(b"HEY! I said only echo works.\n\n")
        else:
            #print("Why no command?")
            req.sendall(b"Where\'s the command.\n\n")

class incoming(socketserver.BaseRequestHandler):
    def handle(self):
        signal.alarm(1500)
        req = self.request
        backend(req)


class ReusableTCPServer(socketserver.ForkingMixIn, socketserver.TCPServer):
    pass


def main():
    uid = pwd.getpwnam('ctf')[2]
    os.setuid(uid)
    socketserver.TCPServer.allow_reuse_address = True
    server = ReusableTCPServer(("0.0.0.0", listen), incoming)
    server.serve_forever()

if __name__ == '__main__':
    main()

We're given a shell, but all binaries in /bin except echo are banned (and indeed, we're forced to have at least one echo in our input).

We can run an equivalent of ls with echo * to see our target, flag.txt in the same directory:

image

At first glance, we can try using echo $(<flag.txt) to concatenate and print the flag file with echo, but unfortunately $(<) is a bash-exclusive construct which won't work on Python's default shell sh.

Still, a clue lies in the way the server checks our input for banned commands:

            parsed = command.replace("$", " ").replace("(", " ").replace(")", " ").replace("|"," ").replace("&", " ").replace(";"," ").replace("<"," ").replace(">"," ").replace("`"," ").split()
            #print(parsed)
            for i in range(len(parsed)):
                if parsed[i] in blacklist:
                    return False

While they seemingly account for trying to escape the plaintext blacklist check using characters like (, &, and ;, they don't account for }.

Thus, we can run any command by using shell variable expansion and $IFS a la

echo *;${IFS}command

(remembering to have at least one echo in the command to satisfy the filter).

Finally, we can run

echo *;${IFS}cat flag.txt

to cat the flag.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment