Глава 3. Распыление

Истина

Когда речь заходит о распылении (fuzzing), часто полагают что вы применяете его для выявления уязвимостей или сбоев эксплуатации. Естественно, вы можете применят распыление и для этого, я в прошлом я уже находил уязвимости, на вы также можете применять распыление для определения поведения браузера и именно об этом данная глава. Распыление сэкономит вам много времени и поможет очень быстро освоить JavaScript. Часто возникает соблазн заглянуть в спецификацию вашего источника истины о конкретном поведении JavaScript, это неверное мышление, потому как у разных браузеров могут иметься свои собственные особенности, которые не соответствуют спецификации или же они могут быть неверно реализованы. Я не утверждаю что не стоит применять спецификацию, я просто говорю не верьте ей и применяйте распыление чтобы добраться до истины.

Моим первым набегом на распыление поведения стал поиск символов, допустимых в URL протокола JavaScript. Я начал с создания адреса URL JavaScript внутри атрибута привязки href и вручную вставлял логические объекты HTML и наводил курсор на данную ссылку, чтобы убедиться что это всё ещё протокол JavaScript. Про себя я подумал, что должен иметься способ получше. В то время я полагал, что наилучший способ осуществления этого - воспользоваться языком программирования сервера, например, PHP. Поэтому я создал некий инструмент для распыления, который перебирал порциями символы и сообщал о результатах. Это было ещё в 2008 году и было найдено множество занятных результатов:


1 jav&#56325ascript:al&#56325ert(1)// применялось для работы в Firefox 2!
 	   

Это прекрасный пример зачем вам требуется распыление, вам придётся вручную отредактировать 56 000 логических объектов для обнаружения данной ошибки. Это также предполагает, что вы желаете тестировать логические объекты вместо простых символов! Чтобы превратить вещи в намного более простые, вам в вашей жизни необходим фаззинг. В 2008 году компьютеры были намного медленнее чем сейчас, и у меня тоже был дерьмовый медленный ноутбук, поэтому мне приходилось выполнять это по частям, в настоящее время компьютеры намного быстрее, вы буквально можете распылять миллионы символов за несколько секунд.

Распыление URL JavaScript

Мой подход к распылению изменился с появлением современного браузера, теперь я пользуюсь свойствами innerHTML и DOM. Вам придётся применять оба, потому как по причине того, что они следуют разным путям кода существуют различные результаты. Допустим, мы хотим распылить URL JavaScript в современном браузере. Первый способ состоит в применении DOM:


1 log=[];
2 let anchor = document.createElement('a');
3 for(let i=0;i<=0x10ffff;i++){
4     anchor.href = `javascript${String.fromCodePoint(i)}:`;
5     if(anchor.protocol === 'javascript:') {
6     log.push(i);
7     }
8 }
9 console.log(log)//9,10,13,58
 	   

Давайте разберём этот достаточно простой код: сначала мы создаём массив и привязку, а затем перебираем все возможные кодовые пункты unicode (их более 1 000 000), затем назначаем href и вставляем свой кодовый пункт при помощи функции пункта String.fromCode и размещаем полученный символ(ы) после строки javascript. Свойство протокола применяется для проверки того, действительно ли выработанная ссылка является протоколом JavaScript. Удивительно, но браузер завершит операцию за считанные секунды. Если вы настолько же стары, как и я, вы помните, когда такого рода вещи просто как DoS в браузере. Теперь, чтобы распылить другие части href, нам просто нужно переместить заполнитель. Должны ли мы распылять начало строки JavaScript? Измените заполнитель на:


1 anchor.href = `${String.fromCodePoint(i)}javascript:`;
 	   

Когда мы запустим этот код снова мы получим различные результаты:


1 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,3\
2 1,32
 	   

Намного больше символов, причём, обратите внимание на NULL в самом начале (когда он выражается как символ, символьный код 0 равен NULL), это свойственно пути кода DOM. Это не будет работать при применении в обычном HTML. Именно поэтому важно распылять оба стиля: DOM и innerHTML. Первое что надлежит сделать, когда вы получили выполнили операцию распыления и получили занимательные результаты, так это проверить их. Это просто сделать, вы просто вручную восстанавливаете тот DOM, который распыляли. Итак, случайным образом выберите кодовый пункт, выработайте для него код DOM и щёлкните по нему, дабы убедиться что он работает:


1 let anchor = document.createElement('a');
2 anchor.href = `${String.fromCodePoint(12)}javascript:alert(1337)`;
3 anchor.append('Click me')
4 document.body.append(anchor)
 	   

Я выбрал кодовый пункт 12 (form feed), создал URL JavaScript, который вызывает alert, добавил к привязке некий текст и, наконец, добавил его в элемент body. При клике на данную ссылку должно вырабатываться предупреждение и теперь вы убедились что наш распылённый код и в самом деле работает. Попробуйте поэкспериментировать с разными кодовыми пунктами, чтобы убедиться что всё работает как задумано. Парой вопросов, которые следует задать себе являются: "Можете ли вы воспользоваться несколькими символами?" или же "Можете ли вы применять несколько символов в разных позициях?" Ответы на эти вопросы я оставляю вам в качестве упражнения.

При работе с DOM следует помнить, что логические объекты HTML не декодируются при непосредственном изменении свойств, за исключением основанных на HTML свойств. Таким образом, нет смысла применять свойство атрибута href для распыления логических объектов HTML. Для этого вам придётся воспользоваться innerHTML. Давайте испробуем тот же символ в HTML и посмотрим, работает ли он:


1 <a href="javascript:alert(1337)">Test</a><!-- Протокол JavaScript работает -->
 	   

В общем случае, когда вы наблюдаете результаты для DOM, вы можете применять их и с логическими объектами HTML. Как уже упоминалось, было одно исключение, помните NULL в самом начале? Он работает в DOM, но не в HTML:


1 <a href="&#0;javascript:alert(1337)">Test</a><!-- Протокол JavaScript не работает -\
2 ->
 	   

Приведённое выше не срабатывает, а потому важно проверять свои результаты и тестировать их как в DOM, так и в innerHTML или HTML. Здесь вы можете наблюдать, что в данном случае имеется множество возможностей для автоматизации, применение Puppeteer или иной инфраструктуры может быть не плохой мыслью для проверки ваших результатов вместо того, чтобы каждый раз выполнять это вручную.

Распыление URL HTTP

Можно также распылять URL HTTP, но вместо применения протокола вы можете воспользоваться именем хоста чтобы узнать было ли оно успешным. Как и ранее, для перебора кодовых пунктов unicode и вставки полученного символа в href вы создаёте некий цикл for, а затем проверяете, существует ли имя хоста, соответствующее ожидаемому значению.


1 a=document.createElement('a');
2 log=[];
3 for(let i=0;i<=0x10ffff;i++){
4     a.href = `${String.fromCodePoint(i)}https://garethheyes.co.uk`;
5     if(a.hostname === 'garethheyes.co.uk'){
6     log.push(i);
7     }
8 }
9 console.log(log)
10 //0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30\
11 ,31,32
 	   

Приведённый выше код выявляет, что URL HTTP поддерживают в точности те же символы, что и начало URL JavaScript. Ещё раз обратите внимание на то, что символ NULL поддерживается в контексте DOM, но не в контексте HTML. Чтобы отыскать открытые на стороне клиента перенаправления, которые могут оказаться полезными в ошибочных цепочках, вы можете пожелать распылить относительные URL протокола. Когда вы не знаете относительные URL протокола, вы можете ссылаться на внешний URL, применяя удвоенную косую черту. Это будет наследовать значение текущего протокола, например, когда сайт применяет значение протокола HTTP://, относительный URL воспользуется тем же самым протоколом. Давайте распылим внутренность косых черт, чтобы обнаружить какие символы поддерживаются:


1 a=document.createElement('a');
2 log=[];
3 for(let i=0;i<=0x10ffff;i++){
4     a.href = `/${String.fromCodePoint(i)}/garethheyes.co.uk`;
5     if(a.hostname === 'garethheyes.co.uk'){
6         log.push(i);
7     }
8 }
9 input.value=log//9,10,13,47,92
 	   

Как вы можете обнаружить, между обратными косыми чертами вы можете помещать пробельные символы, а также обратную косую черту можно применять точно также как прямую косую черту.

Распыление HTML

Мы наблюдали как распылять URL JavaScript, но мы также можем применять тот же самый подход при распылении HTML. Применяя надлежащий API innerHTML мы можем очень быстро выявить причуды синтаксического анализатора, причём точно также, как мы поступали со свойствами DOM. Прежде чем приступить, неплохо бы спросить себя о цели распыления. Например, я задал себе вопрос: "Какие символы допустимы в закрытиях комментариев HTML?" Для ответа на это вопрос давайте задумаемся как достичь этого, когда комментарий HTML закрыт, следующий тег после комментария обязан выстраиваться! Поэтому мы можем просто убедиться отображается ли такой элемент HTML при помощи API querySelector. Вот как это делается:


1 let log=[];
2 let div = document.createElement('div');
3     for(let i=0;i<=0x10ffff;i++){
4 div.innerHTML=`<!----${String.fromCodePoint(i)}><span></span>-->`;
5 if(div.querySelector('span')){
6 log.push(i);
7 }
8 }
9 console.log(log)//33,45,62
 	   

Это очень похоже на распыление свойств DOM и вы можете отметить, что это занимает гораздо больше времени, потому как браузеру приходится выполнять больше работы. Мы создаём элемент div, снова перебираем все кодовые пункты unicode, но на этот раз для создания комментария HTML применяем innerHTML, причём непосредственно перед закрывающим символом больше чем мы внедряем свой символ unicode, когда комментарий закрыт, следующий span не будет отображаться, а потому querySelector для этого span будет нулевым. Мы желаем знать сработал ли комментарий, поэтому когда обнаружен элемент span, мы регистрируем это результат. Я испробовал это в Chrome и любой из приводимых ниже символов закрывает комментарий после двойного дефиса: !->.

Вы можете испробовать это в прочих браузерах и поэкспериментировать, перемещая заполнитель в разные места закрывающего тега комментария. Я запускал несколько лет назад похожий на этот код в Firefox и обнаружил, что вы можете применять символ новой строки перед символом больше чем! Можно воспользоваться querySelector иначе, вы можете проверить работает ли начальный тег комментария, проверив что выстроен идущий следом span. И в самом деле, я воспользовался именно этим методом чтобы обнаружит ещё один недостаток синтаксического анализа в Firefox, который позволял вам применять NULL между дефисами открывающего тега комментария.


1 let log=[];
2 let div = document.createElement('div');
3 for(let i=0;i<=0x10ffff;i++){
4     div.innerHTML=`<!-${String.fromCodePoint(i)}- ><span></span>-->`;
5     if(!div.querySelector('span')){
6         log.push(i);
7     }
8 }
9 console.log(log)//45
 	   

Это тот же самый код что и ранее, за исключением того, что заполнитель находится внутри открывающего тега комментария, а querySelector проверяет существует ли span. Для символа дефиса вы должны получить лишь один результат 45, если вы наблюдаете больше, у вас ошибка браузера. Один момент, который не следует забывать при подобном распылении состоит в том, что вырабатываемая вами разметка не создаёт ложных срабатываний, например, вы можете обнаружить весьма намеренный символ больше чем после пробела и дефиса. Именно это предотвращает потребление span в качестве иного тега различными символами распыления. Это хорошо для подобного этому защитного кода, поскольку сбережёт вам время.

Распыление известного поведения

Если вы не знаете с чего приступать к распылению для нового поведения браузера, вы всегда можете провести распыление для уже имеющегося поведения и посмотреть имеются ли отклонения. Одним из достойных мест для начала является пробельный символ, вам просто необходимо построить нечёткую строку, которая будет появляться лишь в том случае, когда такой символ является пробельным. Лучший найденный мной способ для этого состоит в том чтобы воспользоваться вызовом функции. Вы можете обладать пробельным символом между именем идентификатора функции и круглыми скобками, поэтому вы можете определить соответствующую функцию внутри блока try catch и попытаться вызвать её при помощи указанного идентификатора с добавленным символом посредством eval.


1 function x(){}
2 3
log=[];
4 for(let i=0;i<=0x10ffff;i++){
5     try {
6         eval(`x${String.fromCodePoint(i)}()`)
7         log.push(i)
8     }catch(e){}
9 }
10
11 console.log(log)
12 //9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8\
13 232,8233,8239,8287,12288,65279
 	   

Это удивительное количество символов, хотя оно всего лишь доказывает, что между идентификатором и круглыми скобками можно применять пробельные символы, для выполнения выразительного распыления вам потребуются различные позиции для такого символа и дополнительные контексты. Я оставляю вам проделать это и протестировать различные браузеры. Если вы обнаружите неожиданное поведение, вы можете сообщить об этом. Для чего вы бы могли воспользоваться подобными отклонениями? Что ж, в прошлом я применял их для выхода из песочницы JavaScript, поскольку, раз вы можете обманывать синтаксический анализатор, заставляя его неверно выполнять лексический анализ, интерпретируя такой символ как не пробельный, хотя на самом деле он таковой, вы можете заставлять свой JavaScript выполнять синтаксический анализ в песочнице одним образом и иным образом в браузере или NodJS.

Вы также можете применять пары символов для распыления таких вещей как строки, причём, как правило, скорость распыления повторяющихся символов будет примерно той же, а вот когда мы пользуемся вложенным распылением, всё становится гораздо более медленным. Быть может, нам придётся подождать несколько лет пока это не станет практичным. Распыление для строк достаточно простое; вам просто требуется воспользоваться парой символов и убедиться, что поведение продолжает походить на повеление строки. Один из способов выполнения этого состоит в применении блока try catch и eval и проверить не вызовет ли пакет случайных символов исключительной ситуации при использовании данной пары символов. Это будет означать что имеется подобное строковому поведение.


1 log=[];
2 for(let i=0;i<=0x10ffff;i++){
3     try {
4         eval(`${String.fromCodePoint(i)}%$£234${String.fromCodePoint(i)}`)
5         log.push(i)
6     }catch(e){}
7 }
8 console.log(log)//34,39,47,96
 	   

Вы ожидали три символа? Могли ли вы подумать, что какие- то ещё символы будут обладать поведением строк? Естественно, на пару с передачей прямых косых черт для включения ваших символов, вы можете применять регулярные выражения, причём это не взывает исключительных ситуаций, потому как эти символы будут рассматриваться как регулярное выражение. Chrom достаточно быстро справляется с данной операцией, но на момент проверки Firefox работал очень медленно.

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


1 log=[];
2 for(let i=0;i<=0x10ffff;i++){
3     try {
4         eval(`${String.fromCodePoint(i,i)}%$£234$`)
5         log.push(i)
6     }catch(e){}
7 }
8 console.log(log)//47
 	   

Как и ожидалось, мы обнаружили прямую косую черту. Вы можете наблюдать, что для поиска интересного поведения имеется реальная практика в создании вручную векторов распушения. Вам необходимо задуматься как будет исполняться такой нечёткий вектор и в каком именно контексте во избежания ложных срабатываний.

Наконец, в данной главе мы рассмотрим вложенное распыление для поиска интересного повеления комментариев в JavaScript о котором вы, скорее всего, не знаете. Вложенное распыление сложно, поскольку он содержит гораздо больше символов, а в настоящее время браузеры не столь мощны чтобы исполнять множество циклов for с кодовыми пунктами больше 0xff. Тем не менее, если мы уменьшим число проверяемых кодовых пунктов, всё равно персонал имеет возможность отыскать нечто занятное.


1 log=[];
2 for(let i=0;i<=0xff;i++){
3     for(let j=0;j<=0xfff;j++){
4        try {
5            eval(`${String.fromCodePoint(i,j)}%$£234$`)
6            log.push([i,j])
7        }catch(e){}
8     }
9 }
10 console.log(log)//[35,33],[47,47]
 	   

На этот раз мы применяем два вложенных цикла for, причём, как уже обсуждалось, нам пришлось ограничить общее число символов по причинам производительности. Затем я воспользовался методом fromCodePoint для добавления двух символов по одному из кодовых пунктов в каждом цикле и добавил некий мусор после этих символов чтобы подтвердить что происходит в комментариях. Мы добавляем массив из двух символов, поскольку они оба необходимы для создания комментария. Мы получили ожидаемую двойную косую черту, но что это? 35 и 33 , которые составляют "#!" действуют как комментарий в одну строку при условии, что они находятся в начале исполняемого кода JavaScript. Вы можете проверить свои результаты, попробовав исполнить alert с данным комментарием.


1 #!
2 alert(1337)//исполняет alert с 1337
 	   

Если вы добавляете любой символ перед символом решётки, это приводит к отказу:


1 123
2 #!
3 alert(0)//alert не будет вызываться.
 	   

В приводимом выше примере если перед символом решётки имеются какие бы то ни было символы, комментарий не будет работать. Скорее всего, этот магический куки (shebang), был добавлен в качестве последовательности для комментариев потому как JavaScript применялся в качестве сценариев оболочки с NextJS, а потому распространённым вариантом его применения служило игнорирование.

Распыление экранированной последовательности

В своих усилиях по слому песочницы JavaScript я решился на распыление различных экранированных последовательностей, например, экранированных последовательностей unicode, которые обсуждались в главе введения. Это привело меня к занятному поведению в различных браузерах. Я обнаружил, что в Safari не выставляется исключительная ситуация при обнаружении не завершённых экранированных последовательностей unicode в строках. Старая Opera (до Chromium) также неверно проводила анализ экранированных последовательностей unicode при отправке в eval. Такой вид поведения можно применять для исхода из песочницы, если вам повезёт, но как нам его отыскать? Естественно, распыление! Вам просто надлежит знать тот контекст, в котором вы трудитесь с распылением и требуют ли они двойного кодирования? Давайте распылим простую экранированную последовательность unicode:


1 let a = 123;
2 log=[];
3 for(let i=0;i<=0x10ffff;i++){
4     try{
5         eval(`\\u{${String.fromCodePoint(i)}0061}`);
6         log.push(i);
7     }catch(e){}
8 }
9 console.log(log)//48
 	   

В приведённом выше коде я применяю для всех символов unicode распыление как и ранее в этой главе. На этот раз я определил переменную "a", которая применяется для проверки того прокидывается ли eval или нет. eval возбудит исключительную ситуацию когда наш код попытается выполнить обращение к неопределённой переменной. В данном случае мы желаем знать получен ли успешно доступ к переменной "a" или нет. Мы пользуемся форматом экранирования \u{} и вам придётся экранировать обратную косую черту, потому как мы хотим чтобы eval выполнял интерпретацию этой экранированной последовательности unicode. В заполнитель мы добавляем свой символ распыления. Распылив все символы, мы получаем единственный результат "48", который выступает кодовым символом нуля. Тем самым, при помощи чистого распыления мы определили, что данный формат экранированной последовательности unicode позволяет заполнять её целиком нулями.

Вы можете применять распыление не только для символов. У вас имеется возможность распылять шестнадцатеричное представление, внося изменения в соответствующий заполнитель. Всякий числовой литерал обладает методом toString, который позволяет определять значение системы исчисления и который преобразует целое число в определяемое по основанию от 2 до 36. Шестнадцатеричное представление применяет основание 16, поэтому чтобы преобразовывать его шестнадцатеричный вид, нам следует передавать систему счисления 16:


1 let a = 123;
2 log=[];
3 for(let i=0;i<=0x10ffff;i++){
4     try {
5         eval(`\\u{${i.toString(16)}}`);
6         log.push(i);
7     } catch(e){}
8 }
9 console.log(log)//97,105
 	   

Приводимый выше код перебирает все кодовые пункты и преобразует их значение в шестнадцатеричное для определения того, приводит ли в результате эта экранированная последовательность unicode к ссылке на переменную. У нас имеются два результата: коды символов 97 и 105, которые выступают определёнными для нас символами "a" и "i". Вы можете проводить перемешивание и сопоставлять нечёткие строки с символами и шестнадцатеричными значениями чтобы обнаруживать допускает ли механизм JavaScript конкретные символы до, внутри или после такого шестнадцатеричного значения. Давайте испробуем это прямо сейчас и посмотрим, допускает ли механизм JavaScript какой- нибудь символ внутри шестнадцатеричного значения:


1 let a = 123;
2 log=[];
3 for(let i=0;i<=0x10ffff;i++){
4     try{
5         eval(`\\u{${String.fromCodePoint(i)}61}`);
6         log.push(i);
7     }catch(e){}
8 }
9 console.log(log)//48
 	   

В приведённом выше коде для добавления символа перед шестнадцатеричным значением соответствующей экранированной последовательности unicode применяется заполнитель. В Chrome мы получаем единственный результат, который являет собой код символа 48, что представляет собой ожидаемое число ноль, но стоит испробовать это и в других браузерах, чтобы увидеть получите ли вы те же самые результаты.

Выводы

Надеюсь, в данной главе я обучил вас новому методу создания распылённых (нечётких) векторов! Мы обсудили что не следует доверять спецификациям и отыскивать собственные пути узнавать как работает JavaScript. Я показал вам как применять свойства DOM для распыления URL JavaScript и HTTP. Мы рассмотрели как вы можете применять innerHTML чтобы разобраться с тем как выполняется анализ HTML. Наконец, мы покончили с распылением известного поведения для поиска отклонений и выявили относительно неизвестный комментарий в одну строку JavaScript. За этим последовало изложение того как распылять экранированные последовательности unicode применяя шестнадцатеричное представление и вставляя символы в вырабатываемую шестнадцатеричную строку для определения того, допускает ли механизм JavaScript пробельные или иные символы внутри экранированной последовательности unicode.