
Qualys Security Advisory
Baron Samedit: Sudo में हीप-आधारित बफर ओवरफ्लो (CVE-2021-3156)
सारांश विश्लेषण शोषण आभार समयरेखा
हमने Sudo (https://www.sudo.ws/) में एक हीप-आधारित बफर ओवरफ्लो खोजा। यह भेद्यता:
बिना प्रमाणीकरण के किसी भी स्थानीय उपयोक्ता (सामान्य उपयोक्ता और सिस्टम उपयोक्ता, sudoers और non-sudoers) द्वारा शोषण योग्य है (अर्थात, आक्रमणकारी को उपयोक्ता का पासवर्ड जानने की आवश्यकता नहीं है);
जुलाई 2011 (कमिट 8255ed69) में पेश की गई थी, और अपनी डिफ़ॉल्ट कॉन्फ़िगरेशन में सभी विरासत संस्करणों 1.8.2 से 1.8.31p2 तक और सभी स्थिर संस्करणों 1.9.0 से 1.9.5p1 तक को प्रभावित करती है।
हमने इस भेद्यता के लिए तीन अलग-अलग शोषण विकसित किए, और Ubuntu 20.04 (Sudo 1.8.31), Debian 10 (Sudo 1.8.27), और Fedora 33 (Sudo 1.9.2) पर पूर्ण रूट विशेषाधिकार प्राप्त किए। अन्य ऑपरेटिंग सिस्टम और वितरण भी संभवतः शोषण योग्य हैं।
यदि Sudo को "shell" मोड (shell -c command) में एक कमांड चलाने के लिए निष्पादित किया जाता है:
या तो -s विकल्प के माध्यम से, जो Sudo का MODE_SHELL फ़्लैग सेट करता है;
या -i विकल्प के माध्यम से, जो Sudo के MODE_SHELL और MODE_LOGIN_SHELL फ़्लैग सेट करता है;
तो, Sudo के main() की शुरुआत में, parse_args() सभी कमांड-लाइन तर्कों को जोड़कर (पंक्तियाँ 587-595) और सभी मेटा-वर्णों को बैकस्लैश से एस्केप करके (पंक्तियाँ 590-591) argv को फिर से लिखता है (पंक्तियाँ 609-617):
बाद में, sudoers_policy_main() में, set_cmnd() कमांड-लाइन तर्कों को हीप-आधारित बफर "user_args" में जोड़ता है (पंक्तियाँ 864-871) और मेटा-वर्णों को अन-एस्केप करता है (पंक्तियाँ 866-867), "sudoers मिलान और लॉगिंग प्रयोजनों के लिए":
दुर्भाग्य से, यदि कोई कमांड-लाइन तर्क एक एकल बैकस्लैश वर्ण के साथ समाप्त होता है, तो:
पंक्ति 866 पर, "from[0]" बैकस्लैश वर्ण है, और "from[1]" तर्क का null टर्मिनेटर है (अर्थात, स्पेस वर्ण नहीं);
पंक्ति 867 पर, "from" को बढ़ाया जाता है और यह null टर्मिनेटर की ओर इंगित करता है;
पंक्ति 868 पर, null टर्मिनेटर को "user_args" बफर में कॉपी किया जाता है, और "from" को फिर से बढ़ाया जाता है और यह null टर्मिनेटर के बाद के पहले वर्ण की ओर इंगित करता है (अर्थात, तर्क की सीमा से बाहर);
पंक्तियों 865-869 पर "while" लूप सीमा से बाहर के वर्णों को पढ़ता है और "user_args" बफर में कॉपी करता है।
दूसरे शब्दों में, set_cmnd() हीप-आधारित बफर ओवरफ्लो के प्रति संवेदनशील है, क्योंकि "user_args" बफर में कॉपी किए गए सीमा से बाहर के वर्ण इसके आकार (पंक्तियों 852-853 में गणना) में शामिल नहीं थे।
हालाँकि, सिद्धांत रूप में, कोई भी कमांड-लाइन तर्क एक एकल बैकस्लैश वर्ण के साथ समाप्त नहीं हो सकता है: यदि MODE_SHELL या MODE_LOGIN_SHELL सेट है (पंक्ति 858, संवेदनशील कोड तक पहुँचने के लिए एक आवश्यक शर्त), तो MODE_SHELL सेट है (पंक्ति 571) और parse_args() ने पहले ही सभी मेटा-वर्णों को एस्केप कर दिया है, जिसमें बैकस्लैश भी शामिल हैं (अर्थात, इसने प्रत्येक एकल बैकस्लैश को दूसरे बैकस्लैश के साथ एस्केप किया)।
व्यवहार में, हालाँकि, set_cmnd() में संवेदनशील कोड और parse_args() में एस्केप कोड थोड़ी भिन्न स्थितियों से घिरे हैं:
बनाम:
तो हमारा प्रश्न यह है: क्या हम MODE_SHELL और या तो MODE_EDIT या MODE_CHECK सेट कर सकते हैं (संवेदनशील कोड तक पहुँचने के लिए) लेकिन डिफ़ॉल्ट MODE_RUN नहीं (एस्केप कोड से बचने के लिए)?
उत्तर, ऐसा प्रतीत होता है, नहीं है: यदि हम MODE_EDIT (-e विकल्प, पंक्ति 361) या MODE_CHECK (-l विकल्प, पंक्तियाँ 423 और 519) सेट करते हैं, तो parse_args() "valid_flags" से MODE_SHELL को हटा देता है (पंक्तियाँ 363 और 424) और यदि हम MODE_SHELL जैसा अमान्य फ़्लैग निर्दिष्ट करते हैं तो त्रुटि के साथ बाहर निकलता है (पंक्तियाँ 532-533):
लेकिन हमें एक खामी मिली: यदि हम "sudo" के बजाय "sudoedit" के रूप में Sudo निष्पादित करते हैं, तो parse_args() स्वचालित रूप से MODE_EDIT सेट करता है (पंक्ति 270) लेकिन "valid_flags" को रीसेट नहीं करता है, और "valid_flags" में डिफ़ॉल्ट रूप से MODE_SHELL शामिल होता है (पंक्तियाँ 127 और 249):
परिणामस्वरूप, यदि हम "sudoedit -s" निष्पादित करते हैं, तो हम MODE_EDIT और MODE_SHELL दोनों सेट करते हैं (लेकिन MODE_RUN नहीं), हम एस्केप कोड से बचते हैं, संवेदनशील कोड तक पहुँचते हैं, और एक कमांड-लाइन तर्क के माध्यम से हीप-आधारित बफर "user_args" को ओवरफ्लो करते हैं जो एक एकल बैकस्लैश वर्ण के साथ समाप्त होता है:
perl -e 'print "A" x 65536'
malloc(): corrupted top size
Aborted (core dumped)आक्रमणकारी के दृष्टिकोण से, यह बफर ओवरफ्लो आदर्श है:
हम उस "user_args" बफर के आकार को नियंत्रित करते हैं जिसे हम ओवरफ्लो करते हैं (हमारे संयोजित कमांड-लाइन तर्कों का आकार, पंक्तियाँ 852-854);
हम स्वतंत्र रूप से ओवरफ्लो के आकार और सामग्री को नियंत्रित करते हैं (हमारा अंतिम कमांड-लाइन तर्क सुविधाजनक रूप से हमारे पहले पर्यावरण चरों के बाद आता है, जो पंक्तियों 852-853 में आकार गणना में शामिल नहीं हैं);
हम उस बफर में null बाइट्स भी लिख सकते हैं जिसे हम ओवरफ्लो करते हैं (प्रत्येक कमांड-लाइन तर्क या पर्यावरण चर जो एक एकल बैकस्लैश के साथ समाप्त होता है, "user_args" में एक null बाइट लिखता है, पंक्तियाँ 866-868)।
उदाहरण के लिए, amd64 Linux पर, निम्न कमांड एक 24-बाइट "user_args" बफर (एक 32-बाइट हीप चंक) आवंटित करता है और अगले चंक के आकार क्षेत्र को "A=a\0B=b\0" (0x00623d4200613d41), उसके fd क्षेत्र को "C=c\0D=d\0" (0x00643d4400633d43), और उसके bk क्षेत्र को "E=e\0F=f\0" (0x00663d4600653d45) से अधिलेखित कर देता है:
--|--------+--------+--------+--------|--------+--------+--------+--------+-- | | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.| --|--------+--------+--------+--------|--------+--------+--------+--------+-- size <---- user_args buffer ----> size fd bk
चूँकि Sudo अपने main() फ़ंक्शन की शुरुआत में स्थानीयकरण फ़ंक्शनों को कॉल करता है:
और अनुवाद स्ट्रिंग्स को (gettext() फ़ंक्शन और _() मैक्रो के माध्यम से) प्रारूप-स्ट्रिंग फ़ंक्शनों जैसे निम्न को पास करता है:
हम शुरू में halfdog की आकर्षक तकनीक को पुनः उपयोग करना चाहते थे, जो https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ पर उपलब्ध है, और Sudo के हीप-आधारित बफर ओवरफ्लो को प्रारूप-स्ट्रिंग शोषण में बदलना चाहते थे। अधिक स्पष्ट रूप से:
पंक्ति 154 पर, setlocale() में, हम कई LC पर्यावरण चरों (LC_CTYPE, LC_MESSAGES, LC_TIME, आदि) को malloc()ate और free() करते हैं, जिससे Sudo के हीप की शुरुआत में छोटे छिद्र (free fast या tcache chunks) बन जाते हैं;
पंक्ति 155 पर, bindtextdomain() एक struct binding को malloc()ate करता है, जिसमें एक dirname पॉइंटर होता है जो उस निर्देशिका के नाम की ओर इंगित करता है जिसमें ".mo" कैटलॉग फ़ाइलें और इसलिए अनुवाद स्ट्रिंग्स होती हैं;
set_cmnd() में, हम "user_args" बफर को Sudo के हीप की शुरुआत के छिद्रों में से एक में malloc()ate करते हैं, और इस बफर को ओवरफ्लो करते हैं, जिससे struct binding का dirname पॉइंटर अधिलेखित हो जाता है;
पंक्ति 301 पर (उदाहरण के लिए), gettext() (_() मैक्रो के माध्यम से) अधिलेखित dirname से हमारी अपनी अनुवाद स्ट्रिंग लोड करता है -- दूसरे शब्दों में, हम sudo_printf() को पारित प्रारूप स्ट्रिंग को नियंत्रित करते हैं।
इस प्रारंभिक तकनीक को लागू करने के लिए, हमने एक प्रारंभिक ब्रूट-फोर्सर लिखा जो gdb के अंदर Sudo को निष्पादित करता है, "user_args" बफर को ओवरफ्लो करता है, और निम्नलिखित मापदंडों को यादृच्छिक रूप से चुनता है:
वे LC पर्यावरण चर जिन्हें हम Sudo को पास करते हैं, और उनकी लंबाई (हम "C.UTF-8" लोकेल का उपयोग करते हैं और एक यादृच्छिक "@modifier" जोड़ते हैं);
"user_args" बफर का आकार जिसे हम ओवरफ्लो करते हैं;
ओवरफ्लो का आकार स्वयं;
क्या हम Sudo के प्रमाणीकरण कोड (-A या -n विकल्प) से गुजरते हैं या नहीं (-u #realuid विकल्प)।
दुर्भाग्य से, यह प्रारंभिक तकनीक विफल रही; हमारा ब्रूट-फोर्सर struct binding के dirname पॉइंटर को अधिलेखित करने में सक्षम था:
Program received signal SIGSEGV, Segmentation fault.
0x00007f6e0dde1ea9 in __dcigettext (domainname=domainname@entry=0x7f6e0d9cc020 "sudoers", msgid1=msgid1@entry=0x7f6e0d9cc014 "user NOT in sudoers", msgid2=msgid2@entry=0x0, plural=plural@entry=0, n=n@entry=0, category=5) at dcigettext.c:619
=> 0x7f6e0dde1ea9 <__dcigettext+1257>: cmpb $0x2f,(%rax)
लेकिन LC_MESSAGES हमेशा डिफ़ॉल्ट "C" लोकेल था ("C.UTF-8" नहीं), जो gettext() में स्ट्रिंग अनुवाद को अक्षम कर देता है (अर्थात, gettext() मूल प्रारूप स्ट्रिंग लौटाता है, हमारी अपनी नहीं)।
हालाँकि, सौभाग्य से, हमारे ब्रूट-फोर्सर ने दर्जनों अद्वितीय Sudo क्रैश और gdb बैकट्रेस उत्पन्न किए; इनमें से तीन ने हमारा ध्यान आकर्षित किया, और हमने अंततः तीनों का शोषण किया।
पहला क्रैश जिसने हमारा ध्यान आकर्षित किया वह है:
Program received signal SIGSEGV, Segmentation fault.
0x000056291a25d502 in process_hooks_getenv (name=name@entry=0x7f4a6d7dc046 "SYSTEMD_BYPASS_USERDB", value=value@entry=0x7ffc595cc240) at ../../src/hooks.c:108
=> 0x56291a25d502 <process_hooks_getenv+82>: callq *0x8(%rbx)
rbx 0x56291c1df2b0 94734565372592
अविश्वसनीय रूप से, Sudo का फ़ंक्शन process_hooks_getenv() (पंक्ति 108 पर) क्रैश हो गया क्योंकि हमने सीधे एक फ़ंक्शन पॉइंटर, getenv_fn (हीप-आधारित struct sudo_hook_entry का एक सदस्य) को अधिलेखित कर दिया:
इस struct sudo_hook_entry अधिलेखन का शोषण करने के लिए, हम ध्यान देते हैं कि:
getenv_fn को कॉल (पंक्ति 108 पर) execve() को कॉल करने के साथ संगत है:
. name ("SYSTEMD_BYPASS_USERDB") execve() के pathname तर्क के साथ संगत है;
. &val (एक NULL पॉइंटर की ओर संकेत) execve() के argv के साथ संगत है;
. hook->closure (एक NULL पॉइंटर) execve() के envp के साथ संगत है;
हम फ़ंक्शन पॉइंटर getenv_fn को आंशिक रूप से अधिलेखित करके ASLR को पराजित कर सकते हैं (जो साझा लाइब्रेरी sudoers.so में फ़ंक्शन sudoers_hook_getenv() की ओर इंगित करता है); और सौभाग्य से, sudoers.so की शुरुआत में execve() (या execv()) के लिए एक कॉल होता है:
परिणामस्वरूप, हम निम्नलिखित रणनीति अपनाते हैं:
हमने इस पहले शोषण का सफलतापूर्वक Ubuntu 20.04 पर परीक्षण किया।
दूसरा क्रैश जिसने हमारा ध्यान आकर्षित किया वह है:
Program received signal SIGSEGV, Segmentation fault.
0x00007f6bf9c294ee in nss_load_library (ni=ni@entry=0x55cf1a1dd040) at nsswitch.c:344
=> 0x7f6bf9c294ee <nss_load_library+46>: cmpq $0x0,0x8(%rbx)
glibc का फ़ंक्शन nss_load_library() (पंक्ति 344 पर) क्रैश हो गया क्योंकि हमने पॉइंटर "library" को अधिलेखित कर दिया, जो हीप-आधारित struct service_user का एक सदस्य है:
हम इस struct service_user अधिलेखन को आसानी से मनमाना कोड निष्पादन में बदल सकते हैं:
हम ni->library को NULL पॉइंटर से अधिलेखित करते हैं, ताकि पंक्तियों 330-342 के ब्लॉक में प्रवेश करें, पंक्ति 344 पर क्रैश से बचें, और पंक्तियों 344-359 के ब्लॉक में प्रवेश करें;
हम ni->name (वर्णों की एक सरणी, प्रारंभ में "systemd") को "X/X" से अधिलेखित करते हैं;
पंक्तियाँ 353-357 साझा लाइब्रेरी "libnss_X/X.so.2" का नाम निर्मित करती हैं ("libnss_systemd.so.2" के बजाय);
------------------------------------------------------------------------- at line 359, we load our own shared library "libnss_X/X.so.2" from the current working directory and execute our _init() constructor as root.
We successfully tested this second exploit on Ubuntu 20.04, Debian 10, and Fedora 33.
Our third exploit is not derived from one of Sudo's crashes, but from a casual observation: during our brute-force, Sudo created dozens of new directories in our current working directory (AAAAAA, AAAAAAAAA, etc). Each of these directories belongs to root and contains only one small file, named after our own user: Sudo's timestamp file -- we evidently overwrote def_timestampdir, the name of Sudo's timestamp directory.
If we overwrite def_timestampdir with the name of a directory that does not already exist, then we can race against Sudo's ts_mkdirs(), create a symlink to an arbitrary file, and:
3a/ either chown() this arbitrary file to user root and group root;
3b/ or open (or create) this arbitrary file as root, and write a struct timestamp_entry to it.
We were unable to transform 3a/ into full root privileges (for example, if we chown() our own SUID binary to root, then the kernel automatically removes our binary's SUID bit). If you, dear reader, find a solution to this problem, please post it to the public oss-security mailing list!
Eventually, we were able to transform 3b/ into full root privileges, but we initially faced two problems:
Sudo's timestamp_open() deletes our arbitrary symlink if the file it points to is older than boot time. We were able to solve this first problem by creating a very old timestamp file (from the Unix epoch), by waiting until timestamp_open() deletes it, and by racing against timestamp_open() to create our final, arbitrary symlink.
We do not control the contents of the struct timestamp_entry that is written to the arbitrary file. To the best of our knowledge, we only control three bytes (a process ID or a struct timespec), and we were unable to transform this three-byte write into full root privileges. If you, dear reader, find a solution to this problem, please post it to the public oss-security mailing list!
However, we were able to circumvent this second problem by abusing a minor bug in Sudo's timestamp_lock(). If we win the two races against ts_mkdirs() and timestamp_open(), and if our arbitrary symlink points to /etc/passwd, then this file is opened as root, and:
at line 644, the first 0x38 bytes of /etc/passwd ("root❌0:0:...") are read into a stack-based struct timestamp_entry, entry;
at line 652, entry.type is 0x783a (":x"), not TS_LOCKEXCL;
at lines 657 and 318, entry->size bytes from the stack-based entry are written to /etc/passwd, but entry->size is actually 0x746f ("ot"), not sizeof(struct timestamp_entry).
As a result, we write the entire contents of Sudo's stack to /etc/passwd (including our command-line arguments and our environment variables): we inject an arbitrary user into /etc/passwd and therefore obtain full root privileges. We successfully tested this third exploit on Ubuntu 20.04.
Note: this minor bug in timestamp_lock() was fixed in January 2020 by commit 586b418a, but this fix was not backported to legacy versions.
We thank Todd C. Miller for his professionalism, quick response, and meticulous attention to every detail in our report. We also thank the members of distros@openwall.
2021-01-13: Advisory sent to Todd.Miller@sudo.
2021-01-19: Advisory and patches sent to distros@openwall.
2021-01-26: Coordinated Release Date (6:00 PM UTC).