
استغلال لثغرة استخدام بعد التحرير في Exim (CVE-2020-28018) يحقق تنفيذ كود عن بُعد عبر بدائيات إفساد الذاكرة، بما في ذلك القراءة/الكتابة الاعتباطية وتسريب الكومة، مع سلسلة تصعيد صلاحيات محلية اختيارية.
توجد ثغرة من نوع Use-after-free (UAF) في tls-openssl.c تسمح للمهاجمين غير المصادق عليهم عن بُعد بإفساد بيانات الذاكرة الداخلية، وبالتالي تحقيق تنفيذ برمجيات عن بُعد في النهاية.
الأوليات:
من خلال استخدام كل هذه الأوليات معًا في سلسلة واحدة، يمكن تجاوز جميع الحمايات (mitigations) المتاحة للاستغلال بالكامل، والوصول في النهاية إلى تنفيذ برمجيات عن بُعد بصلاحية مستخدم exim.
تم نشر هذه الثغرة ضمن قائمة ضخمة من الثغرات؛ يربط التقرير الرسمي من Qualys ثغرة Use-After-Free مع CVE-2020-28008 لتنفيذ تصعيد صلاحيات محلي (LPE) بعد تحقيق RCE.
يجب أن يكون exim مُعدًا / مُجمَّعًا بالطريقة التالية:
X_PIPE_CONNECTيمكنك استخدام السكربت checker.py للتحقق مما إذا كان الخادم البعيد يعمل بإصدار متأثر، ويتوفر فيه بعض المتطلبات اللازمة ليكون قابلاً للاستغلال.
[!] لا يقوم checker.py باستغلال الثغرة، بل يتحقق فقط من الإصدار المتأثر وما إذا كان كل من PIPELINING وTLS مفعّلين. هذا يعني أن الفاحص لا يتحقق من وجود التصحيح، وبالتالي قد يُنتج نتائج إيجابية خاطئة.
كما نعرف بالفعل، توجد الثغرة في tls-openssl.c.
/*************************************************
* Write bytes down TLS channel *
*************************************************/
/*
Arguments:
ct_ctx client context pointer, or NULL for the one global server context
buff buffer of data
len number of bytes
more further data expected soon
Returns: the number of bytes after a successful write,
-1 after a failed write
Used by both server-side and client-side TLS.
*/
int
tls_write(void * ct_ctx, const uschar *buff, size_t len, BOOL more)
{
int outbytes, error, left;
SSL * ssl = ct_ctx ? ((exim_openssl_client_tls_ctx *)ct_ctx)->ssl : server_ssl;
static gstring * corked = NULL;
DEBUG(D_tls) debug_printf("%s(%p, %lu%s)\n", __FUNCTION__,
buff, (unsigned long)len, more ? ", more" : "");
/* Lacking a CORK or MSG_MORE facility (such as GnuTLS has) we copy data when
"more" is notified. This hack is only ok if small amounts are involved AND only
one stream does it, in one context (i.e. no store reset). Currently it is used
for the responses to the received SMTP MAIL , RCPT, DATA sequence, only. */
/*XXX + if PIPE_COMMAND, banner & ehlo-resp for smmtp-on-connect. Suspect there's
a store reset there. */
if (!ct_ctx && (more || corked))
{
#ifdef EXPERIMENTAL_PIPE_CONNECT
int save_pool = store_pool;
store_pool = POOL_PERM;
#endif
corked = string_catn(corked, buff, len);
#ifdef EXPERIMENTAL_PIPE_CONNECT
store_pool = save_pool;
#endif
if (more)
return len;
buff = CUS corked->s;
len = corked->ptr;
corked = NULL;
}
for (left = len; left > 0;)
{
DEBUG(D_tls) debug_printf("SSL_write(%p, %p, %d)\n", ssl, buff, left);
outbytes = SSL_write(ssl, CS buff, left);
error = SSL_get_error(ssl, outbytes);
DEBUG(D_tls) debug_printf("outbytes=%d error=%d\n", outbytes, error);
switch (error)
{
case SSL_ERROR_SSL:
ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
log_write(0, LOG_MAIN, "TLS error (SSL_write): %s", ssl_errstring);
return -1;
case SSL_ERROR_NONE:
left -= outbytes;
buff += outbytes;
break;
case SSL_ERROR_ZERO_RETURN:
log_write(0, LOG_MAIN, "SSL channel closed on write");
return -1;
case SSL_ERROR_SYSCALL:
log_write(0, LOG_MAIN, "SSL_write: (from %s) syscall: %s",
sender_fullhost ? sender_fullhost : US"<unknown>",
strerror(errno));
return -1;
default:
log_write(0, LOG_MAIN, "SSL_write error %d", error);
return -1;
}
}
return len;
}
smtp_setup_msg() هي الوظيفة الرئيسية التي تقوم بقراءة الرسالة من العميل.
في حالات معيّنة، يتم استدعاء smtp_reset()، والتي تقوم بتنظيف جميع المخازن المؤقتة والقيم.
يمكن أن يحدث هذا في حالات مثل:
HELO/EHLOSTARTTLSRSETsmtp_setup_msg()في نهاية smtp_reset()، يتم استدعاء store_reset().
store_reset هو ماكرو يغلّف الدالة store_reset_3().
دوال store هي مجرد دوال تدير الذاكرة الديناميكية.
يستخدم Exim مُخصِّص ذاكرة بالتجميع (pool allocator) على الكتل التي يتم الحصول عليها من malloc.
هناك أيضًا وظيفة مثيرة للاهتمام، وهي تنفيذ لسلسلة قابلة للنمو.
هيكل gstring:
typedef struct gstring {
int size; /* Current capacity of string memory */
int ptr; /* Offset at which to append further chars */
uschar * s; /* The string memory */
} gstring;
عند الحاجة إلى مساحة أكبر لدمج سلسلة جديدة، يتم استدعاء gstring_grow().
تحاول هذه الدالة أولاً استدعاء store_extend_3()، والتي تحاول توسيع الذاكرة داخل نفس كتلة التجمع.
يمكن أن يكون ذلك مفيدًا عندما يكون طول الإدخال غير معروف، لكن إذا تم تخصيص المزيد من الذاكرة بعدها، فلن نتمكن من توسيعها.
بعد ذلك تستدعي gstring_grow() الدالة store_newblock_3() التي تُعيد ذاكرة جديدة وتنسخ البايتات الموجودة في الذاكرة القديمة إلى الجديدة.
ثم يتم استعادة المؤشر g->s من gstring_catn().
في الدالة tls_write()، نرى متغيرًا من نوع BOOL يُسمى more.
يشير إلى ما إذا كانت هناك بيانات إضافية يجب نسخها إلى المخزن النصي قبل إعادة البيانات إلى المستخدم.
إذا كان الأمر كذلك، فلا يتم تعيين المؤشر إلى NULL.
إذا لم يكن كذلك، فسيتم إرجاع البيانات الموجودة في المخزن النصي إلى المستخدم.
تفتح هذه الوظيفة بعض الطرق المثيرة لتحفيز ثغرة Use-After-Free.
أولاً، يتم تخزين المؤشر إلى هيكل gstring في متغير ثابت (static)، مما يعني أننا سنتمكن من استخدامه في استدعاءات tls_write() اللاحقة.
كيف يمكننا تحرير المخزن المؤقت ثم استخدامه؟
نحتاج إلى جعل smtp_setup_msg() تستدعي smtp_reset() بينما لا يزال أحد المخازن المؤقتة لدينا في server_corked (لم يتم تعيينه إلى NULL).
بعد إعادة الضبط، إذا قمنا باستدعاء tls_write() بأي شكل، فسيظل المؤشر موجودًا، وبالتالي يسمح لنا باستخدامه بعد تحرير الذاكرة.
تعمل smtp_reset() على تحرير جميع ذاكرة POOL_MAIN، التي تحتوي على المخزن المؤقت الخاص بنا.
للتحكم في ثغرة Use-After-Free، نحتاج أولاً إلى تهيئة اتصال جديد.
بما أننا نريد استغلال tls_write()، نحتاج أولاً إلى بدء جلسة TLS جديدة.
لذا نرسل أولاً أمر EHLO، يليه STARTTLS لبدء اتصال TLS.
ثم لجعل more تساوي 1، نُرسل أمرًا عبر خط الأنابيب (pipeline)، وسيكون الأمر الأخير هو نصف أمر NOOP.
نغلق اتصال TLS ونرسل بقية أمر NOOP.
نرسل الآن EHLO مرة أخرى، مما سيؤدي إلى استدعاء smtp_reset وتحرير المخزن المؤقت الخاص بنا.
نحتاج الآن إلى بدء اتصال TLS آخر لنتمكن من استخدام tls_write() مرة أخرى.
نرسل STARTTLS.
الآن، إرسال أي أمر إلى الخادم سيؤدي إلى استدعاء tls_write() لإرجاع استجابة.
لكن... لا يزال server_corked يحتوي على مؤشر إلى مكان ما في الذاكرة المحررة.
وقد يتم استخدام تلك البيانات بواسطة دوال أخرى لأنها محررة... لذلك سيتعرض هيكل gstring الخاص بنا للتلف ببيانات ثنائية عشوائية.
هذه نتيجة تحفيز UAF:
gef➤ p *corked
$1 = {
size = 0x54595c9c,
ptr = 0xa7e800ba,
s = 0x7e35043433160bd3 <error: Cannot access memory at address 0x7e35043433160bd3>
}
gef➤ p corked
$2 = (gstring *) 0x555ad3be1b58
gef➤
يكون الهيكل بهذا الشكل تمامًا عند الدخول إلى tls_write() للأمر الذي يلي STARTTLS.
من الواضح أنه عند محاولة الوصول إلى corked->s، يحدث انقطاع SIGSEGV.
كما ذكرت Qualys، يستخدمون ثلاث خطوات لاستغلال الثغرة: