Accedi per seguire   
Seguaci 0
cereal

Php Form Spoofing & Brute Force

7 messaggi in questa discussione

Sto scrivendo un form di login, e sto cercando di renderlo sicuro, restando il fatto che mi trovo su un hosting condiviso con PHP 4, attualmente:

  1. verifico che i dati vengano ricevuto con POST, utilizzo la variabile REQUEST_METHOD
  2. verifico l'ordine di invio dei dati
  3. verifico la natura dei dati inviati compresa la lunghezza della password
  4. limito i risultati della query di verifica ad un solo risultato con LIMIT 1

Fatto questo mando in esecuzione lo script, se è tutto regolare uso setcookie e header per entrare nella zona "protetta". Mi chiedevo se vale la pena di:

  1. registrare in un file log o database i tentativi di login non riusciti, nonché ogni connessione riuscita
  2. inserire un controllo sul referer: è un'informazione trasportata dal client percui può essere modificata
  3. limitare il numero di tentativi di login, tipo attesa di 15 minuti dopo tre tentativi falliti
  4. utilizzare i captcha per impedire l'uso di bot
  5. utilizzare un token

Il punto C non saprei come svilupparlo, ci ho pensato un po' e credo si potrebbe fare in questo modo: creo una sessione nella pagina di login, ogni tentativo di connessione lo salvo in una tabella del database, in cui registro: IP, data del tentativo, USER_AGENT, sessione, user e password tentati.

Per ogni tentativo che giunge dalla medesima sessione salvo un nuovo record con i dati, di cui cambiano soltanto la data del tentativo, user e password. Un costrutto if else controlla che il numero di tentativi sia inferiore a tre, nel caso contrario blocca l'invio dei dati e impedisce all'utente di riprovarci per altri 15 minuti. Il tempo lo stabilisco con l'ultimo update della data, che non faccio aggiornare dopo i tre tentativi andati male.

Come identifico l'utente che sbaglia? Bastano ip e user agent o devo aggiungere un cookie?

Trascorsi i 15 minuti come cancello i dati nel database che bloccano l'utente? Uso sleep? Non li cancello?! Creo una nuova sessione e annullo quella precedente? Hmm.. Sono consapevole del fatto che all'utente basta cambiare browser o user agent per ritentare, e che probabilmente uno più bravo potrebbe utilizzare tecnologie come Tor per cambiare continuamente IP oltre che lo user agent. Che manca?

Da qualche parte, ho anche visto chi codifica la password con sha1 già nel client con un javascript, credo sia una misura in più perché i dati viaggiano in chiaro.. tuttavia mi chiedo che senso abbia, tanto un attaccante può usare tranquillamente sia la stringa criptata quanto la password in chiaro, no?

Non appena sistemo lo script lo inserisco in questo thread.

Gracias per l'attenzione. :)

Condividi questo messaggio


Link di questo messaggio
Condividi su altri siti

Per il punto C puoi fare tutto senza utilizzare il db :)

Invece che fare tutte le operazioni da te elencate puoi:

- Creare comunque una sessione per ogni utente che accede al forum (compresi quelli che non fanno login). Questi utenti verranno resi unici ugualmente dalla variabile di sessione. Al primo tentativo di login di questo utente, gli imposti una variabile di sessione con l'ora del tentativo (ti serve per controllare i 15 minuti di tempo). Imposti anche la variabile (sempre di sessione) con il numero di tentativi a 1. All'n-esimo tentativo (se errato) controlli l'ora, fai la sottrazione. Se è oltre i 15 minuti imposti il contatore tentativi a 1, altrimenti controlli che la variabile non sia già a 2. Se fosse a 2 quello è il 3° tentativo ed ha finito le prove. Se è a 1, la incrementi a 2 :P

Per quanto riguarda la sicurezza prima di passare username e password al db, "escapizzala" e "sql-injeckizzala" in modo che non ti trovi la maniera di ottenere comunque un record dal db che viene visto dal tuo controllo d'accesso come valido. Poi, se fossi in te, registrerei sul DB solo hash md5 o sha1 e la password la convertirei in quell'hash prima di spedirla al server (e al db).

Condividi questo messaggio


Link di questo messaggio
Condividi su altri siti

Ok, grazie per i consigli! Riguardo al punto C pensavo al database per tenere traccia dei tentativi, ma posso sempre usare dei file di log temporanei. Uso sha1 che sembra essere più sicuro di md5, sul db conservo gli hash. Lo script attuale è questo:

<?php
if($_SERVER['REQUEST_METHOD'] == "POST") {

if($_POST['form'] == "login") {

	$form = array();
	$form[] = 'form';
	$form[] = 'username';
	$form[] = 'password';
	$form[] = 'login';

	$values = array_keys($_POST);

	if($form == $values) {

		function clean($string) {
		   $string = stripslashes($string);
		   $string = htmlentities($string);
		   $string = strip_tags($string);
		   $string = ctype_alnum($string) ? $string : NULL;
		   $string = (strlen($string) >= 6) ? $string : NULL;
		   return substr($string, 0, 10);
		}

		$u = @clean($_POST['username']);
		$p = @clean($_POST['password']);

		if($u != NULL && $p != NULL) {
			$pwd = @sha1($p);
			@include $_SERVER['DOCUMENT_ROOT'] . "/db/conn.inc";
			$sql = "SELECT * FROM users WHERE user = '" . $u . "' AND pass = '". $pwd . "' LIMIT 1";
			$result = @mysql_query($sql);
			$check = @mysql_num_rows($result);
			if($check == 0) {
				echo 'Login failed! Please retry.';
			} else {
				$expire = time() + 3600;
				setcookie(User, $u, $expire);
				setcookie(Pass, $pwd, $expire);
				@header('Location: destination');
			}
		} else {
			echo 'Login failed! Please retry.';
		}
	}
}
}
?>

<form action="/login.php" method="post">
<input name="form" type="hidden" value="login" />
<input name="username" type="text" />
<input name="password" type="password" />
<input name="login" type="submit" value="login" />
</form>

Ci lavoro su ^_^'

ciao :)

Condividi questo messaggio


Link di questo messaggio
Condividi su altri siti

Se ti serve una history dei tentativi, allora si, puoi tranquillamente usare il db. Alla fine sarà anche più comodo che avere dei file di testo sparsi in giro...

Puoi unire le due soluzioni: Per gestire i timeout dei tentativi usi le sessioni. Ogni tentativo (a buon fine o meno) lo logghi sul db...

:)

Condividi questo messaggio


Link di questo messaggio
Condividi su altri siti

Oltre alla history ho trovato un motivo valido per usare il database: limitare le richieste sui singoli nick ad un massimo di tre tentativi ogni quindici minuti. Con le sessioni basta aprire più connessioni con altrettanti browser, macchine virtuali o pc remoti, bloccando il nick 'fermo' (rallento) chiunque provi a trovare la password di quel determinato nick. Ecco una bozza:

<?php
session_start();
ob_start("ob_gzhandler");
$checktIdle = time() - $_SESSION['checkIdle'];
$checkAge = time() - $_SESSION['checkTime'];
function clean($string, $min=6, $max=10) {
	$string = stripslashes($string);
   $string = htmlentities($string);
   $string = strip_tags($string);
   $string = ctype_alnum($string) ? $string : NULL;
	$string = (strlen($string) >= $min) ? $string : NULL;
	return substr($string, 0, $max);
}

function queryUsers($stringA,$stringB) {
	$sql = "SELECT * FROM users WHERE user = '" . $stringA . "' AND pass = '" . $stringB . "' LIMIT 1";
	$result = mysql_query($sql);
	$row = mysql_num_rows($result);
	return $row;
}

function queryHash($stringA,$stringB) {
	$sql = "SELECT * FROM users WHERE userHash = '" . $stringA . "' AND pass = '" . $stringB . "' LIMIT 1";
	$result = mysql_query($sql);
	$row = mysql_num_rows($result);
	return $row;
}

function blackList($string) {
	$created = date('Y-m-d H:i:s');
	mysql_query("INSERT INTO history (user, created) VALUES ('$string','$created')");
}

function checkShots($string) {
	$sql = mysql_query("SELECT * FROM history WHERE user = '" . $string . "' LIMIT 1");
	$row = mysql_fetch_array($sql);
	return $row;
}

function saltSha1($a,$b) {
	$salt = "1447" . substr($a,0,2) . "2334" . substr($b,1,3) . "0246" . $a . "76221";
	$result = sha1($b . $salt);
	return $result;
}

function attemps($u) {
	if($row['attemps'] != 0 && $row['attemps'] <= 2) {
		$created = date('Y-m-d H:i:s');
		mysql_query("UPDATE history SET attemps = $row[attemps] + 1, created = '$created' WHERE user = '$u'");
		echo $u . ' exist, but wrong password ';
	}

	if($row['attemps'] == 0) {
		blackList($u);
		echo $u . ' added to temporary blacklist';
	}
}

function rand_string() {
	$string = uniqid(mt_rand(), true);
	return sha1($string);
}

function home($u,$p,$type) {

	if($type == "POST") {
		$usr = saltSha1($u,$p);
		session_regenerate_id();
		$check = rand_string();
		$expire = @time() + 3600;
		$_SESSION['userHash'] = $usr;
		$_SESSION['user'] = $u;
		$_SESSION['check'] = $check;
		$_SESSION['checkTime'] = time();
		$_SESSION['checkIdle'] = time();
		$_SESSION['screenSaver'] = 'safe';
	} 

	if($row['username'] == $u) {
		mysql_query("DELETE FROM history WHERE user = '$u' LIMIT 1");
	}
	$lastlog = date('Y-m-d H:i:s');
	mysql_query("UPDATE users SET lastlog = '$lastlog' WHERE user = '$u'");
	@header('Location: /destination.php');
	exit;
}


if($_SERVER['REQUEST_METHOD'] == "POST") {

if($_POST['form'] == "login") {

	$form = array();
	$form[] = 'form';
	$form[] = 'user';
	$form[] = 'pass';
	$form[] = 'login';

	$values = array_keys($_POST);

	if($form == $values) {

		$u = clean($_POST['user'],6,10);
		$p = clean($_POST['pass'],6,10);

		if($u != NULL && $p != NULL) {
			@include $_SERVER['DOCUMENT_ROOT'] . "db/connect.inc";

			$row = checkShots($u);

			if($row['attemps'] == 0 || $row['attemps'] <= 2) {
				$pwd = saltSha1($u,$p);
				$usr = saltSha1($p,$u);
				$r = queryUsers($u, $pwd);

				if($r == 0) {
					if($row['attemps'] != 0 && $row['attemps'] <= 2) {
						$created = date('Y-m-d H:i:s');
						mysql_query("UPDATE history SET attemps = $row[attemps] + 1, created = '$created' WHERE user = '$u' ORDER BY id DESC LIMIT 1");
						echo $u . ' exist, but wrong password ';
					}

					if($row['attemps'] == 0) {
						blackList($u);
						echo $u . ' added to temporary blacklist';
					}
				} else {
					//Finally home!
					home($u,$p,'POST');
				}


			} else {
				//correggere l'echo, viene visualizzato anche quando $u viene tolto dalla blacklist
				echo 'wrong way <strong>' . $u . '</strong> in blacklist for 15 minutes';
				if($row['attemps'] >= 3) {
					$minutes = strtotime("-15 minutes");
					$deadline = date("Y-m-d H:i:s", $minutes);
					$ask = mysql_query("SELECT * FROM history WHERE user = '$u' AND created <= '". $deadline ."' LIMIT 1");
					$response = mysql_num_rows($ask);

					if($response == 1) {
						$created = date("Y-m-d H:i:s");
						mysql_query("UPDATE history SET attemps = 1, created = '$created' WHERE user = '$u'");
					}
				}
			}
		}
	}
}
//SESSION idle timeout check
if($_POST['form'] == "confirm") {
	$form = array();
	$form[] = 'form';
	$form[] = 'password';
	$form[] = 'login';

	$values = array_keys($_POST);

	if($form == $values) {

		$u = clean($_SESSION['user'],6,10);
		$p = clean($_POST['pass'],6,10);

		if($u != NULL && $p != NULL) {
			@include $_SERVER['DOCUMENT_ROOT'] . "db/connect.inc";

			$row = checkShots($u);

			if($row['attemps'] == 0 || $row['attemps'] <= 2) {
				$pwd = saltSha1($u,$p);
				$usr = saltSha1($p,$u);
				$r = queryUsers($u, $pwd);

				if($r == 0) {
					if($row['attemps'] != 0 && $row['attemps'] <= 2) {
						$created = date('Y-m-d H:i:s');
						mysql_query("UPDATE history SET attemps = $row[attemps] + 1, created = '$created' WHERE user = '$u' ORDER BY id DESC LIMIT 1");
						echo $u . ' exist, but wrong password ';
					}

					if($row['attemps'] == 0) {
						blackList($u);
						echo $u . ' added to temporary blacklist';
					}
				} else {
					//Finally home!
					home($u,$p,'POST');
				}


			} else {
				//correggere l'echo, viene visualizzato anche quando $u viene tolto dalla blacklist
				echo 'wrong way <strong>' . $u . '</strong> in blacklist for 15 minutes';
				if($row['attemps'] >= 3) {
					$minutes = strtotime("-15 minutes");
					$deadline = date("Y-m-d H:i:s", $minutes);
					$ask = mysql_query("SELECT * FROM history WHERE user = '$u' AND created <= '". $deadline ."' LIMIT 1");
					$response = mysql_num_rows($ask);

					if($response == 1) {
						$created = date("Y-m-d H:i:s");
						mysql_query("UPDATE history SET attemps = 1, created = '$created' WHERE user = '$u'");
					}
				}
			}
		}
	}
}
}

//now check for cookies and get
/*** cookies: ***/

elseif(isset($_COOKIE['user']) && isset($_COOKIE['pass'])) {
	echo ' wrong user or password ';
}

/*** get:	***/

elseif($_SERVER['REQUEST_METHOD'] == 'GET') {
	if(isset($_GET['user']) || isset($_GET['pass'])) {
		echo ' wrong user or password ';
	}
}

/*** session:
check if session is expired
if not go inside
if yes force to login
***/

if(isset($_SESSION['check']) && isset($_SESSION['user']) && isset($_SESSION['userHash'])) {
	if($checkAge <= 10800) {
		if($checktIdle <= 900) {
			mysql_query("UPDATE users SET lastlog = '$lastlog' WHERE user = '$_SESSION[user]'");
			header('Location: /destination.php');
			exit;
		} else {
			unset($_SESSION['userHash']);
		}
	} else {
		unset($_SESSION['checkTime']);
		unset($_SESSION['checkIdle']);
		unset($_SESSION['user']);
		unset($_SESSION['userHash']);
		unset($_SESSION['check']);
	}
}


if(isset($_SESSION['user']) && isset($_SESSION['checkTime']) && !isset($_SESSION['userHash']))  {
	echo' <form action="" method="post">
<input name="form" type="hidden" value="confirm" />
<label for="pass">type your password here: </label>
<input name="pass" class="text" type="password" />
<input name="login" id="submit" type="submit" value="login" />
</form>';
} else {
	echo '<form action="" method="post">
<input name="form" type="hidden" value="login" />
<label for="user">user: </label>
<input name="user" class="text" type="text" />
<label for="pass">password: </label>
<input name="pass" class="text" type="password" />
<input name="login" id="submit" type="submit" value="login" />
</form>';
}

ob_end_flush();

?>

Nel database ci sono due tabelle:

- users

- history

Nella prima ci va l'elenco degli utenti presenti, nella seconda tutti i tentativi di trovare user e password. Per ogni nick registro un massimo di tre tentativi, se questi falliscono il nick viene bloccato per 15 minuti. Ciò significa che in quei 15 minuti nessuno può loggarsi con quel nick specifico. Il controllo non lo faccio soltanto sui nick effettivamente presenti ma su qualsiasi nick venga provato. Questo per creare confusione: l'attaccante non deve avere certezza di aver trovato un nick realmente presente.

Superati i 15 minuti si può ritentare: altri tre errori, altri quindici minuti di stop. In questo modo posso limitare un bruteforce ad un massimo di 12 tentativi per nick all'ora. Non è male, mi darebbe il tempo di rendermi conto dell'attacco.

Potrei aggiungere uno script per mandare una mail di avviso quando un nick viene bloccato, ci devo pensare.

Nello script ci sono tante altre cose, inizialmente avevo pensato di usare i Cookie ma ho preferito evitarli per adesso. Preferisco usare le sessioni anche se ci devo capire ancora molto. Sono certo che ci sia molto da migliorare, ma intanto eccolo qua.

Condividi questo messaggio


Link di questo messaggio
Condividi su altri siti

L'idea è interessante! Mi piace! :)

Condividi questo messaggio


Link di questo messaggio
Condividi su altri siti

Ecco dopo questa...

posso andare a dormire... :P:):P:P non lo capirò mai :sigh: :sigh: :sigh: fajepaura.gif

:P

Condividi questo messaggio


Link di questo messaggio
Condividi su altri siti

Crea un account o accedi per lasciare un commento

Devi essere un utente registrato per partecipare

Crea un account

Iscriviti per un nuovo account nella nostra community. È facile!


Registra un nuovo account

Accedi

Sei già registrato? Accedi qui.


Accedi Ora
Accedi per seguire   
Seguaci 0