Spot The Bug challenge 2015 write-up

Introduction

We recommend keeping the code at hand as you look through this write-up, to get a better feeling for the vulnerable code patterns. Also, take the time to re-read the code if you forgot its workings. If you didn’t compete in the challenge, try to see how many bugs you can find before reading on. The original briefing for this challenge (and the code) can be found here.

We thank all contestants for their hard work. It was rewarding to see so many security minded people take the time to make an effort. Findings, attack strategies and recommendations ranged enormously, beautifully revealing the diverse ways one can review code.

More than 20 contestants entered the Spot The Bug challenge and the deadline for submitting reports has passed. Based on the number of findings, the reports and the recommendations, we've picked a top 3:

  1. Cernica Ionut
  2. William Breuer
  3. Siu Siu Ha

Congratulations Cernica! You’ve earned the Parrot AR. Drone 2.0 Elite Edition, and it’s being sent your way.

The bugs

Basic application flow

The application contains a simple login mechanism. Logging in can be done by supplying a username, a password and a ‘nonce’. If the login is successful, a session token is generated with the nonce and the username. The username is appended to the session token. The session token is then stored in a cookie. The browser must always send the cookie and a nonce in the URL. To verify if the session is valid, the server recalculates the token with the username and the nonce. If the session token is valid, the user is greeted with his user data.

Authentication and authorization bypasses

“What do you mean, why's it got to be built? It's a bypass. You've got to build bypasses.”

1. Retrieving other users' data

If a user logs in or has a valid session token, the user’s data is retrieved using the method uf.getUserData.

68. String username = request.getParameter("user");
[...]
78. if (verifySession(ses, nonce))
88. {
89. 	message = "Welcome " + ses.split("-")[1];
90.		data = uf.getUserData(username);

The session token contains the username, and is used together with the nonce to generate the token for validation. To set the “Welcome” message, the username from the validated token is used. But the username for retrieving user data is derived from the user parameter in the URL! An attacker with any valid session can retrieve user data for any user by changing the user parameter. The programmer should have used the username from the validated session token.

2. Spoofing a successful login using the nonce

The nonce submitted by the user is matched against a regular expression. If there is no match, an error message is generated that contains the nonce. This error message is stored in the variable message.

When a login is successful or a valid session token is provided, the string “Welcome” is added to the message variable. Line 116 contains an if statement that checks if the variable message contains the string "Welcome". If it does, the application assumes the user is logged in.

82.	if (!uf.isNullOrEmpty(nonce) && nonce.matches("([a-zA-Z]+)*"))
83.		message = "Nonce must be a number, not " + nonce;
[...]
116. if (message.contains("Welcome"))

This check can be bypassed by supplying a nonce that contains "Welcome” and some characters that don’t match the regular expression. The generated error message will contain the nonce and thus the string “Welcome”, making the application think the user supplied a valid session.

3. Possible bypass due to insecure pattern matching

The checkLogin method is used to check if a login attempt is successful. The username and password provided by the user are sent to a SOAP service. Code for the SOAP service is not provided, but what we do know is that the response from the server is validated against a regular expression. This expression checks if the response from the server contains the string “Authorized”.

13.public Boolean checkLogin(String username, String password)
14.{
15.	String loginResult = uf.getLoginSOAPXML(username,password);
16.	return loginResult.matches("^.*Authorized.*$");
17.}

Any response from the SOAP service containing the string "Authorized" will make the application think the user logged in successfully. A few possible bypasses may be possible here. For example, if the server would return “Not Authorized”, the method will return true. Or if the username “Authorized” is provided and the service would respond with "User Authorized doesn't exist”, the login would be bypassed. Other possibilities are stack traces that contain “Authorized”, etc. Wildcard matching in such a way poses a security risk and should never be used in real applications.

Code execution

4. Spring Expression Language Injection (ELI)

The tag spring:message is used for displaying the username. The username variable comes from a URL parameter, and can contain any value.

118. Welcome <spring:message text="<%= username %>" /> <br> 

JSP Expression Language can be used for handy in-line JSP magic. This can come in handy for programmers. For example, the code “${param.user}” will evaluate to the user parameter in the URL.

The EL in the text attribute of the spring:message tag will be evaluated. But in some versions of Spring this value is evaluated twice! This means that the username provided by the user will also be evaluated as Expression Language. By passing EL in the username, it’s possible to retrieve or set all sorts or values. For example, is the username is “${uf.secret}”, we might get back the secret used by the application. In some cases of ELI Remote Code Execution can be achieved by using input such as “${java.lang.Runtime.getCurrentRuntime().exec(“..”)}”..

Denial of service

The next two vulnerabilities are partially caused by an integer overflow in the getSquare method. This method will square the nonce from the user and return it.

36. public Long getSquare(String nonce)
37. {
38. 	long n = Long.parseLong(nonce);
39. 	long squared = 0;
40. 	for(int i = 0; i != (n * Math.abs(n)); i++)
41. 	{
42. 		if (i < 0)	i = 0;
43. 		squared += 1;
44. 	}
45. 	return squared;
46. }

5. Infinite loop when nonce is negative

What if our nonce is negative? Notice the use of Math.abs. If the nonce is -5, then (n * Math.abs(n))* = *-5 * 5* = *-25. The integer i starts at 0 and will increase until it reaches n*Match.abs(n. If you’re not familiar with overflows you might think i can never become negative if we keep increasing it.

If i = 0, and we keep increasing i, can i every become negative? In many programming languages - including Java - this is the case. An integer like i can have a value from -2^31 to 2^31-1. If an integer has the value 2^31-1 and we increase it by one, the counter will continue at -2^31. This is called an integer overflow.

So if n is negative, i will continue counting until an overflow happens. But because of the code “if (i < 0) i = 0;”, i will become positive as soon as an overflow occurs. Thus the application will be caught in a infinite loop. A possible ‘patch’ for this issue would be to remove this if statement to allow i to become negative. However, this would not fix our following finding.

6. Infinite loop when nonce is greater than sqrt(maxint)

Longs can hold much larger values than integers. In our code, n is a long and i is an integer. As explained in the previous finding, if i becomes greater than 2^31-1, it will continue negative. So, if we can get n*Math.abs(n) to become greater than 2^31-1, the loop will continue infinitely. If the nonce n is greater than sqrt(maxint) = sqrt(2147483647) = 46340, i will never reach this value so it'll be stuck in an infinite loop.

7. Hanging a regular expression

The regular expression (regex) on line 82 checks if the nonce of a user contains lower or uppercase alphabet characters.

82.	if (!uf.isNullOrEmpty(nonce) && nonce.matches("([a-zA-Z]+)*"))

If we translate this regex to a sentence, it would be something like “Find zero or more occurrences of one or more occurrences of lower or uppercase alphabet characters”. Such an expression can be tricked to evaluate in exponential time.

Let’s say our input is the string “aaa”. Our regex will go through the string to find that all characters match the expression (“This string contains one occurrence of three occurrences of the letter a”). Great. But if our string is “aaa!”, our regex will first go through all the characters only to find that “This string contains one occurrence of three occurrences of the letter a, but also an invalid character”. Now it will continue to find all combinations of two consecutive a’s, then all combinations of single a’s. For every a we add, the regex will evaluate an exponential amount of possibilities.

With an input like ”aaaaaaaaaaaaaaaaaaaaaaaa!”, the evaluation will take so long that the application will hang. This is a classic example of a ReDoS vulnerability.

Cross-Site Scripting

A Cross-Site Scripting (XSS) vulnerability is a vulnerability that allows attackers to inject malicious scripts into a webpage of a user.

8. Reflected XSS in the return URL

The most basic XSS vulnerability is caused by the output of the returnUrl URL parameter. This parameter is shown on the page without any encoding.

125. 	Please <a href="<%= request.getParameter("returnUrl") %>">return</a> to the login page.

An attacker can craft a URL to inject malicious scripts into the web page. For example, if the returnUrl parameter is:

”></a><script>alert(1);</script><a href=“ (including the double quotes)

the generated source code will be:

Please <a href=""></a><script>alert(1);</script><a href="">return</a> to the login page.

The script tags are injected into the webpage, and the Javascript "alert(1)" will be evaluated by the browser. This is called a reflected XSS vulnerability, because the attacker’s value is reflected from the URL onto the web page. By tricking a user into clicking on a specially crafted URL, or by making the user visit an external website that triggers this URL, an attacker can inject any Javascript into the webpage of the user.

9. Reflected XSS using the nonce

When the nonce contains lowercase or uppercase characters, the nonce will be appended to message. The application will return this value without any encoding, resulting in an XSS vulnerability just like the previous one.

82. if (!uf.isNullOrEmpty(nonce) && nonce.matches("([a-zA-Z]+)*"))
83. message = "Nonce must be a number, not " + nonce;
[...]
124.	Error: <%= message %> <br>

10. Reflected XSS through spring:message tag

This one is a bit tricky. The spring:message tag used in the application outputs the username as provided by the user.

118. 	Welcome <spring:message text="<%= username %>" /> <br>

Some contestants claimed that this could be used for reflected XSS. However, only certain old versions of the Spring framework contain this vulnerability. Points for this finding were only given if the contestant specified that only certain Spring versions were vulnerable, with extra points for exact version information.

11. Persistent XSS via user data

User data is shown on the page without encoding. If an attacker can inject malicious scripts into user data, the script will be evaluated by the browser.

119. 	<%= data %>

To exploit this vulnerability, the malicious script (the payload) only needs to be injected once. When the payload is stored on the server as user data, the server will serve this payload to the user every time the page is visited. This is called persistent XSS.

Information disclosure

12. Retrieving a valid session token

The programmer's comment contains a valid session token.

4. <!-- 
5. 	These sessions need not be saved server-side!
6. 	Example of an authorization cookie:
7. 	amwwgcqpdtxthauepzxbjrropg-testuser	
8. 	-->

This is an HTML comment, and HTML comments can be read by anyone. This discloses the valid session to anyone viewing the page's source. As we’ve seen before, one valid session is enough to retrieve user data for any user. And as we'll see later, one token is enough to crack the rotation secret. Sensitive information like this should never be stored inside of code comments.

13. Exception details shown in response

The application will return details of any exception.

109. catch (Exception e)
110. {
111.	message = "Login Error: " + e.getMessage();	
112.}

Any information about the inner workings of an application can be of aid during the reconnaissance phase of an attack. Exception details can provide insight into used methods, how variables are parsed (remember our DoS vulnerabilities?), software packages and versions, etc. In a production environment, applications should never return any such debug information.

Cryptography

48. public String generateSecret(Long seed)
49. {
50. 	Random gen = new Random(seed);
51. 	String dat = "";
52. 	for (int i = 0; i <= 25; i++)
53. 		dat += (char)(gen.nextInt(25) + 97);	
54. 	dat = uf.ROTWithSecret(dat);
55. 	return dat;
56. }

14. Guessing the cipher used

Line 54 hints that a rotation cipher is used to encode the generated token. Rotating means we increase a letter by some value. For example, if we rotate by one, “a” becomes “b”, “b becomes “c” and “z” becomes “a”. Our code rotates with the value of characters of some secret. We’ll come back to this later on in this article to crack the secret.

Most such classical ciphers can easily be cracked and should never be used in modern applications.

15. Deterministic PRNG used in security context

The programmer uses Random to generate a sequence for a security related string such as a session token. This method uses a linear congruential generator. Such a generator should not be used in a security context, because the output can be predicted in some cases. Predicting session tokens would break this application’s security completely.

Sessions should be randomly generated with a cryptographically strong random number generator.

16. Seeding the PRNG with user input

As stated in the previous finding, the Random method is used to generate a sequence of numbers. Such numbers are generated from a starting point called a seed. This method will always return the same sequence of numbers if it’s given the same seed. In our code, the seed for generateSecret (and thus for the Random method) is based on the nonce and the username provided by the user.

28. public String createSession(String nonce, String username)
29. { 
30. 	Long squaredNonce = getSquare(nonce);
31. 	int UniqueUsernameNumber = getUniqueUsernameNumber(username);
32. 	
33. 	return generateSecret(squaredNonce + UniqueUsernameNumber) + "-" + username;
34. }

Any combination of username and nonce will always produce the same session token. This increases the attack surface of the application. For example, what if two users generate the same seed?

Miscellaneous

17. Not so unique username numbers

The method getUniqueUsernameNumber sums up the characters in a username. But it does not return unique numbers for usernames like the name suggests. The username ”ab” will generate the same number as “ba”.

To hijack the account of the user “admin”, an attacker could register an account with username “admni”. With a valid session, changing the username in the session token to “admin” will generate the same number and thus the attacker's session will be seen as valid.

An attacker might even register a username with a number equal to a Cross-Site Scripting payload or even an ELI payload!

18. Blacklist validation

The purpose of the regex on line 82 is to check if the nonce that is provided by the user is a number.

82.	if (!uf.isNullOrEmpty(nonce) && nonce.matches("([a-zA-Z]+)*"))
83.	message = "Nonce must be a number, not " + nonce;

A blacklist is used here. Blacklisting means you block some input if it matches some value(s). The values in this case are the lower and uppercase alphabet. One problem with blacklisting is that it’s easy to forget characters or sequences to block. For example, this code will not block a nonce such as ”123!”.

It’s best practice to use a whitelist in such cases. This means only allowing the nonce if it consists of numbers, instead of blocking it if it consists of certain non-numbers. The advantage of a whitelist over a blacklist is that you can’t forget to miss certain characters to block, because all characters except the whitelisted characters are blocked by default.

The session cookie is missing the HTTPOnly and Secure flags.

The Secure flag ensures that the browser will only send the cookie over a secure connection. If the cookie is send over an insecure connection, an attacker performing a Man-in-the-middle attack can intercept and/or modify the vulnerable cookie.

The HTTPOnly flag prevents the cookie from being read or manipulated through JavaScript. JavaScript is used in attacks such as Cross-Site Scripting. With a successful Cross-Site Scripting attack an attacker can steal all cookies that don’t have this flag enabled.

20. Sessions don't expire

Sessions in the application will never expire. If a session token gets stolen form a user then the token can be used to log in as the user indefinitely.

21. Other bugs/recommendations from contestants

  • Header injection through username in cookie
  • Missing brackets could cause security bugs like goto fail
  • Increasing the possible entropy of the tokens by using Base64 encoding: “The secrets live inside a 116bit space, which is reasonably weak. Base64 encoding 18 8-bit bytes would take the same space and give you 144 bits (260 million times more combinations). “
  • Username and password in URL could leak information
  • No logout functionality
  • No CSRF protection
  • Data interception if webpage is visited over an unencrypted channel
  • Open redirect using the redirectUrL parameter
  • Servlet logic inside the JSP (missing mcv development pattern)
  • Missing brute force protections
    • If an attacker intercepts a cookie, he could brute force the nonce
    • Possible to brute force username/password
  • HTTP status codes not implemented
  • Cookie's expiration time is too long
  • Missing audit logs
  • Code does not comply with the Java Coding Conventions
  • Missing Javadoc
  • Exceptions are handled in a too broad way
  • Deeply nested conditional statements

Cracking the secret

Some reports stated that an attacker could use session data to get to know the secret used in the rotation-cipher. In this section one way to crack the secret will be demonstrated.

The application creates a session like this:

data = generateSecret(nonce, usernamenumber)
secret
---------- +ROT
session

Because of the HTML comment, we have a valid session. If we want to know the secret, we can just rotate the session back like this:

session
data = generateSecret(nonce, usernamenumber)
---------- -ROT
secret

In this scenario, we know the session and the username (and thus the username number). The only thing that is missing is the nonce. Remember that the nonce can't be greater than sqrt(maxint) = 46340? One assumption an attacker can make is that the programmer uses four-digit nonces, because if he had used 5-digit nonces he would notice the application freezing a lot.

One way to crack the secret is to generate every token from before the rotation, and rotate the session back like described above. Let's first generate all tokens for four-digit nonces.

String username, nonce;
username = "testuser";
for (int i = 0; i < 10000; i++) { 
	nonce = Integer.toString(i);
	System.out.println(createSession(nonce, username));
}

Generating will take reasonable time. By writing the output of the above code to a file, we've obtained all possible tokens from before the rotation (for four digit nonces). What we need to do now, is rotate the session back with every token to obtain possible secrets. Then, using a dictionary attack, we can check if a secret contains valid words.

The rotation cipher contains one little tweak. Normally, a basic ROT would be to take the value of a letter in the alphabet, for example 'a', and assign a number to it. So a=1, b=2, etc. Our programmer’s implementation used a=0, b=1, c=2..

The following Python script will perform the reverse rotation and the dictionary attack.

englishWords = open('/usr/share/dict/words','r').read().split("\n")
allSecrets = open('allSecrets','r').read().split("\n")
isEncoded =  "amwwgcqpdtxthauepzxbjrropg"
def reverse(notEncoded):
	global isEncoded
	leSecret = []
	for i,v in enumerate(notEncoded):
		a = ord(v)
		b = ord(isEncoded[ i])
		if b < a:
			dif = (26 - abs(b-a))
		elif b == a:	dif = 0
		else:
			dif = abs(b-a)
		leSecret.append(chr(dif + 97))
	return ''.join(leSecret)
for secretLine in allSecrets:
	if secretLine == '':	continue
	try:
		notEncoded = secretLine.split("-")[0]
	except:
		print "Error splitting line: " + secretLine
	reversedSecret = reverse(notEncoded)
	for word in englishWords:
		if word == '' or len(word) < 6:	continue
		if word in reversedSecret:
			print reversedSecret + " -> " + word

The output will be:

alwebeyelpcpaqapsoubolffrr -> webeye
iamgeniusiamgeniusiamgeniu -> genius
lcmrmxwagaunoafrpsmwwlknfn -> wagaun
vvpalqswallotamdmcodcdhocf -> swallo
sooraffiahbdduutfepnwphgyy -> raffia
ncuzpeoavseyfsgpoposolesfd -> posole
dymalkinaglqhawwpznlfzvcnk -> malkin
oigfetblarnyjjbcrsstvgmure -> blarny

Only one secret is not gibberish, and that is "iamgeniusiamgeniusiamgeniu". Thus the secret rotation key is 'iamgenius'! With this key, one can create a valid session for any username.

See you next time

So what did you think of the challenge? Too simple, too hard? Any constructive feedback is welcome, as well as hints/tips or bug ideas. We'll try to incorporate your feedback to improve future challenges. The email address for this challenge is dronemission@securify.nl.

You can subscribe to our Spot The Bug mailing list by sending an email to spotthebug@securify.nl. You'll receive information about the status of current and future challenges.

Creating this challenge was both exciting and interesting. All bugs from this challenge have existed in real applications, and we find bugs like these on a daily basis. If you're interested in working as a code reviewer feel free to drop us a line.

Questions or feedback?