FunPHP#7

Поговорим про PHP7

Относительно недавно писал про Паблика Морозова. Появились новые мысли и захотелось немного добавить. Вдохновение почерпнул из доклада Alexander Lisachenko который был недавно на PHPRussia.

У Александра был доклад про магию (я очень порадовался что в мире есть единомышленники) и если вы интересуетесь магией, то вам явно стоит увидеть этот доклад (теперь уже в записи). Александр там упомянул stream filetrs, которые есть в PHP начиная с 5й версии (если не ошибаюсь). Когда впервые я увидел эти фильтры я не понял для чего их можно применить на практике, чтобы прям была польза. Ведь по сути это возможность парсить подключаемый файл перед парсингом интерпретатора, что можно сделать и без фильтров (что я и делал еще во времена PHP4 (когда-то был у меня самописный фреймворк со своим синтаксическим сахаром aka DSL)).

Были разные идеи применения для обфускации, например(были времена работы в вебстудии когда делиться кодом было не принято). Но вот чтобы применить эти фильтры для того, чтобы Паблик Морозов мог взломать защищенные методы — эта идея пришла только сейчас, с подачи Александра.

И так, суть, в PHP есть возможность регистрировать свои фильтры, которые потом можно применить при инклуде. Допустим мы пишем такой фильтр:

<?php
class UnlockFilter extends PHP_User_Filter {
private $_data;
function onCreate() {
$this->_data = '';
return true;
}
public function filter($in, $out, &$consumed, $closing) {
while($bucket = stream_bucket_make_writeable($in)) {
$this->_data .= $bucket->data;
$this->bucket = $bucket;
$consumed = 0;
}
if($closing) {
$consumed += strlen($this->_data);
$str = preg_replace(
'~private|protected~', 'public', $this->_data
);
$this->bucket->data = $str;
$this->bucket->datalen = strlen($this->_data);
if(!empty($this->bucket->data))
stream_bucket_append($out, $this->bucket)
;
return PSFS_PASS_ON;
}
return PSFS_FEED_ME;
}
}

Далее мы его регистрируем:

stream_filter_register('unlock', 'UnlockFilter');

И вот теперь мы можем подключить файл, в котором есть класс полностью защищенный (а нам по каким-то причинам ну очень надо все раскрыть):

include 'php://filter/read=unlock/resource=foo.php';

Что делает фильтр? По сути это просто тупо автозамена ключевых слов, такой парсинг перед парсингом, после чего код выполняется. Вы не модифицируете оригинальный файл, но модифицируете загружаемый код на лету. Следующи шаг взлома — это просто ручками залезть и поменять все приваты и протектед :)

Чем плох такой подход в лоб? Тем, что мы не разбираемся что заменяем и меняем все подряд. Более умный подход — это реализовать фильтр с разбором исходного кода. Для этого можно использовать как встроенный в PHP tokenizer, так и более продвинутые библиотеки наподобие PHP AST.

Кстати, частенько спрашивают, а зачем знать магию? Где это применять? Теперь как пример могу смело приводить фреймворк Go!AOP (Аспектно-Ориентированный Фреймворк) все от того же Alexander Lisachenko, который работает с применением магии. Кстати, по такому же принципу работают unfinal или AspectMock.

Вообще спорный вопрос, что есть магия, тем более если это описано в документации и это документированные возможности языка. Почему-то считается, что если есть что-то в языке, что мало кому известно (или мало применяется) — это сразу магия. Может просто почаще перечитывать документацию? :)

Если развивать идею применения фильтров, то можно создавать свои варианты шаблонизаторов, различные DSL или вовсе выдумать свой диалект PHP. Глядя на фронтенд сообщество с его бабелями и обилием диалектов, начинаешь думать, а почему бы и нет? Возможно вернемся к этим фантазиям в другой статье, где я покажу разные идеи применения фильтров для написания собственного DSL. Кстати, Александр предложил написать статью и разобрать его фреймворк GoAOP. По его словам мало людей в мире могут это сделать 🙃. Если вам, мои читатели, это интересно — напишите в комментариях, пожалуйста, нужно ли разобрать или в сети достаточно информации?

Продолжим… Если вы помните, то Паблик Морозов всячески показывал как получить доступ к приватным и протекдем свойствам и методам. Мы там пропустили примеры закрытых классов — когда есть приватный конструктор.

Допустим есть какой-то синглтон:

<?php declare(strict_types=1);
class Singleton {
private static $instance;
public $foo = 0;

private function __construct() {
$this->foo = 123;
}

public static function instance(): self {
if (!self::$instance)
self::$instance = new self;
return self::$instance;
}
};

Нужна возможность получить больше 1го инстанса, что делать?

Самый простой случай это просто склонировать созданный объект и далее его модифицировать:

$foo1 = Singleton::instance();
$foo2 = clone $foo1;

По этой причине закрывают клонирование через добавление:

private function __clone() {}

Вот теперь уже просто склонировать не получится. Но, вспоминая примеры Паблика Морозова, мы можем обойти этот запрет через:

$foo1 = Singleton::instance();
$foo2 = (function() { return clone $this; })->bindTo($foo1, Singleton::class)();

$foo1->foo = 456;
var_dump($foo1); // 456
var_dump($foo2); // 123

А что если это не Singleton и нет никакого метода instance?

class Closed {
private $foo = 0;
private function __construct() { $this->foo = 123; }
private function __clone() {}
public function getFoo() { return $this->foo; }
}

Вообще полностью закрытый класс. Мы можем заглянуть в документацию и посмотреть что нам предлагает ReflectionAPI, а предлагает он нам создать класс в обход конструктора:

$foo = (new ReflectionClass(Closed::class))
-> newInstanceWithoutConstructor()
;
var_dump($foo->getFoo()); // = int(0)

Но есть минус и заключается он в том, что не будет выполнена логика, зашитая в конструктор. Мы его можем вызвать опять же через функцию извне, привязав ее к контексту:

(function() { return $this->__construct(); })->bindTo($foo, Closed::class)();
var_dump($foo->getFoo()); // = int(123)

Но если мы так заморочились, не проще ли сразу получить нужный инстанс через внешнюю функцию через привязку к контексту?

$foo = (function() { return new static; })
->bindTo(null, Closed::class)()
;

Ну вот и все. И конструктор сработал, и инстанс получили.

В конце статьи про Паблика Морозова был показан хак, как создавать stdClass с приватными свойствами. И в комментариях там писали что можно использовать для взлома десериализатор. Суть: мы можем сериализовать класс и посмотреть как он выглядит, если бы у него все методы были паблик:

<?php
class Closed { function __construct() {} }

var_dump( serialize(new Closed()) );

Получается строка:

string(17) "O:6:"Closed":0:{}"

Т.е. мы можем взять и составить такую же строку и провернуть все наоборот:

class Closed { private function __construct() {} }
$foo = unserialize(
sprintf(
'O:%d:"%s":0:{}',
strlen(Closed::class), Closed::class
)
)
;

И мы получим инстанс класса, от которого как бы нельзя было инстанцироваться. Эта же техника применима и к методам и свойствам.

Другие статьи по теме


Лайк, хлопок, шер. Подписывайтесь на Телеграм канал. Следить за обновлениями и прочими материалами от меня можно именно там: @prowebit . В этом канале публикую различные новости и мысли, которых может не быть в этом блоге. Подписывайтесь!

𝔾𝕖𝕖𝕜 🄹🄾🄱 — анонимный поиск работы без палева где можно найти новую работу без проблем на текущем месте. Только для IT, никакого “левого” стафа. Только релевантные предложения. Скоро будет мега апдейт ;)

New.HR — место где помогают найти работу мечты. Работаем только с отборными вакансиями в сфере IT & Digital. Помогаем кандидатам найти работу по душе. Работаем с кандидатами, которые не ищут работу!