single-packet attack से वेब race conditions को जीतना

अधिकांश लेख आपको बताते हैं कि race condition एक “check और use के बीच का timing window” है। सही है, और बेकार भी — क्योंकि आपके race attacks के असफल होने की वजह यह नहीं है कि आप concept नहीं समझते। वजह यह है कि window इतना छोटा है कि internet की precision उसके सामने कम पड़ जाती है।

एक सामान्य race window — किसी app द्वारा balance पढ़ने और balance - amount लिखने के बीच का अंतराल — microseconds से लेकर कुछ milliseconds में मापा जाता है। दो HTTP requests जिन्हें आप एक ही क्षण पर भेजते हैं, उनके पहुँचने और server पर schedule होने तक का jitter नियमित रूप से 1 से 10 milliseconds होता है, और public internet के आर-पार तो और भी बुरा। तो आपकी “एक-साथ वाली” requests एक साफ-सुथरी कतार में पहुँचती हैं, server उन्हें एक-एक करके process करता है, limit टिकी रहती है, और आप निष्कर्ष निकाल लेते हैं कि endpoint सुरक्षित है। वह नहीं है। आप बस server के वोट देने से पहले ही network jitter के हाथों race हार गए।

वेब races को exploit करने का पूरा हुनर इस लड़ाई में है कि N requests एक ही क्षण पर पहुँचें। यह जीत लो, और “unexploitable” limit एक ही झटके में गिर जाती है।

last-byte sync कभी पूरी तरह क्यों काफी नहीं था

पुरानी HTTP/1.1 चाल — last-byte synchronization — आपको करीब तक पहुँचा देती है: ~20 connections खोलो, हर request को उसके अंतिम byte को छोड़कर भेजो, फिर सभी अंतिम bytes को एक साथ छोड़ दो। यह काम करती है, और वर्षों तक यही state of the art थी। पर इसमें एक ऐसा दोष है जिसे ठीक नहीं किया जा सकता: वे 20 अलग-अलग TCP connections हैं। हर एक का अपना congestion window है, अपनी path timing है, अपनी kernel scheduling है। अंतिम byte रोक लेने के बावजूद, arrival फिर भी कुछ milliseconds में फैल जाती है — इतनी कि microsecond का window हार जाए। last-byte sync 1-in-1000 race को शायद 1-in-10 में बदल देती है। बेहतर, फिर भी अस्थिर, फिर भी “सिर्फ उसी region के EC2 box से reproduce होती है।”

single-packet attack: एक packet, कोई jitter नहीं

यह रहा वह primitive जिसने इसे बदल दिया, James Kettle के Smashing the State Machine research से। HTTP/2 कई requests को एक ही connection पर multiplex करता है। तो 20 connections की race लगाने के बजाय, आप:

  1. एक ही HTTP/2 connection पर 20-30 requests भेजते हैं, पर हर एक का अंतिम frame रोक लेते हैं (वह हिस्सा जो server को बताता है कि “यह request पूरी हो गई”)।
  2. जब सब staged हो जाएँ, तो हर अंतिम frame को एक साथ छोड़ देते हैं — और चूँकि वे बहुत छोटे हैं, सभी 20-30 एक single ~1,500-byte TCP packet (एक network MTU) के अंदर समा जाते हैं।
  3. वह एक packet atomically deliver होता है। Server सभी 20-30 requests को एक ही क्षण पर “complete” होते देखता है और उन्हें एक साथ schedule करता है।

अब network jitter अप्रासंगिक है, क्योंकि कोई “requests के बीच” वाला हिस्सा है ही नहीं — बस एक packet है। internet के शोर के वे 10ms जो पहले आपको serialize कर देते थे, गायब हो गए। एक race जो सिर्फ localhost पर काम करती थी, वह अब आपके laptop से WiFi पर, भरोसेमंद तरीके से, एक ही प्रयास में काम करती है। यही “theoretically vulnerable” और आपकी report में एक screenshot के बीच का अंतर है।

एक पेच है जो बहुत से पहले प्रयासों को खा जाता है: connection warming. अगर आपका attack packet connection पर पहली ही चीज है, तो वह TLS handshake round-trips और TCP slow-start को खा जाता है, और requests फिर से फैल जाती हैं। पहले मुट्ठी भर throwaway requests fire करें (कुछ GET /) ताकि handshake पूरा हो और congestion window ramp हो जाए, फिर attack को stage करें। इसे छोड़ दिया तो आप कसम खाएँगे कि technique काम ही नहीं करती।

limit-overrun से आगे: अच्छे bugs असल में कैसे दिखते हैं

“एक coupon को दो बार redeem करना” तो tutorial है। जो findings फल देती हैं वे इन तीन आकारों में आती हैं:

एक endpoint पर limit-overrun. क्लासिक, और अब भी हर उस जगह मौजूद जहाँ पैसा रहता है:

  • Gift cards / store credit. एक $50 का card एक ही packet में 10 carts पर apply करें → $500 का credit, क्योंकि सभी 10 “balance: $50” पढ़ लेते हैं, इससे पहले कि कोई भी उसे decrement करे।
  • Withdrawals / transfers. $100 balance से $100 को 5 बार withdraw करें — हर request balance को किसी भी debit के landing से पहले पढ़ती है। सीधा-सीधा double-spend।
  • 2FA / OTP rate limits. एक endpoint जो “3 code प्रयास, फिर lockout” की अनुमति देता है, वह attempt counter को increment करने से पहले जाँचता है। एक ही packet में 30 guesses भेजें और सभी 30 “attempts: 0” पढ़ते हैं और gate पास कर जाते हैं — आपको हर window में 3 के बजाय 30 tries मिलती हैं। यह अक्सर एक brute-force के असंभव होने और lunch-time के काम होने के बीच का पूरा फर्क होता है।

Multi-endpoint collisions. सूक्ष्म, high-severity वर्ग: दो अलग requests को टकराने के लिए timed किया गया। सबसे प्रचलित उदाहरण है email verification — confirm-email (आपके नियंत्रण वाले पते के लिए) और change-email (victim के पते पर) को एक साथ fire करें, और ऐसी state में पहुँचें जहाँ victim का email आपके account पर confirmed मार्क हो जाए। check-and-act दो endpoints पर फैला होता है, इसलिए single-endpoint locking उन्हें नहीं बचाती।

छिपे हुए sub-states. Kettle की गहरी बात: कई operations एक intermediate state से गुजरते हैं जिसका कभी observable होना ही नहीं चाहिए — एक user row जो मौजूद है पर पूरी नहीं हुई, एक session जो authenticated है पर अभी scoped नहीं, एक file जो बन गई पर अभी ACL’d नहीं। एक race आपको उस sub-state के दौरान observe या act करने देती है। ये खुद को “limits” के रूप में घोषित नहीं करतीं, और ठीक इसीलिए ये बची रह जाती हैं। इन्हें आप एक read को एक write के खिलाफ race लगाकर खोजते हैं और देखते हैं कि read कुछ ऐसा return तो नहीं करती जो अभी मौजूद ही नहीं होना चाहिए।

Detection: side effect गिनें, 200s नहीं

वह जाल जो false negatives और false positives दोनों पैदा करता है: HTTP status से आँकना। दो खास बातें जो आपको बचाएँगी:

  • 200s की एक दीवार झूठ हो सकती है। कुछ backends सभी 30 requests स्वीकार कर लेते हैं, फिर एक database unique constraint चुपचाप duplicates को reject कर देता है — आप तीस 200s देखते हैं और कोई bug नहीं। असली side effect की पुष्टि करें: क्या balance सचमुच दो बार हिला? क्या दो rows सचमुच बनीं? response code ही outcome नहीं है।
  • एक जीत भी एक finding है। आपको सभी 30 के सफल होने की जरूरत नहीं। अगर 29 “coupon already used” return करें और एक अतिरिक्त redemption land कर जाए, तो limit टूट गई — आपको उसे बस एक बार तोड़ना था। किसी run को इसलिए मत त्यागिए कि वह ज्यादातर rejections थी।

अदृश्य sub-state bugs के लिए, पहले benchmark करें: एक batch भेजें और responses का सामान्य spread record करें, फिर outlier ढूँढें — वह एक response जो अलग length का हो, अलग state का हो, एक ऐसा object जो दिखना ही नहीं चाहिए था। वह anomaly ही sub-state का रिसना है।

वे गलतियाँ जो आपसे bug छीन लेती हैं

  • कोई connection warming नहीं → आपका पहला packet धीमा और फैला हुआ होता है। इसे warm करें।
  • Idempotency keys. अगर client हर action के लिए एक unique key भेजता है और server उसका सम्मान करता है, तो आपके 30 duplicates एक में सिमट जाते हैं। key को हटाएँ या बदलें और फिर से प्रयास करें।
  • Constraints को शोर मानें, फैसला नहीं। एक unique index इस race को हरा सकता है जबकि उसी object पर एक अलग endpoint पर कोई नहीं होता। एक असफल race एक code path के बारे में जानकारी है, न कि स्वच्छ सेहत का प्रमाणपत्र।
  • State freshness. एक coupon जो सचमुच single-use है, पहले वैध उपयोग के बाद कुछ भी “confirm” नहीं करेगा — हर run की शुरुआत एक साफ, अनुपयोगित object से करें।

इसे बचाव क्यों दिया गया है (और कैसे)

आपकी report के remediation section के लिए: fix कभी भी “एक check जोड़ो” नहीं होता। यह data layer पर atomicity है — SELECT … FOR UPDATE, एक unique constraint, एक atomic decrement (UPDATE … SET balance = balance - 1 WHERE balance >= 1), या एक version column के साथ optimistic locking। जो कुछ भी application code में, दो statements के आर-पार, read-then-write करता है, वह खुद अपने साथ race लगा रहा है। Idempotency keys duplicate-submission वाले variant को बंद कर देती हैं।

मुद्दा

Race conditions दुर्लभ नहीं हैं — वे कम ही reproduce की जाती हैं, क्योंकि requests को एक साथ land कराने की technique, हाल तक, इतनी अविश्वसनीय थी कि लोग स्पष्ट coupon endpoint को test करते, असफल होते, और आगे बढ़ जाते। single-packet attack उस अस्थिरता को हटा देता है। यह app की हर limit, हर “आप यह केवल एक बार कर सकते हैं”, और हर multi-step flow को एक ऐसे candidate के रूप में फिर से पेश करता है जिसे आप एक packet और दो सेकंड में test कर सकते हैं। bugs हमेशा से वहाँ थे; अब timing मुफ्त है।

Crusader HTTP/2 बोलता है, इसलिए यह single-packet attack को सीधे stage कर सकता है — connection को warm करें, अंतिम frames को रोकें, उन्हें एक साथ छोड़ें, और outcomes को diff करें। इसे Finding IDOR and BOLA में दिए गए access-control काम के साथ जोड़ें — दोनों business-logic bugs हैं जिन्हें कोई scanner आपके लिए कभी नहीं ढूँढ पाएगा। Crusader मुफ्त डाउनलोड करें और इसे किसी भी ऐसी limit पर तानें जिसके enforced होने की app कसम खाता है।

अक्सर पूछे जाने वाले सवाल

single-packet attack क्या है?
एक ऐसी technique जो 20-30 पूर्ण HTTP/2 requests को एक ही TCP packet के अंदर रख देती है। आप हर request भेजते हैं पर उसका अंतिम frame रोक लेते हैं, फिर सभी अंतिम frames एक साथ छोड़ते हैं, ताकि requests server पर एक साथ पहुँचें। यह उस network jitter को खत्म कर देता है जो अन्यथा requests को serialize कर देता और race window को बंद कर देता है। इसे James Kettle द्वारा PortSwigger research में पेश किया गया था और यह HTTP/2 तथा HTTP/3 पर काम करता है।
भरोसेमंद race conditions को HTTP/2 की जरूरत क्यों होती है?
HTTP/1.1 पर आप कई अलग-अलग TCP connections के बीच last-byte synchronization से केवल एक-साथपन का अनुमान भर लगा सकते हैं, और उन connections के बीच का jitter arrival को फिर भी कुछ milliseconds तक फैला देता है। HTTP/2 कई requests को एक ही connection पर multiplex करता है, इसलिए आप उन्हें एक packet में पैक कर सकते हैं और arrival सचमुच एक साथ होता है। HTTP/1.1 attacks पुरानी, कम भरोसेमंद विधि पर वापस लौट जाते हैं।
connection warming क्या है और यह क्यों मायने रखता है?
attack से पहले कुछ throwaway requests भेजना ताकि TLS handshake पूरा हो जाए और server-side connection तैयार हो। इसके बिना, handshake round-trips और TCP slow-start आपके attack packet में असमान देरी ला देते हैं और वही timing spread फिर से ले आते हैं जिसे हटाने के लिए single-packet attack मौजूद है।
Crusader मुफ़्त आज़माएँ →