एक Asterisk, दस साल: Alpine Linux का चुपचाप SSH बग
Alpine Linux के SSH बग से लंबे पासवर्ड वाले लॉक्ड accounts दस साल तक खुले रहे। जानिए musl की crypt() गड़बड़ी ने यह कैसे किया।

यह संख्या आपको परेशान करनी चाहिए: 257। लगभग दस साल तक, गलत तरह के Linux सिस्टम पर SSH प्रॉम्प्ट में 257 या उससे ज़्यादा अक्षरों का कोई भी पासवर्ड डालने पर आपको रिजेक्ट नहीं किया जाता था — आप अंदर पहुँच जाते थे। कोई भी पासवर्ड। कीबोर्ड पर यूँ ही हाथ चलाना, आपकी छुट्टियों की फ़ोटो का base64 डंप, गलती से पेस्ट किया गया आधा Slack मैसेज: बस लंबा होना चाहिए था, दरवाज़ा खुल जाता था। “sshole” नाम से जाना गया Alpine Linux SSH बग उन कमज़ोरियों में से है जो शर्तें जाँचने तक मज़ाक लगती हैं, और फिर किसी incident report जैसी लगने लगती हैं।
संक्षेप में: musl libc के साथ बने OpenSSH पर — यानी Alpine, OpenWrt, Buildroot, Yocto, और इंटरनेट पर मौजूद Docker base images का एक बड़ा हिस्सा — वे accounts जिन्हें गणितीय रूप से असंभव होना चाहिए था, वे 257 से 1,024 अक्षरों के बीच कोई भी पासवर्ड स्वीकार कर लेते थे। Root। आपका केवल-key वाला deploy user। Alpine की आधिकारिक AWS और Azure images का डिफ़ॉल्ट alpine user, जो passwordless doas के साथ आता है। इसका fix OpenSSH 10.6p2 में आया। लेकिन यह कैसे हुआ, इसकी कहानी 1980 तक जाती है — छोटे-छोटे, समझदारी भरे फ़ैसलों की एक कड़ी, और उस समय इसकी हर कड़ी सही लग रही थी।
Alpine Linux SSH बग असल में कैसे काम करता है
इस टक्कर को समझने के लिए तीन हिस्से चाहिए, और हर एक अलग codebase व अलग दशक का है। पहला: Linux पासवर्ड कैसे स्टोर करता है। जब आप पासवर्ड सेट करते हैं, तो hash /etc/shadow में जाता है, हर account के लिए एक लाइन। सामान्य account की लाइन ऐसी दिखती है $6$salt$hash, जहाँ $6$ का मतलब sha512crypt है। लेकिन जिन accounts को कभी पासवर्ड स्वीकार नहीं करना चाहिए — daemons, केवल-key वाले users, locked root — उन्हें इसके बजाय एक placeholder मिलता है:
alice:$6$yJ3kXw9q$k2P9vLm4...:19723:0:99999:7:::
deploy:*:19723:0:99999:7:::
root:*:19723:0:99999:7:::
लॉग इन करते समय sshd आपका टाइप किया हुआ पासवर्ड लेता है, उसे stored salt के साथ hash करता है, और नतीजे की तुलना stored field से string-compare करके करता है। एक जैसी string हुई, तो आप अंदर। asterisk एक sentinel है: यह किसी भी चीज़ का hash नहीं है, इसलिए कोई पासवर्ड इससे कभी मेल नहीं खा सकता। Unix के शुरुआती दिनों से केवल-key वाले accounts को लॉक करने का यही तरीका रहा है। यह इसलिए काम करता है क्योंकि तुलना कभी सफल नहीं हो सकती — जब तक hashing function को खुद asterisk पैदा करने पर मजबूर न किया जा सके। और यही दूसरे हिस्से पर ले जाता है।
दूसरा हिस्सा musl की error handling है। परंपरागत रूप से, crypt() तब NULL लौटाता है जब वह अपना काम नहीं कर सकता — मान लीजिए salt ऐसा algorithm माँगे जो सिस्टम में नहीं है। दिक्कत यह थी कि दशकों तक लिखे गए software ने NULL की जाँच ही नहीं की और उसे खुशी-खुशी strcmp को दे दिया, जिससे segfault हो जाता। इसलिए 2012 में musl ने जानबूझकर एक डिज़ाइन फ़ैसला लिया: उसका crypt() कभी NULL नहीं लौटाएगा। विफलता पर वह एक ऐसी string लौटाएगा जो किसी असली password hash से मेल न खाए, ताकि टूटे हुए callers को crash के बजाय गलत पासवर्ड दिखे। जो failure string चुनी गई वह थी *। तीन हफ़्ते बाद musl ने sha512crypt जोड़ा, जिसमें 256-byte का input cap था — hashing की लागत पासवर्ड की लंबाई के साथ बढ़ती है, इसलिए बहुत बड़े पासवर्ड denial-of-service का ज़रिया बन सकते हैं। जो भी इससे लंबा हो उसे reject करो, जो एक failure है, और जो * लौटाता है। इसका दस्तावेज़ीकरण करने वाली comment स्रोत कोड में पूरे समय पड़ी रही।
तीसरा हिस्सा sshd की शिष्टता है। 2016 में CVE-2016-6210 ने दिखाया कि response का समय नापकर valid usernames पता किए जा सकते हैं: जो users मौजूद नहीं थे उन्हें तुरंत reject मिल जाता था, क्योंकि hash निकालने के लिए कुछ था ही नहीं। इसका fix बढ़िया था — जिन accounts में कोई usable hash नहीं है, sshd किसी दूसरे account का salt उधार लेता है और दिया गया पासवर्ड फिर भी hash करता है, सिर्फ़ उतना ही समय खर्च करने के लिए। एक धीमा “नहीं”। पकड़ यह है: यह रास्ता सिर्फ़ मौजूद न होने वाले users के लिए नहीं चलता। यह उन असली users के लिए भी चलता है जिनका shadow field * है, क्योंकि asterisk usable hash नहीं है। तो अब sshd crypt(आपका_पासवर्ड, उधार_लिया_salt) निकालता है और नतीजे की तुलना * से करता है।
/* what sshd effectively does in this configuration */
const char *stored = shadow_field(user); /* "*" for locked accounts */
const char *salt = pick_salt(user); /* borrowed: "*" isn't a valid hash */
char *ret = crypt(typed_password, salt); /* musl: "*" if password > 256 bytes */
if (ret && strcmp(ret, stored) == 0)
allow_login(); /* "*" == "*" → welcome in */
हर तत्व को आपस में मेल खाना होगा: stored field * है, crypt विफल होता है, और failure value भी * है। glibc पर विफल crypt NULL लौटाता है, जो तुलना को सुरक्षित रूप से फेल कर देता है। libxcrypt वाले सिस्टम पर failure string *0 है — एक अक्षर का फ़र्क, और यही एक अक्षर पूरा खेल है, क्योंकि *0 कभी * के बराबर नहीं होता। musl पर error value और lock value एक ही अक्षर हैं, और strcmp के पास error और match में फ़र्क करने का कोई तरीका नहीं। एक तार, दो मतलब, और रिसीवर इनमें फ़र्क नहीं कर सकता।
एक दशक के पूरी तरह समझदार फ़ैसले
इस बग को पढ़ने लायक क्या बनाता है? इसका mechanism नहीं — बल्कि यह कि इसमें शामिल किसी ने भी कुछ साफ़-साफ़ बेवकूफ़ी भरा नहीं किया। 1980 से शुरू करें: जब password hashes world-readable /etc/passwd में रहते थे, तब Bill Joy ने daemon नाम का एक account बनाया जिसमें कोई कभी लॉग इन नहीं करना चाहेगा, और उसके password hash की जगह * रख दिया। एक साफ़ sentinel, जो दशकों आगे था। (कहा जाता है कि real-name field में लिखा था “the devil himself।”) Unix आज भी accounts को ठीक इसी तरह लॉक करता है।
musl का 2012 का फ़ैसला भी हर कोण से बचाव योग्य लगता है। crash करने वाले callers एक असली समस्या हैं, और जो C library दशकों के लापरवाह application code के खिलाफ NULL को हथियार बनाती है, वह बहुत दुश्मन बनाती है। ऐसी string लौटाना जो कभी मेल न खा सके, graceful degradation है। hash-आधारित denial of service रोकने के लिए पासवर्ड की लंबाई सीमित करना भी ऐसा ही है। sshd का timing fix भी — unhashable users के लिए response को पैड करना standard practice है। यहाँ तक कि Alpine ने इसके बाद जो किया, वह भी। 2019 में CVE-2019-5021 ने पता चलाया कि Alpine की Docker images में root का password field खाली भेजा गया था — कुछ configurations में तो कोई पासवर्ड ही नहीं था। Alpine ने root को ! से लॉक किया। लेकिन PAM के बिना sshd ! को पूरी तरह लॉक मानता है, जिससे SSH key logins भी बंद हो जाते हैं — और cloud images key logins पर ही चलती हैं। तो 2022 में Alpine 3.16 ने root को * पर ले जाया: अब भी लॉक, keys चलती हैं, सब खुश। बस * ठीक वही string थी जिसे musl के crypt से छापने पर बहकाया जा सकता था।
हर कदम एक असली समस्या का बचाव योग्य जवाब था। यही बात असहज करती है — आप हर फ़ैसले के समय कमरे में होते, तो हर बार सिर हिलाकर सहमति देते।
लगभग-चूक तो और भी बुरी हैं। एक Debian developer ने एक बार glibc से musl वाला ठीक वही * व्यवहार अपनाने को कहा; छह साल बाद Debian libxcrypt पर चला गया, जिसके *0 ने छेद बस संयोग से बंद कर दिया। एक BusyBox थ्रेड में passwd के 257-अक्षर वाले पासवर्ड पर अटकने की शिकायत थी — वही asterisk, वही सीमा, और उस थ्रेड में किसी ने “sshd” नहीं कहा। एक security reviewer ने sshd के pick_salt() function को patch किया, जबकि असली बग उसके चार लाइन नीचे बैठा था, और किसी के उसे ढूँढने से चार दिन पहले। यह चीज़ बार-बार उन लोगों के सामने से गुज़री जो लगभग सीधे उसी को देख रहे थे।

क्या आपका इन्फ़्रास्ट्रक्चर musl OpenSSH बग से प्रभावित है?
sshole की राहत यह है कि इसके चलने के लिए पूरी शर्तें एक साथ चाहिए। कोई एक भी छूट जाए तो आप सुरक्षित हैं। आप खतरे में हैं अगर ये सभी सच हैं:
- आपका sshd musl के साथ बना है: Alpine, OpenWrt, Buildroot, Yocto, या
alpineसे निकली कोई Docker image जिसमेंopenssh-serverइंस्टॉल है। - sshd नहीं PAM के ज़रिए चलता — Alpine का openssh-server डिफ़ॉल्ट रूप से ऐसा नहीं करता, या आपने
UsePAM noसेट किया है। PAM का code path*वाले account के लिए कभी crypt नहीं बुलाता। - Password authentication चालू है। डिफ़ॉल्ट रूप से यह चालू होता है।
- कम से कम एक account के
/etc/shadowमें बेयर*है: केवल-key वाले users, LDAP-backed accounts, Alpine पर root। - कम से कम एक दूसरे account में असली password hash है, ताकि उधार लेने के लिए salt मौजूद हो।
पाँचों शर्तें पूरी हों, तो बॉक्स पर हर * account 257 से 1,024 अक्षरों के बीच का कोई भी पासवर्ड स्वीकार करता है। सबसे भयानक configuration Alpine की आधिकारिक AWS और Azure images है: डिफ़ॉल्ट alpine user केवल-key वाला है (यानी *) और उसके पास passwordless doas है। वहाँ लंबे पासवर्ड से लॉग इन करना privilege escalation भी नहीं — सीधे root का दरवाज़ा है। एक बचाव का श्रेय देना होगा जिसने इसे इतने समय तक चुप रखा: OpenSSH का PermitRootLogin 2015 से root के लिए पासवर्ड लॉग इन मना करता रहा है, इसलिए सबसे स्पष्ट निशाना डिफ़ॉल्ट रूप से आंशिक रूप से सुरक्षित था। deploy users और service accounts को कोई सुरक्षा नहीं मिली थी।
अगर आप Debian, Ubuntu, Fedora या कोई और glibc-आधारित सिस्टम चलाते हैं, तो आप सुरक्षित हैं। PAM से गुज़रने वाला कोई भी सेटअप: सुरक्षित। Dropbear: सुरक्षित। OpenBSD: सुरक्षित। संदेह करने वालों के लिए एक चेतावनी — इसे दोहराने की शुरुआती community कोशिशें असंगत रही हैं, क्योंकि पाँचों शर्तें एक-एक करके छूटना आसान है। कोई आधिकारिक Alpine image आज़माता है, यह नहीं समझता कि root का field * नहीं बल्कि ! है, असफल होता है, और आगे बढ़ जाता है। “मेरे लैपटॉप पर दोहरा नहीं पाया” को production के बारे में कोई सबूत मत मानिए। शर्तें जाँचिए, माहौल नहीं।
असली मूल कारण: In-Band Signaling और Magic Strings
sshole को “अजीब libc edge case” मानकर आगे बढ़ जाना लुभावना है। लेकिन विफलता का तरीका इससे पुराना और ज़्यादा सामान्य है: in-band signaling। अक्षर * को दो काम सौंप दिए गए थे — /etc/shadow में इसका मतलब है “इस account के लिए कोई पासवर्ड नहीं है,” और musl के crypt में इसका मतलब है “hashing विफल हुई।” एक ही channel पर दो संदेश, और रिसीवर इन्हें अलग नहीं कर सकता। सामान्य डेटा में छिपे sentinel values ही वह तरीका हैं जिससे सिस्टम खुद का नुकसान करने लगते हैं। NULL एक स्वीकार्य error signal था (crash होने वाले callers के बावजूद)। exception बेहतर है। और एक Result<T, E> जो caller को failure स्पष्ट रूप से handle करने पर मजबूर करे, सबसे अच्छा है। व्यावहारिक नियम, जिसे मैं हर systems programmer के शरीर पर गुदवा दूँ: कोई magic string नहीं। अगर एक ज़रूरी ही है, तो कुछ ऐसा चुनो जो problem domain की किसी भी value से टकरा न सके — libxcrypt का *0, musl के * से ठीक एक byte आगे है, और वही एक byte कमज़ोरी और CVE के बीच का फ़र्क है।
इसी से जुड़ा दूसरा सबक है: चुपचाप compatibility shims। musl का graceful-* फ़ैसला टूटे software को crash होने से रोकने के लिए है — यानी यह टूटे software को कभी टूटा हुआ दिखने ही नहीं देता। segfault भद्दा है, पर यह एक संकेत भी है: कोई बग दर्ज करता है, कोई caller ठीक करता है। error को निगल लो और गलती दस साल तक चुपचाप बढ़ती रहती है। मुझे अब यकीन होता जा रहा है कि system design में यह सबसे गहरी दरार है: विपदा का विरोध करने वाला engineering आम तौर पर जल्दी और ज़ोर से फ़ेल होता है, जबकि सहजता के लिए optimize करने वाला engineering छिपा कर्ज़ जमा करता रहता है, जब तक बिल एक साथ नहीं आ जाता।
और यह एक आरामदायक मिथक भी तोड़ना चाहिए: कि पुराना, व्यापक रूप से इस्तेमाल होने वाला code इसलिए ज़्यादा भरोसेमंद है क्योंकि वह battle-tested है। उम्र जाँच-पड़ताल को समान रूप से नहीं बाँटती — वह उसे hot paths पर केंद्रित कर देती है और ठंडे हिस्सों को जमने के लिए छोड़ देती है। सब crypto का ऑडिट करते हैं। लगभग कोई उस बोरिंग glue का ऑडिट नहीं करता जहाँ sshd पासवर्ड के बारे में libc से बात करता है, क्योंकि वह code “हमेशा से चल रहा है” वाली श्रेणी में आ गया है। दो अल्पसंख्यक configurations का मेल — musl सिस्टम, और उनमें भी वे जो PAM इस्तेमाल नहीं करते — ठीक वही जगह है जहाँ कोई बग दस साल तक बिना छेड़े रह सकता है।
sshole बग कैसे ठीक करें और अपने logs कैसे जाँचें
patch आसान हिस्सा है: OpenSSH 10.6p2 छेद बंद कर देता है। Alpine पर यह एक command है। अगर आज अपडेट नहीं कर सकते, तो mitigation उतना ही सीधा और पूरी तरह प्रभावी है — password authentication बंद कर दें। पासवर्ड जाँच नहीं, तो बग भी नहीं। सच कहूँ तो दोनों करने चाहिए, क्योंकि key-only auth इस बग के आने से पहले भी सही जवाब था और सबके patch कर लेने के बाद भी रहेगा। यह उस सबक की गूँज है जो हमने patch बनाम mitigate की दूसरी बहसों में देखा है: जब mitigation सस्ता और पूरा हो, तो होशियारी मत दिखाइए।
# patch: openssh 10.6p2 or later
apk upgrade openssh
# can't patch yet? passwords off, keys only
echo "PasswordAuthentication no" > /etc/ssh/sshd_config.d/00-no-passwords.conf
rc-service sshd restart

फिर कठिन सवाल: क्या किसी ने पहले वहाँ पहुँच बनाई? दस साल लंबा dwell time है। आप उन accounts के सफल password authentications ढूँढ रहे हैं जिनके पास कभी पासवर्ड था ही नहीं — केवल-key वाले deploy users, service accounts, LDAP identities, Alpine boxes पर root। इन accounts के लिए password method से सफल logins बेहद दुर्लभ होने चाहिए; कोई भी मिलना पाँच-अलार्म वाली आग है। असहज सच यह है कि sshd सफलता दर्ज करता है, पासवर्ड की लंबाई नहीं, इसलिए लिखे जाने के साथ ही ज़्यादातर सीधे सबूत गायब हो जाते हैं। और जैसा हम पहले भी कवर कर चुके हैं, cloud authentication logging में अच्छे दिनों में भी blind spots होते हैं — short-lived VMs पर instance-level sshd logs ठीक वहीं रहते हैं। जो आपके पास है उसे grep कीजिए। मान लीजिए वह अधूरा है।
दस शांत सालों से हमें क्या सीखना चाहिए
खोज खुद उतनी ही चर्चा की हक़दार है जितना बग। sshole खोजने वाले शोधकर्ता ने crypto को नहीं घूरा — उन्होंने OpenSSH और libc के बीच की उस उबाऊ सीमा को देखा, sshd को हर सोची जा सकने वाली configuration में बनाया (glibc, musl, PAM, बिना PAM, shadow, बिना shadow), और matrix पर लगभग 17,600 गलत-बने logins फेंके। अजीब hashes, टूटे accounts, बहुत लंबे पासवर्ड। हर build हर login पर एक-दूसरे से सहमत था, सिवाय एक के: बिना PAM वाला musl। उसने उन चीज़ों पर हाँ कह दिया जिन पर उसे नहीं कहना चाहिए था। यह कोई किस्मत वाली खोज नहीं; यह coverage है। configuration matrices पर differential testing ही वह तरीका है जिससे intersections में छिपे बग मिलते हैं — और यह ठीक वही थका-न-देने वाला, संपूर्ण काम है जिसमें agent swarms हैरान करने वाली हद तक अच्छे साबित हो रहे हैं। अगला sshole दस शांत साल नहीं पाएगा।
वह सवाल जो operators की रातों की नींद उड़ा देना चाहिए: क्या सचमुच दस साल बिना किसी के ध्यान दिए निकल गए — या गलत लोगों ने ध्यान दे दिया? सिर्फ़ आकस्मिक मामला सोचिए: पासवर्ड prompt में गलत clipboard सामग्री चिपका दीजिए — आधा Slack thread, पूरी SSH private key, 257 अक्षरों से ज़्यादा कुछ भी — और कमज़ोर बॉक्स पर एक लॉक्ड account बस खुल जाता है। अगर आपने कभी पासवर्ड फ़ील्ड में कुछ गलत पेस्ट नहीं किया, तो आप मेरे साथ काम करने वाले किसी भी व्यक्ति से ज़्यादा सावधान हैं। कोई, कहीं, ऐसे बॉक्स में लॉग इन कर चुका है जहाँ उसे नहीं जाना था, और चुपचाप terminal बंद कर दिया। शायद बहुत से कोई।
तो patch कीजिए, हाँ। पासवर्ड बंद कीजिए, बिल्कुल। लेकिन फिर एक घंटा निकालकर अपने सिस्टम में उन अक्षरों को ढूँढिए जो दो काम कर रहे हैं — sentinel values, चुपचाप मिलने वाले fallbacks, “यह कभी नहीं हो सकता” वाली branches, और वे compat shims जिन्हें किसी को याद नहीं कि किसने लिखे। हर परिपक्व codebase में ये होते हैं। ये इसलिए अदृश्य हैं क्योंकि इन्होंने कभी ऐसी समस्या नहीं दी जो कोई देख सके। आपके stack में अभी कहीं एक asterisk चुपचाप दो अर्थ एक साथ लिए बैठा है। किसी लंबे पासवर्ड वाले व्यक्ति के उसे पा लेने से पहले उसे ढूँढ निकालिए।


