תקיפת תהליכים מרובי שלבים - Multi-Step Process Attacks¶
מהן חולשות בתהליכים מרובי שלבים¶
תהליכים מרובי שלבים (Multi-Step Processes) הם תהליכים שבהם המשתמש עובר דרך מספר שלבים לפני השלמת פעולה - הרשמה, רכישה, שינוי סיסמה, אישור עסקה, ועוד. חולשות בתהליכים אלו נוצרות כאשר השרת לא מוודא שהמשתמש עבר את כל השלבים בסדר הנכון, או כאשר הנתונים שעוברים בין השלבים ניתנים למניפולציה.
דילוג על שלבים - Step Skipping¶
הבעיה הבסיסית¶
תהליך רכישה תקין:
שלב 1: POST /checkout/cart -> סקירת עגלה
שלב 2: POST /checkout/shipping -> הזנת כתובת משלוח
שלב 3: POST /checkout/payment -> הזנת פרטי תשלום
שלב 4: POST /checkout/confirm -> אישור סופי
תקיפה - דילוג ישירות לאישור:
קוד פגיע¶
// שלב 1 - סקירת עגלה
app.post('/checkout/cart', (req, res) => {
req.session.cart = getCart(req.session.userId);
res.json({ items: req.session.cart.items, total: req.session.cart.total });
});
// שלב 2 - כתובת משלוח
app.post('/checkout/shipping', (req, res) => {
req.session.shippingAddress = req.body.address;
res.json({ shippingCost: calculateShipping(req.body.address) });
});
// שלב 3 - תשלום
app.post('/checkout/payment', (req, res) => {
const result = processPayment(req.body.cardDetails, req.session.cart.total);
req.session.paymentConfirmed = result.success;
res.json({ paymentId: result.id });
});
// שלב 4 - אישור (פגיע!)
app.post('/checkout/confirm', (req, res) => {
// פגיע - לא בודק שהתשלום בוצע
const order = createOrder({
userId: req.session.userId,
items: req.session.cart.items,
address: req.session.shippingAddress
});
clearCart(req.session.userId);
res.json({ orderId: order.id, message: 'Order confirmed!' });
});
התוקף שולח POST /checkout/confirm ישירות אחרי שלב 1, בלי לעבור דרך שלב התשלום.
מניפולציית פרמטרים בין שלבים¶
שינוי פרמטרים שעוברים בין שלבים¶
שלב 1: סקירה -> השרת מחזיר סיכום עם total=1337
שלב 2: תשלום -> הלקוח שולח total=1337 חזרה
שלב 3: אישור -> השרת מאשר על סמך ה-total שנשלח
תקיפה:
-- שלב 2 המקורי
POST /checkout/payment HTTP/1.1
Content-Type: application/x-www-form-urlencoded
total=1337&cardNumber=4111111111111111&expiry=12/28
-- שלב 2 שונה
POST /checkout/payment HTTP/1.1
Content-Type: application/x-www-form-urlencoded
total=1&cardNumber=4111111111111111&expiry=12/28
קוד פגיע:
@app.route('/checkout/payment', methods=['POST'])
def process_payment():
# פגיע - סומך על הסכום מהלקוח
total = float(request.form['total'])
card = request.form['cardNumber']
# מחייב את הכרטיס בסכום שנשלח מהלקוח
result = charge_card(card, total)
if result['success']:
session['payment_amount'] = total # שומר את הסכום השגוי
return redirect('/checkout/confirm')
תקיפת שידור חוזר - Replay Attacks¶
שימוש חוזר בתגובת שלב מוצלח¶
תהליך תקין:
1. POST /transfer -> בקשת העברה
2. קבלת טוקן אישור: token=abc123
3. POST /transfer/confirm?token=abc123 -> אישור ההעברה
תקיפה - שימוש חוזר בטוקן:
3. POST /transfer/confirm?token=abc123 -> העברה ראשונה (מוצלחת)
4. POST /transfer/confirm?token=abc123 -> העברה שנייה (שימוש חוזר!)
5. POST /transfer/confirm?token=abc123 -> העברה שלישית...
קוד פגיע:
app.post('/transfer/confirm', (req, res) => {
const { token } = req.query;
// פגיע - בודק שהטוקן תקף אבל לא מבטל אותו
const transfer = pendingTransfers.find(t => t.token === token);
if (!transfer) {
return res.status(400).json({ error: 'Invalid token' });
}
// מבצע את ההעברה
executeTransfer(transfer);
// אבל לא מסיר את הטוקן מהרשימה!
res.json({ success: true });
});
עקיפת מעקב שלבים מבוסס Session¶
מניפולציית משתנה session¶
# השרת עוקב אחרי השלב הנוכחי ב-session
@app.route('/wizard/step1', methods=['POST'])
def step1():
session['current_step'] = 1
session['data_step1'] = request.form.to_dict()
return redirect('/wizard/step2')
@app.route('/wizard/step2', methods=['POST'])
def step2():
if session.get('current_step') != 1:
return redirect('/wizard/step1')
session['current_step'] = 2
session['data_step2'] = request.form.to_dict()
return redirect('/wizard/step3')
@app.route('/wizard/step3', methods=['POST'])
def step3():
if session.get('current_step') != 2:
return redirect('/wizard/step1')
# מבצע את הפעולה
process_wizard(session['data_step1'], session['data_step2'], request.form)
הבעיה: מה קורה אם התוקף פותח שני חלונות?
חלון 1: step1 -> step2 (session['current_step'] = 2)
חלון 2: step1 -> step2 (session['current_step'] = 2, דורס את חלון 1)
חלון 1: step3 -> מצליח! (current_step == 2)
אבל data_step1 ו-data_step2 הם מחלון 2!
שימוש חוזר בטוקנים בין שלבים¶
בעיית טוקן לא ייחודי¶
תהליך שינוי סיסמה:
1. POST /change-password/verify -> שולח קוד SMS
2. POST /change-password/confirm?code=123456 -> מאמת קוד
3. POST /change-password/set -> מגדיר סיסמה חדשה
תקיפה - שינוי הנמען בין שלב 2 ל-3:
-- שלב 1: בקשת שינוי סיסמה לחשבון שלנו
POST /change-password/verify HTTP/1.1
Cookie: session=my_session
{"email": "attacker@evil.com"}
-- שלב 2: אימות הקוד (קוד שקיבלנו ב-SMS)
POST /change-password/confirm HTTP/1.1
Cookie: session=my_session
{"code": "123456"}
-- שלב 3: שינוי הפרמטר לחשבון הקורבן!
POST /change-password/set HTTP/1.1
Cookie: session=my_session
{"email": "victim@example.com", "newPassword": "hacked123"}
קוד פגיע:
app.post('/change-password/set', (req, res) => {
// בודק שהמשתמש עבר אימות
if (!req.session.passwordVerified) {
return res.status(403).json({ error: 'Not verified' });
}
// פגיע - לוקח את ה-email מהבקשה הנוכחית
// במקום מה-session שבו בוצע האימות
const email = req.body.email;
const newPassword = req.body.newPassword;
updatePassword(email, newPassword);
req.session.passwordVerified = false;
res.json({ success: true });
});
עקיפת אימות מרובה שלבים¶
דילוג על שלב 2FA¶
תהליך התחברות:
1. POST /login -> אימות שם משתמש וסיסמה
2. POST /login/2fa -> הזנת קוד OTP
3. GET /dashboard -> גישה למערכת
תקיפה:
-- שלב 1: התחברות רגילה
POST /login HTTP/1.1
Content-Type: application/x-www-form-urlencoded
username=victim&password=password123
HTTP/1.1 302 Found
Location: /login/2fa
Set-Cookie: session=abc123
-- דילוג על שלב 2 - גישה ישירה
GET /dashboard HTTP/1.1
Cookie: session=abc123
HTTP/1.1 200 OK
-- אם השרת לא בודק שה-2FA הושלם, נקבל גישה
קוד פגיע:
@app.route('/login', methods=['POST'])
def login():
user = authenticate(request.form['username'], request.form['password'])
if user:
session['user_id'] = user.id
session['authenticated'] = True
# פגיע - מגדיר authenticated=True לפני 2FA
if user.has_2fa:
return redirect('/login/2fa')
return redirect('/dashboard')
@app.route('/dashboard')
@login_required # בודק רק authenticated=True
def dashboard():
return render_template('dashboard.html')
מניפולציית session בין משתמשים¶
-- התחברות כמשתמש רגיל (ללא 2FA)
POST /login HTTP/1.1
username=wiener&password=peter
HTTP/1.1 302 Found
Location: /dashboard
Set-Cookie: session=sess_wiener
-- שינוי ה-session לקורבן דרך שלב 2FA
POST /login HTTP/1.1
username=victim&password=victimpass
HTTP/1.1 302 Found
Location: /login/2fa
Set-Cookie: session=sess_victim
-- שימוש ב-session של הקורבן (שכבר authenticated)
-- עם שינוי ה-user parameter
POST /login/2fa HTTP/1.1
Cookie: session=sess_victim
code=000000&username=victim
-- אם השרת לא בודק את הקוד כראוי
-- או אם ניתן לשנות את ה-username ב-session
מניפולציית תהליך רכישה¶
שינוי מוצר אחרי תשלום¶
שלב 1: הוספת מוצר זול (כבל USB - 5$) לעגלה
שלב 2: תשלום 5$
שלב 3: שינוי המוצר בעגלה ללפטופ (1500$) לפני אישור
שלב 4: אישור - מקבלים לפטופ ב-5$
-- שלב 2: תשלום
POST /checkout/payment HTTP/1.1
Content-Type: application/json
{"cartId": "cart_123", "amount": 5}
HTTP/1.1 200 OK
{"paymentId": "pay_456", "status": "success"}
-- בין שלב 2 ל-3: שינוי המוצר בעגלה
PUT /cart/items/1 HTTP/1.1
Content-Type: application/json
{"productId": 42, "quantity": 1}
-- שלב 3: אישור
POST /checkout/confirm HTTP/1.1
Content-Type: application/json
{"paymentId": "pay_456", "cartId": "cart_123"}
הגנות¶
ולידציית שלבים בצד שרת¶
// מנגנון מעקב שלבים מאובטח
class CheckoutProcess {
constructor(userId) {
this.userId = userId;
this.steps = {
cart: { completed: false, data: null },
shipping: { completed: false, data: null },
payment: { completed: false, data: null },
confirm: { completed: false, data: null }
};
this.currentStep = 'cart';
}
canProceedTo(step) {
const stepOrder = ['cart', 'shipping', 'payment', 'confirm'];
const targetIndex = stepOrder.indexOf(step);
// בדיקה שכל השלבים הקודמים הושלמו
for (let i = 0; i < targetIndex; i++) {
if (!this.steps[stepOrder[i]].completed) {
return false;
}
}
return true;
}
}
app.post('/checkout/confirm', (req, res) => {
const process = getCheckoutProcess(req.session.userId);
// מוגן - בדיקה שכל השלבים הקודמים הושלמו
if (!process.canProceedTo('confirm')) {
return res.status(400).json({
error: 'Please complete all previous steps'
});
}
// בדיקה נוספת שהתשלום תואם לעגלה הנוכחית
if (process.steps.payment.data.amount !== process.steps.cart.data.total) {
return res.status(400).json({
error: 'Payment amount mismatch'
});
}
// אישור ההזמנה
createOrder(process);
process.steps.confirm.completed = true;
});
טוקנים קריפטוגרפיים לכל שלב¶
const crypto = require('crypto');
function generateStepToken(userId, step, data) {
const payload = JSON.stringify({ userId, step, data, timestamp: Date.now() });
const hmac = crypto.createHmac('sha256', SECRET_KEY);
hmac.update(payload);
return {
payload: Buffer.from(payload).toString('base64'),
signature: hmac.digest('hex')
};
}
function verifyStepToken(token, expectedStep) {
const { payload, signature } = token;
const hmac = crypto.createHmac('sha256', SECRET_KEY);
hmac.update(Buffer.from(payload, 'base64').toString());
if (hmac.digest('hex') !== signature) {
return null; // חתימה לא תקפה
}
const data = JSON.parse(Buffer.from(payload, 'base64').toString());
if (data.step !== expectedStep) {
return null; // שלב לא תואם
}
// בדיקת תוקף זמן
if (Date.now() - data.timestamp > 30 * 60 * 1000) {
return null; // פג תוקף (30 דקות)
}
return data;
}
ביטול טוקנים חד-פעמיים¶
app.post('/transfer/confirm', async (req, res) => {
const { token } = req.body;
// מוגן - פעולה אטומית: בדיקה + מחיקה
const transfer = await db.query(
'DELETE FROM pending_transfers WHERE token = ? RETURNING *',
[token]
);
if (!transfer) {
return res.status(400).json({ error: 'Invalid or used token' });
}
// הטוקן נמחק - לא ניתן לשימוש חוזר
await executeTransfer(transfer);
res.json({ success: true });
});
נעילת נתונים אחרי תשלום¶
app.post('/checkout/payment', async (req, res) => {
const cart = await getCart(req.session.userId);
// יצירת snapshot של העגלה ברגע התשלום
const snapshot = {
items: JSON.parse(JSON.stringify(cart.items)),
total: cart.total,
hash: hashCart(cart)
};
const payment = await processPayment(req.body.cardDetails, snapshot.total);
if (payment.success) {
// שמירת ה-snapshot - לא העגלה עצמה
req.session.paymentSnapshot = snapshot;
req.session.paymentId = payment.id;
}
});
app.post('/checkout/confirm', async (req, res) => {
const snapshot = req.session.paymentSnapshot;
const currentCart = await getCart(req.session.userId);
// בדיקה שהעגלה לא השתנתה מאז התשלום
if (hashCart(currentCart) !== snapshot.hash) {
return res.status(400).json({
error: 'Cart changed since payment. Please pay again.'
});
}
// יצירת הזמנה על בסיס ה-snapshot
createOrder(snapshot);
});
סיכום¶
תקיפת תהליכים מרובי שלבים מנצלת את העובדה שמפתחים מניחים שהמשתמש ילך בסדר הנכון. הנקודות המרכזיות:
- תמיד ודאו בצד השרת שכל השלבים הקודמים הושלמו
- אל תסמכו על פרמטרים שנשלחים מהלקוח בין שלבים
- השתמשו בטוקנים חד-פעמיים ומחקו אותם אחרי שימוש
- נעלו את הנתונים ברגע התשלום ובדקו שלא השתנו
- בדקו את הזהות בכל שלב - לא רק בשלב הראשון
- הגדירו timeout לתהליכים - שלא יישארו פתוחים לנצח