Thursday 21 September 2017

Web Tutorial: The Anti-CSRF Token

Today's web tutorial is security-based, and it's one of the most elementary things you should know about when developing web applications.

I will be demonstrating a very simple Cross Site Request Forgery (CSRF) attack, and detailing how to foil it. Most frameworks already include this protection, but I would not recommend relying exclusively upon this protection without at least a rudimentary understanding of how it works.

A CSRF occurs when one party outside of your web application's domain makes a request to your web application, mimicking all the necessary data needed for the request to be processed. If that sounded like gibberish to you, maybe the diagram below will help.

CSRF attack diagram


And if that still doesn't help, no sweat. I'll be walking you through an example.

Take this PHP code. I'm not going to explain every line because that's not the purpose of this tutorial. Basically, this code makes a request to tx.php to return some data.
index.php
<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Test</title>
    </head>
    <body>
        <form method="POST" action="tx.php">
            Show transactions with:
            <select name="ddlTxWith">
                <option value="0">Sundar Pichai</option>
                <option value="1">Mark Zuckerberg</option>
                <option value="2">Steve Jobs</option>
            </select>
            <input type="submit" value="Go">
        </form>
    </body>
</html>


This is what you should see when your server runs it. Here, I'm assuming that the intended user is already logged in. You have a drop-down list with three big names, and clicking the "Go" button will reveal all the transactions you've had with the selected person.


Now, this code defines a multi-dimensional array, simulating some data from a database. It takes the value of the drop-down list submitted, and uses it to grab the required data.
tx.php
<?php
$TxWith = -1;
$TxObj = array();

$Tx = array();
$Tx[0][0] = array("Date"=>"20 May 2010", "Amount"=> 200, "Comments" => "10-course dinner");
$Tx[0][1] = array("Date"=>"5 July 2016", "Amount"=> 10500, "Comments" => "Website fees for Google domain");
$Tx[0][2] = array("Date"=>"18 June 2011", "Amount"=> 50, "Comments" => "Monthy Gmail fee");

$Tx[1][0] = array("Date"=>"10 July 2011", "Amount"=> 660, "Comments" => "Facebook ad registration");
$Tx[1][1] = array("Date"=>"10 September 2011", "Amount"=> 2, "Comments" => "Starbucks coffee");

$Tx[2][0] = array("Date"=>"10 June 2010", "Amount"=> 2500, "Comments" => "Apple design");
$Tx[2][1] = array("Date"=>"12 June 2012", "Amount"=> 1200, "Comments" => "iOS Seminar Booth");
$Tx[2][2] = array("Date"=>"5 August 2015", "Amount"=> 2000, "Comments" => "iPad");

if (isset($_POST["ddlTxWith"]))
{
        $TxWith = intval($_POST["ddlTxWith"]);
        $TxObj = $Tx[$TxWith];
}

?>

<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Test</title>
    </head>
    <body>
        <?php
        if (sizeof($TxObj)>0)
        {
            for ($i = 0; $i< sizeof($TxObj); $i++)
            {
                echo "Date: " . $TxObj[$i]["Date"] . "<br />";
                echo "Amount:  $" . $TxObj[$i]["Amount"] . "<br />";
                echo "Comments: " . $TxObj[$i]["Comments"] . "<br />";
                echo "<br />";
            }
        }
        ?>
    </body>
</html>


So, for example, if you select "Steve Jobs" and click "Go", this is what you get. Yes, I know in the real world, Steve Jobs is not going to pay me $2000 for an iPad (besides, the dude is dead), but I can dream, right?


Here comes the attack!

Now, on a separate folder, which we'll call csrf_attack, let's create index.html. That's right, you don't even need sever-side code to do a CSRF. Scary, huh?
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Attack</title>
    </head>
    <body>

    </body>
</html>


OK, there's a blank HTML template right there. How do we know what variables to send? Well, assuming you had an account for that web application in csrf_test, you could view the source and get this...



That's just one way out of a multitude of rather more sophisticated (and automated) methods. I'm just using the most obvious way.

So after that, we use the code! Note that in the action parameter of the form tag, we've set it to submit the request to the site we're attacking. In this case, it's localhost/csrf_test/tx.php.
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Attack</title>
    </head>
    <body>
        <form method="POST" action="http://localhost/csrf_test/tx.php">
            <select name="ddlTxWith">
                <option value="0">Sundar Pichai</option>
                <option value="1">Mark Zuckerberg</option>
                <option value="2">Steve Jobs</option>
            </select>
            <input type="submit" value="Go">
        </form>

    </body>
</html>


Open this up in another browser. I'm using Chrome for csrf_test, so let's go with Firefox for csrf_attack.


Now click Go, and you have all the transactions with Sundar Pichai! That's data that you, as an attacker, have no right to. Viewing unauthorized data is damaging enough; imagine if your request actually involved editing, adding or deleting data. Or, if this page actually allowed a user to perform transactions, an attacker could use this to send money from the victim to himself.


Foiling the attack

The recommended way is to use an anti-CSRF token, one that the attacker cannot replicate. You could use a randomly-generated token... or you could use one that has already been provided by you, via PHP's session token.

So do this. It begins a PHP session. Ordinarily, you would already have this code, if the page handled user logins.
index.php
<?php
session_start();
?>


<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Test</title>
    </head>
    <body>
        <form method="POST" action="tx.php">
            Show transactions with:
            <select name="ddlTxWith">
                <option value="0">Sundar Pichai</option>
                <option value="1">Mark Zuckerberg</option>
                <option value="2">Steve Jobs</option>
            </select>
            <input type="submit" value="Go">
        </form>
    </body>
</html>


Add this to the HTML portion. It's a hidden field, with the session id embedded. For extra security, we'll hash it with MD5 encryption.
index.php
<?php
session_start();
?>

<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Test</title>
    </head>
    <body>
        <form method="POST" action="tx.php">
            Show transactions with:
            <select name="ddlTxWith">
                <option value="0">Sundar Pichai</option>
                <option value="1">Mark Zuckerberg</option>
                <option value="2">Steve Jobs</option>
            </select>
            <input type="submit" value="Go">
            <input type="hidden" name="hidCSRF" value="<?php echo md5(session_id()); ?>">
        </form>
    </body>
</html>


View your source. See that hidden field with "f7c6332b0ec5529210f7959a0d304521"in there? That's the MD5 hash of your unique session id.


Now, in tx.php, we'll start a session as well.
tx.php
<?php
session_start();

$TxWith = -1;
$TxObj = array();

$Tx = array();
$Tx[0][0] = array("Date"=>"20 May 2010", "Amount"=> 200, "Comments" => "10-course dinner");
$Tx[0][1] = array("Date"=>"5 July 2016", "Amount"=> 10500, "Comments" => "Website fees for Google domain");
$Tx[0][2] = array("Date"=>"18 June 2011", "Amount"=> 50, "Comments" => "Monthy Gmail fee");

$Tx[1][0] = array("Date"=>"10 July 2011", "Amount"=> 660, "Comments" => "Facebook ad registration");
$Tx[1][1] = array("Date"=>"10 September 2011", "Amount"=> 2, "Comments" => "Starbucks coffee");

$Tx[2][0] = array("Date"=>"10 June 2010", "Amount"=> 2500, "Comments" => "Apple design");
$Tx[2][1] = array("Date"=>"12 June 2012", "Amount"=> 1200, "Comments" => "iOS Seminar Booth");
$Tx[2][2] = array("Date"=>"5 August 2015", "Amount"=> 2000, "Comments" => "iPad");

if (isset($_POST["ddlTxWith"]))
{
        $TxWith = intval($_POST["ddlTxWith"]);
        $TxObj = $Tx[$TxWith];
}

?>

<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Test</title>
    </head>
    <body>
        <?php
        if (sizeof($TxObj)>0)
        {
            for ($i = 0; $i< sizeof($TxObj); $i++)
            {
                echo "Date: " . $TxObj[$i]["Date"] . "<br />";
                echo "Amount:  $" . $TxObj[$i]["Amount"] . "<br />";
                echo "Comments: " . $TxObj[$i]["Comments"] . "<br />";
                echo "<br />";
            }
        }
        ?>
    </body>
</html>


And then we'll add an If conditional block to check if the MD5 hash of your current session id matches the one you sent in the form!
tx.php
<?php
session_start();

$TxWith = -1;
$TxObj = array();

$Tx = array();
$Tx[0][0] = array("Date"=>"20 May 2010", "Amount"=> 200, "Comments" => "10-course dinner");
$Tx[0][1] = array("Date"=>"5 July 2016", "Amount"=> 10500, "Comments" => "Website fees for Google domain");
$Tx[0][2] = array("Date"=>"18 June 2011", "Amount"=> 50, "Comments" => "Monthy Gmail fee");

$Tx[1][0] = array("Date"=>"10 July 2011", "Amount"=> 660, "Comments" => "Facebook ad registration");
$Tx[1][1] = array("Date"=>"10 September 2011", "Amount"=> 2, "Comments" => "Starbucks coffee");

$Tx[2][0] = array("Date"=>"10 June 2010", "Amount"=> 2500, "Comments" => "Apple design");
$Tx[2][1] = array("Date"=>"12 June 2012", "Amount"=> 1200, "Comments" => "iOS Seminar Booth");
$Tx[2][2] = array("Date"=>"5 August 2015", "Amount"=> 2000, "Comments" => "iPad");

if (isset($_POST["ddlTxWith"]))
{
    if (md5(session_id()) == $_POST["hidCSRF"])
    {

        $TxWith = intval($_POST["ddlTxWith"]);
        $TxObj = $Tx[$TxWith];
    }
    else
    {
        echo "You are not authorized to view this data.";
    }

}

?>

<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Test</title>
    </head>
    <body>
        <?php
        if (sizeof($TxObj)>0)
        {
            for ($i = 0; $i< sizeof($TxObj); $i++)
            {
                echo "Date: " . $TxObj[$i]["Date"] . "<br />";
                echo "Amount:  $" . $TxObj[$i]["Amount"] . "<br />";
                echo "Comments: " . $TxObj[$i]["Comments"] . "<br />";
                echo "<br />";
            }
        }
        ?>
    </body>
</html>


Try your code again. See if you can still get all of the transactions with, say, Mark Zuckerberg? There should be no change to the results. It should all be transparent to the user.


Now let's attack again!

Let's grab the code and add it to your index.html. Yes, even the hidden field.
index.html
<!DOCTYPE html>
<html>
    <head>
        <title>CSRF Attack</title>
    </head>
    <body>
        <form method="POST" action="http://localhost/csrf_test/tx.php">
            <select name="ddlTxWith">
                <option value="0">Sundar Pichai</option>
                <option value="1">Mark Zuckerberg</option>
                <option value="2">Steve Jobs</option>
            </select>
            <input type="hidden" name="hidCSRF" value="f7c6332b0ec5529210f7959a0d304521">
            <input type="submit" value="Go">
        </form>

    </body>
</html>


Run it. Then try to get all transactions with Steve Jobs. Bingo! The attacker gets nothing, because tx.php's unique session id for the attacker did not match the one sent in the form!


What if the attacker could get hold of the actual session id that a user is currently using?

Good thinking!

But let's consider this - a useful session id needs to not yet have expired, which means the user in question must still be in an active session. The window to act is pretty small. If the attacker could get that, there would be no need to resort to a CSRF. He'd probably have a far more direct means of attack at his disposal.

So this method is fool-proof?

Nothing's ever 100% fool-proof. But, for that threat level, this is probably adequate.

That's all for today. Good Job(s)!
T___T

No comments:

Post a Comment