В предыдущей статье, мы рассмотрели SSTI, а теперь пришла очередь Client Side Template Injection (CSTI). Главным героем этой статьи станет AngularJS.

Введение или Отличия CSTI от SSTI

Уязвимость Template Injection  возникает, когда приложение, используя какой-нибудь шаблонизатор, динамически внедряет пользовательский ввод в веб-страницу. Когда страница отображается, фреймворк ищёт в странице шаблонное выражение и выполняет его.

Основное отличие CSTI от SSTI заключается в том, что при CSTI мы можем добиться лишь выполнения произвольного JavaScript кода, то есть XSS.

Критичность CSTI уже будет зависеть от самого приложения, данных и функционала, которые содержит данное приложение и т.д. Если приложение используется только для того, чтобы отображать определенный контент без аутентификации или контроля доступа, тогда CSTI не принесёт особого вреда для приложения. Но если используются, например, куки от другого, более важного с точки зрения безопасности, домена, то CSTI может быть использована для атаки на это приложение, что приводит нас уже к очень высокому риску.

Кстати, браузеры не способны засечь и предотвратить CSTI.

ВНИМАНИЕ: сразу хочу предупредить, что в этой статье я решил не изобретать велосипед, а использовать уже существующий. Поэтому данная статья будет базироваться на исследованиях автора канала LiveOverflow.

AngularJS и его проблемы

AngularJS —  это JavaScript-фреймворк с открытым исходным кодом, который предназначен для разработки одностраничных приложений.
Обнаружить использование AngularJS легко: в исходном коде веб-страницы, которую вы просматриваете, будет такая строка, как ‘ng-app‘ (в основном, либо в качестве атрибута элемента html, либо в качестве атрибута элемента body), например:

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-12-11-11-16

Шаблоны Angular могут содержать выражения — JavaScript-подобный код, размещенный внутри {{ваш_код_здесь}}. Аналогично SSTI, если мы на вход приложению дадим {{1+1}}, то на выходе получим 2. Выражения AngularJS не зловредны сами по себе и не могут нанести значительного вреда, но если мы обьеденим их с побегом из песочницы, то мы можем добиться выполнения стороннего JavaScript кода и нанести вред.

Давайте рассмотрим пример.

Этот код динамически внедряет пользовательский ввод, но, поскольку используется htmlspecialchars, XSS не возникает.

А теперь самое интересное: данный код использует AngularJS и тоже использует функцию htmlspecialchars, но если мы внедрим шаблонное выражение (и совместим с побегом из песочницы), то получим XSS. Да-да, htmlspecialchars нас здесь не защитит (вообще, в документации говорится, что не следует использовать выражения Angular в php коде, если это касается пользовательского ввода… но кто же читает эти документации…).Заметьте, что нам нужно, чтобы ‘ng-app‘ находился выше нашего выражения в DOM.

Побег из песочницы в AngularJS 1.0.8

Представим, что у нас есть очень простая веб-страница, которая получает на вход Angular выражение и возвращает его уже в обработаном виде.

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-13-12-54-31

Теперь представим, что главным обьектом у нас является scope. Все, что мы будем писать в форме, будет признано как свойство обьекта scope. То есть, если мы вставим в форму {{username}}, то где-то внутри кода обращение будет происходить к scope.username.  В AngularJS (версии 1.0.8) присутствует функция getterFn(), которая проверяет является ли заданое выражение свойством обьекта. Представим, что у нас есть обьект a со свойствами b и c:

a = {b:1, c:2}

К этим свойствам можно обратиться двумя способами: a.b вернёт 1 и a[«b»] вёрнёт 1. Обьект a вызывает функцию toString():

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-13-13-29-33

Вот и получается, что обьект a имеет ещё и свойство toString:

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-13-13-32-15

Именно поэтому {{alert(1)}} не сработает, так как функция alert() не является свойством обьекта scope. Так как свойства scope[«alert»](1) не существует, то вызывается функция noop(), которая ничего не делает:

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-13-13-12-04

Так как мы можем выйти за предели этой песочницы? Обьект scope, как и все JavaScript обьекты, имеет свойство constructor. То есть scope.constructor — это конструктор обьекта, но scope.constructor.constructor — это уже конструктор функции:

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-13-13-44-23

Function(«alert(1)») создаст функцию alert(1), а вот Function(«alert(1)»)() уже вызовет нашу функцию alert(1). Вот и всё. В качестве Function() у нас constructor.constructor(), а чтобы вызвать alert(1), нам нужно ввести следущее выражение: {{constructor.constructor(«alert(1)»)()}}

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-13-13-54-31

Побег из песочницы в AngularJS 1.4.7

В версии 1.4.8, как вы догадались, многое изменилось в сравнении с версией 1.0.8.

Вот наше уязвимое веб-приложение.

Предыдущая полезная нагрузка не сработает. Давайте я сразу покажу вам, что сработает, а потом обьясню почему. Вот наша полезная нагрузка AngularJS 1.4.7: {{‘a’.constructor.prototype.charAt=’a’.concat; $eval(‘exploit=1 } } };alert(1)//’);}}

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-14-07-49-25

Давайте кратко ознакомимся с одной функцией. Функция ensureSafeObject() — выполняет проверку, является ли обьект конструктором функции, окном или конструктором обьекта. Если любая из этих проверок вернёт TRUE, то возбудится исключение и обработка выражения остановится.

Именно функция ensureSafeObject() не даёт нам воспользоваться конструктором. так как использование конструкторов объектов и конструкторов функций запрещено.

Для демонстрации побега из песочницы, вам нужно будет узнать ещё одну вещь. Когда мы даём на вход приложению выражение, например, {{1+1}}, то в итоге его обрабатывает функция $eval(), то есть $eval(«1+1»).

Условно нашу полезную нагрузку можно разделить на две части: ‘a’.constructor.prototype.charAt=’a’.concat; и $eval(‘exploit=1 } } };alert(1)//’);. Теперь давайте рассмотрим каждую часть подробней. ‘a’.constructor — это конструктор строки:

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-14-07-52-27

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-14-07-58-05

Предыдущий скрин аналогичен записи String(«b») (т.к. это конструктор строк). С помощью выражения ‘a’.constructor.prototype.charAt, мы сссылаемся на функцию charAt(), которую поддерживают все обьекты String. Давайте посмотрим на функцию concat():

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-14-08-19-16

Думаю, здесь всё понятно. Но самое интересное, что ‘a’.constructor.prototype.charAt=’b’.concat  полностью разрушает логикку функции charAt и функция charAt() становится функцией concat():

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-14-08-27-19

Тперь давайте посмотрим, как будет обрабатываться наше выражение внутри программы. Сначало наше выражение попадает к функции lex(). Lex() — это функция, которая выполняет лексический анализ нашего выражения. Что такое лексический анализ? Это когда вы даёте на вход функции последовательность символ, а она формирует на их основе токены.Вот вам очень краткий пример: мы дали на вход функции строку new_var = assets и она вернёт нам такую последовательность токенов, как ИМЯ «new_var», ПРИСВАИВАНИЕ, ИМЯ «assets». Лексический анализ — это первый шаг, который выполняет компилятор. Да-да, внутри AngularJS находится JavaScript компилято :) Вот мы передали наше выражение функции lex() и на выходе она вернула нам массив токенов, которые являются обьектами. И вот, что получается:

  • 1-ый элемент массива — «a» (не кавычка, так как кавычки игнорируются)
  • 2-ой элемент — «.«
  • 3-ий элемнт — «constructor» и т.д.

Теперь мы пропустим значительную часть этого процеса и вёрнемся к тому, что на выходе даёт компилятор.  А на выходе он даёт функцию, которая проверяет с помощью функции ensureSafeObject(), является ли объект безопасным конструктором функции, и это так, т.к. конструктор String() вполне безопасен. Затем сслыка на prototype помещается в переменную, а далее выполяется проверка на то, имеет ли prototype свойство charAt (конечно же). И вот здесь компилятор допускает ошибку, так как следующим его шагом является проверка строки ‘b’.concat на то, является ли она безопасной. И..да, она является безопасной, но только эта строка полностью переписывает функцию charAt! Далее обрабатывается выражение, которое начинается с функции $eval(). Наше выражение передаётся функции lex(), разбивается на токены. Но теперь каждый символ запрашивается через функцию charAt(), а charAt() теперь у нас concat():

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-14-09-20-56

В итоге при дальнейшей проверке, вот что произойдёт с переменной v5:

%d1%81%d0%bd%d0%b8%d0%bc%d0%be%d0%ba-%d1%8d%d0%ba%d1%80%d0%b0%d0%bd%d0%b0-%d0%be%d1%82-2016-11-14-09-26-04

Вот отчёт на HackerOne :)

Выводы

Уязвимость CSTI не так опасна, как SSTI, но тоже может нести печальные последствия. Так что всем советую читать в документациях раздел Security Consideration, если такой есть.