Ujawnienie danych sesji
Na wspólnym hoście zabezpieczenia nigdy nie są tak szczelne, jak na hoście dedykowanym. Jest to kompromis, który trzeba zaakceptować w zamian za niewygórowaną cenę.
Jednym z największych niebezpieczeństw, jakie wiążą się z używaniem wspólnych hostów jest wspólny magazyn sesji. Domyślnie PHP przechowuje dane sesji w lokalizacji /tmp
i dotyczy to każdego. Ludzie natomiast mają tendencję do pozostawania przy domyślnych ustawieniach we wszystkim, także jeśli chodzi o sesje. Na szczęście nie każdy może odczytywać pliki sesji, ponieważ dostęp do nich ma tylko serwer:
$ ls /tmp total 12 -rw------- 1 nobody nobody 123 May 21 12:34 sess_dc8417803c0f12c5b2e39477dc371462 -rw------- 1 nobody nobody 123 May 21 12:34 sess_46c83b9ae5e506b8ceb6c37dc9a3f66e -rw------- 1 nobody nobody 123 May 21 12:34 sess_9c57839c6c7a6ebd1cb45f7569d1ccfc $
Niestety napisanie skryptu odczytującego te dane jest banalnie proste, a ponieważ skrypt taki działa z uprawnieniami użytkownika nobody
(lub o jakiejś innej nazwie), ma wszystkie potrzebne mu do tego uprawnienia.
Tę i kilka innych podobnych luk można załatać za pomocą dyrektywy safe_mode
, ale ponieważ dyrektywa ta ma zastosowanie tylko do PHP, nie stanowi to rozwiązania przyczyny problemu. Atakujący może po prostu użyć innego języka programowania.
Lepsze rozwiązanie
Jakie rozwiązanie jest lepsze? Nie używać tego samego magazynu sesji, co wszyscy inni. Najlepiej dane te przechowywać w bazie danych, dostęp do której jest możliwy tylko po podaniu informacji uwierzytelniających. W tym celu należy użyć funkcji session_set_save_handler()
, która pozwala zastąpić domyślny mechanizm obsługi sesji własnymi funkcjami.
Na poniższym listingu znajduje się uproszczony przykładowy skrypt zapisujący dane sesji w bazie danych:
<?php
session_set_save_handler('_open',
'_close',
'_read',
'_write',
'_destroy',
'_clean');
function _open()
{
global $_sess_db;
$db_user = $_SERVER['DB_USER'];
$db_pass = $_SERVER['DB_PASS'];
$db_host = 'localhost';
if ($_sess_db = mysql_connect($db_host, $db_user, $db_pass))
{
return mysql_select_db('sessions', $_sess_db);
}
return FALSE;
}
function _close()
{
global $_sess_db;
return mysql_close($_sess_db);
}
function _read($id)
{
global $_sess_db;
$id = mysql_real_escape_string($id);
$sql = "SELECT data
FROM sessions
WHERE id = '$id'";
if ($result = mysql_query($sql, $_sess_db))
{
if (mysql_num_rows($result))
{
$record = mysql_fetch_assoc($result);
return $record['data'];
}
}
return '';
}
function _write($id, $data)
{
global $_sess_db;
$access = time();
$id = mysql_real_escape_string($id);
$access = mysql_real_escape_string($access);
$data = mysql_real_escape_string($data);
$sql = "REPLACE
INTO sessions
VALUES ('$id', '$access', '$data')";
return mysql_query($sql, $_sess_db);
}
function _destroy($id)
{
global $_sess_db;
$id = mysql_real_escape_string($id);
$sql = "DELETE
FROM sessions
WHERE id = '$id'";
return mysql_query($sql, $_sess_db);
}
function _clean($max)
{
global $_sess_db;
$old = time() - $max;
$old = mysql_real_escape_string($old);
$sql = "DELETE
FROM sessions
WHERE access < '$old'";
return mysql_query($sql, $_sess_db);
}
?>
Skrypt ten do działania potrzebuje bazy danych o nazwie sessions
o następującym schemacie:
mysql> DESCRIBE sessions;
+--------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+------------------+------+-----+---------+-------+
| id | varchar(32) | | PRI | | |
| access | int(10) unsigned | YES | | NULL | |
| data | text | YES | | NULL | |
+--------+------------------+------+-----+---------+-------+
W MySQL bazę tę można utworzyć przy użyciu następującego polecenia:
CREATE TABLE sessions
(
id varchar(32) NOT NULL,
access int(10) unsigned,
data text,
PRIMARY KEY (id)
);
Zapisując dane sesji w bazie danych sprawiasz, że o ich bezpieczeństwie decydują zabezpieczenia tej bazy. Przypomnij sobie informacje o bazach danych z części o bazach danych i języku SQL, ponieważ mają one tu zastosowanie.
Przeglądanie systemu plików
Dla zabawy poniżej przedstawiony jest skrypt przeglądający system plików:
<?php
echo "<pre>n";
if (ini_get('safe_mode'))
{
echo "[Tryb safe_mode włączony]nn";
}
else
{
echo "[Tryb safe_mode wyłączony]nn";
}
if (isset($_GET['dir']))
{
ls($_GET['dir']);
}
elseif (isset($_GET['file']))
{
cat($_GET['file']);
}
else
{
ls('/');
}
echo "</pre>n";
function ls($dir)
{
$handle = dir($dir);
while ($filename = $handle->read())
{
$size = filesize("$dir$filename");
if (is_dir("$dir$filename"))
{
if (is_readable("$dir$filename"))
{
$line = str_pad($size, 15);
$line .= "<a href="{$_SERVER['PHP_SE LF']}?dir=$dir$filename/">$filename/</a>";
}
else
{
$line = str_pad($size, 15);
$line .= "$filename/";
}
}
else
{
if (is_readable("$dir$filename"))
{
$line = str_pad($size, 15);
$line .= "<a href="{$_SERVER['PHP_SELF']}?file=$dir$filename">$filename</a>";
}
else
{
$line = str_pad($size, 15);
$line .= $filename;
}
}
echo "$linen";
}
$handle->close();
}
function cat($file)
{
ob_start();
readfile($file);
$contents = ob_get_contents();
ob_clean();
echo htmlentities($contents);
return true;
}
?>
Dyrektywa safe_mode
może uniemożliwić działanie tego skryptu, ale co jeśli podobny skrypt zostanie napisany w innym języku?
Dobrym rozwiązaniem jest zapisywanie poufnych informacji w bazie danych i chronienie danych uwierzytelniających do niej za pomocą technik, które zostały opisane wcześniej (gdzie dane te są zapisywane w zmiennych $_SERVER['DB_USER']
i $_SERVER['DB_PASS']
).
Najlepszym rozwiązaniem jest korzystanie z dedykowanego hosta.