【調査】curl の CVE-2023-38545 について

過去最悪の脆弱性と自称したことで話題になった curl の CVE-2023-38545 について、ソースコードベースでどういう挙動をしていたのか解説します。

3行まとめ

  • SOCKS5プロキシハンドシェイクにおけるヒープベースのバッファオーバーフロー
  • socks5h 指定時に255文字超のホスト名を処理するとバッファを破壊する可能性
  • curl 8.4.0 で修正済、影響範囲は 7.69.0〜8.3.0

解説

SOCKS5プロキシの挙動について

do_SOCKS5() による状態遷移

SOCKS5プロキシ接続処理は do_SOCKS5() でステートマシンとして実装されています。
この関数はSOCKS5プロキシとの接続確立から最終的な接続要求送信までの一連の処理を担当し、複数回に分けて呼ばれながらハンドシェイクを進めます。

// lib/socks.c
/*
 * This function logs in to a SOCKS5 proxy and sends the specifics to the final
 * destination server.
 */
static CURLproxycode do_SOCKS5(struct Curl_cfilter *cf,
                               struct socks_state *sx,
                               struct Curl_easy *data)
{
  /*
    According to the RFC1928, section "6.  Replies". This is what a SOCK5
    replies:

        +----+-----+-------+------+----------+----------+
        |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
        +----+-----+-------+------+----------+----------+
        | 1  |  1  | X'00' |  1   | Variable |    2     |
        +----+-----+-------+------+----------+----------+

    Where:

    o  VER    protocol version: X'05'
    o  REP    Reply field:
    o  X'00' succeeded
  */
  struct connectdata *conn = cf->conn;
  unsigned char *socksreq = (unsigned char *)data->state.buffer;
  int idx;
  CURLcode result;
  CURLproxycode presult;
  bool socks5_resolve_local =
    (conn->socks_proxy.proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE;
  const size_t hostname_len = strlen(sx->hostname);
  ssize_t len = 0;
  const unsigned char auth = data->set.socks5auth;
  bool allow_gssapi = FALSE;
  struct Curl_dns_entry *dns = NULL;

  switch(sx->state) {
  case CONNECT_SOCKS_INIT:
...
    presult = socks_state_send(cf, sx, data, CURLPX_SEND_CONNECT,
                               "initial SOCKS5 request");
    if(CURLPX_OK != presult)
      return presult;
    else if(sx->outstanding) {
      /* remain in sending state */
      return CURLPX_OK;
    }
    sxstate(sx, data, CONNECT_SOCKS_READ);
    goto CONNECT_SOCKS_READ_INIT;
...

  case CONNECT_SOCKS_SEND:
...

CONNECT_SOCKS_READ_INIT:
  case CONNECT_SOCKS_READ_INIT:
    sx->outstanding = 2; /* expect two bytes */
    sx->outp = socksreq; /* store it here */

  case CONNECT_SOCKS_READ:
    presult = socks_state_recv(cf, sx, data, CURLPX_RECV_CONNECT,
                               "initial SOCKS5 response");
    if(CURLPX_OK != presult)
      return presult;
    else if(sx->outstanding) {
      /* remain in reading state */
      return CURLPX_OK;
    }
    else if(socksreq[0] != 5) {
      failf(data, "Received invalid version in initial SOCKS5 response.");
      return CURLPX_BAD_VERSION;
    }
    else if(socksreq[1] == 0) {
      /* DONE! No authentication needed. Send request. */
      sxstate(sx, data, CONNECT_REQ_INIT);
      goto CONNECT_REQ_INIT;
    }
    else if(socksreq[1] == 2) {
      /* regular name + password authentication */
      sxstate(sx, data, CONNECT_AUTH_INIT);
      goto CONNECT_AUTH_INIT;
    }
...

  default: /* do nothing! */
    break;

  case CONNECT_AUTH_INIT: {
...
  case CONNECT_AUTH_SEND:
...
  case CONNECT_AUTH_READ:
...
  case CONNECT_REQ_INIT:
...
  case CONNECT_RESOLVING:
...
  case CONNECT_RESOLVED: {
...
  case CONNECT_RESOLVE_REMOTE:
...
  case CONNECT_REQ_SEND:
...
  case CONNECT_REQ_SENDING:
...
  case CONNECT_REQ_READ:
...
  case CONNECT_REQ_READ_MORE:
...
  infof(data, "SOCKS5 request granted.");

  return CURLPX_OK; /* Proxy was successful! */
}

38行目の switch 文でステートを判定し、各ステートに適した処理を行います。
各ステートの処理では末尾に break を置かず sxstate() でステートを変更して後続の処理 (case) に goto でジャンプします(例: 49, 76, 81行目)

また、処理のために pending が入る場合は一度 CURLPX_OK を返し、再度 do_SOCKS5() を実行することで任意のステートから再開する設計になっています(例:47, 68行目)

CONNECT_SOCKS_INIT

一番最初に呼び出される初期化の部分です。

// lib/socks.c
static CURLproxycode do_SOCKS5(struct Curl_cfilter *cf,
                               struct socks_state *sx,
                               struct Curl_easy *data)
{
...
  bool socks5_resolve_local =
    (conn->socks_proxy.proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE;
  const size_t hostname_len = strlen(sx->hostname);
...

switch(sx->state) {
  case CONNECT_SOCKS_INIT:
...
    /* RFC1928 chapter 5 specifies max 255 chars for domain name in packet */
    if(!socks5_resolve_local && hostname_len > 255) {
      infof(data, "SOCKS5: server resolving disabled for hostnames of "
            "length > 255 [actual len=%zu]", hostname_len);
      socks5_resolve_local = TRUE;
    }
...
    idx = 0;
    socksreq[idx++] = 5;   /* version */
    idx++;                 /* number of authentication methods */
    socksreq[idx++] = 0;   /* no authentication */
    if(allow_gssapi)
      socksreq[idx++] = 1; /* GSS-API */
    if(sx->proxy_user)
      socksreq[idx++] = 2; /* username/password */
    /* write the number of authentication methods */
    socksreq[1] = (unsigned char) (idx - 2);

    sx->outp = socksreq;
    sx->outstanding = idx;
    presult = socks_state_send(cf, sx, data, CURLPX_SEND_CONNECT,
                               "initial SOCKS5 request");
    if(CURLPX_OK != presult)
      return presult;
    else if(sx->outstanding) {
      /* remain in sending state */
      return CURLPX_OK;
    }
    sxstate(sx, data, CONNECT_SOCKS_READ);
    goto CONNECT_SOCKS_READ_INIT;
...
}

// lib/url.c
static CURLcode parse_proxy(struct Curl_easy *data,
                            struct connectdata *conn, char *proxy,
                            curl_proxytype proxytype)
{
...
    if(strcasecompare("https", scheme)) {
      if(proxytype != CURLPROXY_HTTPS2)
        proxytype = CURLPROXY_HTTPS;
      else
        proxytype = CURLPROXY_HTTPS2;
    }
    else if(strcasecompare("socks5h", scheme))
      proxytype = CURLPROXY_SOCKS5_HOSTNAME;
    else if(strcasecompare("socks5", scheme))
      proxytype = CURLPROXY_SOCKS5;
...
}

do_SOCKS5()では、まずプロキシタイプに応じてリモートDNS解決を行うか、ローカルでDNS解決を行うかをブール値の socks5_resolve_local で判断します​(7行目)

リモートでDNS解決を行う場合 URL は socks5h:// で始まり proxytype には CURLPROXY_SOCKS5_HOSTNAME が入ります(61行目)
ローカルでDNS解決を行う場合 URL は socks5:// で始まり proxytype には CURLPROXY_SOCKS5 が入ります(63行目)
よって proxytype == CURLPROXY_SOCKS5 であればローカル解決のため socks5_resolve_local == TRUE になります。

なお、リモートでDNS解決を行う際は RFC1928 の仕様によりホスト名は 255 までと制限されているので 255以上のホスト名を指定するとリモートDNS解決(socks5h://)でもローカルDNS解決にフラグが切り替わります。後述しますが、これが今回のCVEの直接的な原因となります。

その後、クライアントはSOCKS5プロキシへ挨拶メッセージを送信します(35行目)
内容はSOCKSバージョンやサポートする認証方式などについてです。
その後、CONNECT_SOCKS_READ 状態に遷移しプロキシからの応答を待ちます。

CONNECT_SOCKS_READ

挨拶メッセージの返答を読み取ります。

  case CONNECT_SOCKS_READ_INIT:
    sx->outstanding = 2; /* expect two bytes */
    sx->outp = socksreq; /* store it here */

  case CONNECT_SOCKS_READ:
    presult = socks_state_recv(cf, sx, data, CURLPX_RECV_CONNECT,
                               "initial SOCKS5 response");
    if(CURLPX_OK != presult)
      return presult;
    else if(sx->outstanding) {
      /* remain in reading state */
      return CURLPX_OK;
    }
    else if(socksreq[0] != 5) {
      failf(data, "Received invalid version in initial SOCKS5 response.");
      return CURLPX_BAD_VERSION;
    }
    else if(socksreq[1] == 0) {
      /* DONE! No authentication needed. Send request. */
      sxstate(sx, data, CONNECT_REQ_INIT);
      goto CONNECT_REQ_INIT;
    }
    else if(socksreq[1] == 2) {
      /* regular name + password authentication */
      sxstate(sx, data, CONNECT_AUTH_INIT);
      goto CONNECT_AUTH_INIT;
    }

選択された認証方式を受け取ります
0の場合は認証不要のため CONNECT_REQ_INIT にジャンプしてリクエストの初期化に飛びます。
2の場合はユーザ名・パスワード認証が必要のため CONNECT_AUTH_INIT に飛びます。

CONNECT_AUTH_INIT

  case CONNECT_AUTH_INIT:
...
    sxstate(sx, data, CONNECT_AUTH_SEND);
    sx->outstanding = len;
    sx->outp = socksreq;

  case CONNECT_AUTH_SEND:
...
    sxstate(sx, data, CONNECT_AUTH_READ);

  case CONNECT_AUTH_READ:
...
    sxstate(sx, data, CONNECT_REQ_INIT);

認証については今回扱うCVEと無関係のため、状態遷移だけを記載します。
CONNECT_AUTH_INITCONNECT_AUTH_SENDCONNECT_AUTH_READCONNECT_REQ_INIT

認証の有無に関わらず、どちらも CONNECT_REQ_INIT にジャンプします。

CONNECT_REQ_INIT

SOCKS5の接続先要求 (CONNECT request) をプロキシに送信します。

  case CONNECT_REQ_INIT:
    if(socks5_resolve_local) {
      enum resolve_t rc = Curl_resolv(data, sx->hostname, sx->remote_port,
                                      TRUE, &dns);

      if(rc == CURLRESOLV_ERROR)
        return CURLPX_RESOLVE_HOST;

      if(rc == CURLRESOLV_PENDING) {
        sxstate(sx, data, CONNECT_RESOLVING);
        return CURLPX_OK;
      }
      sxstate(sx, data, CONNECT_RESOLVED);
      goto CONNECT_RESOLVED;
    }
    goto CONNECT_RESOLVE_REMOTE;

ここでターゲットのアドレス情報(ホスト名またはIPアドレスとポート)をプロキシに伝えます。

ホスト名をプロキシに解決させる場合(socks5h)は CONNECT_RESOLVE_REMOTE にジャンプします。
一方、ローカルで名前解決する場合(socks5)は Curl_resolv() で名前解決をして CONNECT_RESOLVED にジャンプします。

ローカルの名前解決時に PENDING が帰ってきたら CONNECT_RESOLVING に設定して一度リターンします。再実行時に Curl_fetch_addr()Curl_resolv_check() を呼び出してローカルの名前解決を完了します。break がないのでそのまま CONNECT_RESOLVED に続きます。

  case CONNECT_RESOLVING:
    /* check if we have the name resolved by now */
    dns = Curl_fetch_addr(data, sx->hostname, sx->remote_port);

    if(dns) {
      infof(data, "SOCKS5: hostname '%s' found", sx->hostname);
    }

    if(!dns) {
      result = Curl_resolv_check(data, &dns);
      if(!dns) {
        if(result)
          return CURLPX_RESOLVE_HOST;
        return CURLPX_OK;
      }
    }

  case CONNECT_RESOLVED: 

CONNECT_RESOLVED

  case CONNECT_RESOLVED: {
    char dest[MAX_IPADR_LEN] = "unknown";  /* printable address */
    struct Curl_addrinfo *hp = NULL;
    if(dns)
      hp = dns->addr;
    if(!hp) {
      failf(data, "Failed to resolve \"%s\" for SOCKS5 connect.",
            sx->hostname);
      return CURLPX_RESOLVE_HOST;
    }

    Curl_printable_address(hp, dest, sizeof(dest));

    len = 0;
    socksreq[len++] = 5; /* version (SOCKS5) */
    socksreq[len++] = 1; /* connect */
    socksreq[len++] = 0; /* must be zero */
    if(hp->ai_family == AF_INET) {
      int i;
      struct sockaddr_in *saddr_in;
      socksreq[len++] = 1; /* ATYP: IPv4 = 1 */

      saddr_in = (struct sockaddr_in *)(void *)hp->ai_addr;
      for(i = 0; i < 4; i++) {
        socksreq[len++] = ((unsigned char *)&saddr_in->sin_addr.s_addr)[i];
      }

      infof(data, "SOCKS5 connect to %s:%d (locally resolved)", dest,
            sx->remote_port);
    }
    else {
      hp = NULL; /* fail! */
      failf(data, "SOCKS5 connection to %s not supported", dest);
    }

    Curl_resolv_unlock(data, dns); /* not used anymore from now on */
    goto CONNECT_REQ_SEND;
  }

リクエストバッファ socksreq にメタデータと取得した IPアドレスをセットします(25行目)
その後 CONNECT_REQ_SEND にジャンプしリクエストを送信します。

CONNECT_RESOLVE_REMOTE

CONNECT_REQ_INIT からリモートで名前解決を行う時にジャンプする先です(socks5h://)

  case CONNECT_RESOLVE_REMOTE:
    /* Authentication is complete, now specify destination to the proxy */
    len = 0;
    socksreq[len++] = 5; /* version (SOCKS5) */
    socksreq[len++] = 1; /* connect */
    socksreq[len++] = 0; /* must be zero */

    if(!socks5_resolve_local) {
      /* ATYP: domain name = 3,
         IPv6 == 4,
         IPv4 == 1 */
      unsigned char ip4[4];
      if(1 == Curl_inet_pton(AF_INET, sx->hostname, ip4)) {
        socksreq[len++] = 1;
        memcpy(&socksreq[len], ip4, sizeof(ip4));
        len += sizeof(ip4);
      }
      else {
        socksreq[len++] = 3;
        socksreq[len++] = (char) hostname_len; /* one byte address length */
        memcpy(&socksreq[len], sx->hostname, hostname_len); /* w/o NULL */
        len += hostname_len;
      }
      infof(data, "SOCKS5 connect to %s:%d (remotely resolved)",
            sx->hostname, sx->remote_port);
    }

  case CONNECT_REQ_SEND:

IPアドレスを指定された場合は名前解決する必要がないので、そのまま入れます(15行目)
ホスト名を指定された場合、ATYP = 3 にセットして、ホスト名をコピーします(19-21行目)

break が無いため、そのまま CONNECT_REQ_SEND に入り、リクエストを送信します。

CONNECT_REQ_SEND

  case CONNECT_REQ_SEND:
...
    sxstate(sx, data, CONNECT_REQ_SENDING);

  case CONNECT_REQ_SENDING:
...
    sxstate(sx, data, CONNECT_REQ_READ);

  case CONNECT_REQ_READ:
...
      if(len > 10) {
        sx->outstanding = len - 10; /* get the rest */
        sx->outp = &socksreq[10];
        sxstate(sx, data, CONNECT_REQ_READ_MORE);
      }
      else {
        sxstate(sx, data, CONNECT_DONE);
        break;
      }

  case CONNECT_REQ_READ_MORE:
...
    sxstate(sx, data, CONNECT_DONE);

  infof(data, "SOCKS5 request granted.");
  return CURLPX_OK; /* Proxy was successful! */
}

リクエスト送信については今回扱うCVEと無関係のため、状態遷移だけを記載します。
do_SOCKS5() はこれで終わりです。

CONNECT_REQ_SENDCONNECT_REQ_SENDING CONNECT_REQ_READ (→ CONNECT_REQ_READ_MORE) → CONNECT_DONE

connect_SOCKS()

connect_SOCKS()do_SOCKS5() を呼び出す関数です。

static CURLcode connect_SOCKS(struct Curl_cfilter *cf,
                              struct socks_state *sxstate,
                              struct Curl_easy *data)
{
  CURLcode result = CURLE_OK;
  CURLproxycode pxresult = CURLPX_OK;
  struct connectdata *conn = cf->conn;

  switch(conn->socks_proxy.proxytype) {
  case CURLPROXY_SOCKS5:
  case CURLPROXY_SOCKS5_HOSTNAME:
    pxresult = do_SOCKS5(cf, sxstate, data);
    break;

  case CURLPROXY_SOCKS4:
  case CURLPROXY_SOCKS4A:
    pxresult = do_SOCKS4(cf, sxstate, data);
    break;

  default:
    failf(data, "unknown proxytype option given");
    result = CURLE_COULDNT_CONNECT;
  } /* switch proxytype */
  if(pxresult) {
    result = CURLE_PROXY;
    data->info.pxcode = pxresult;
  }

  return result;
}

socks_proxy_cf_connect()

socks_proxy_cf_connect()connect_SOCKS() を呼び出す関数です。

static CURLcode socks_proxy_cf_connect(struct Curl_cfilter *cf,
                                       struct Curl_easy *data,
                                       bool blocking, bool *done)
{
  CURLcode result;
  struct connectdata *conn = cf->conn;
  int sockindex = cf->sockindex;
  struct socks_state *sx = cf->ctx;

  if(cf->connected) {
    *done = TRUE;
    return CURLE_OK;
  }

  result = cf->next->cft->do_connect(cf->next, data, blocking, done);
  if(result || !*done)
    return result;

  if(!sx) {
    sx = calloc(sizeof(*sx), 1);
    if(!sx)
      return CURLE_OUT_OF_MEMORY;
    cf->ctx = sx;
  }

  if(sx->state == CONNECT_INIT) {
    /* for the secondary socket (FTP), use the "connect to host"
     * but ignore the "connect to port" (use the secondary port)
     */
    sxstate(sx, data, CONNECT_SOCKS_INIT);
...
  }

  result = connect_SOCKS(cf, sx, data);
  if(!result && sx->state == CONNECT_DONE) {
    cf->connected = TRUE;
    Curl_verboseconnect(data, conn);
    socks_proxy_cf_free(cf);
  }

  *done = cf->connected;
  return result;
}

static void socks_proxy_cf_free(struct Curl_cfilter *cf)
{
  struct socks_state *sxstate = cf->ctx;
  if(sxstate) {
    free(sxstate);
    cf->ctx = NULL;
  }
}

20: ステートを確保します
30: ステートを CONNECT_SOCKS_INIT にセットします
34: connect_SOCKS() を呼び出しステートマシンを進めます
35-38: CONNECT_DONE になったら cf->connected に TRUE を設定しステートを開放します。
41: CONNECT_DONE にならない限り 引数 done には FALSE が入るので、ステートマシンが完了するまでこの関数が再度呼ばれ続けます

問題となった部分

ここからが本題です。
今回のバグは「プロキシにホスト名解決を任せる(socks5h://)」設定で255バイトを超えるホスト名を扱った場合に発生します。

このバグはSOCKS5処理をブロッキング実装から非ブロッキングのステートマシン実装に変更した際に発生しました。
https://github.com/curl/curl/commit/4a4b63d

動作解説の部分で触れたとおり、ステートマシンは pending が発生すると一度リターンして再実行されるのを待つという挙動をしています。

それを踏まえて以下のコードを見てみましょう。

static CURLproxycode do_SOCKS5(struct Curl_cfilter *cf,
                               struct socks_state *sx,
                               struct Curl_easy *data)
{
  unsigned char *socksreq = (unsigned char *)data->state.buffer;
  bool socks5_resolve_local =
    (conn->socks_proxy.proxytype == CURLPROXY_SOCKS5) ? TRUE : FALSE;
  const size_t hostname_len = strlen(sx->hostname);

  switch(sx->state) {
  case CONNECT_SOCKS_INIT:
...
    /* RFC1928 chapter 5 specifies max 255 chars for domain name in packet */
    if(!socks5_resolve_local && hostname_len > 255) {
      infof(data, "SOCKS5: server resolving disabled for hostnames of "
            "length > 255 [actual len=%zu]", hostname_len);
      socks5_resolve_local = TRUE;
...
  case CONNECT_RESOLVE_REMOTE:
...
    if(!socks5_resolve_local) {
      /* ATYP: domain name = 3,
         IPv6 == 4,
         IPv4 == 1 */
      unsigned char ip4[4];
      if(1 == Curl_inet_pton(AF_INET, sx->hostname, ip4)) {
        socksreq[len++] = 1;
        memcpy(&socksreq[len], ip4, sizeof(ip4));
        len += sizeof(ip4);
      }
      else {
        socksreq[len++] = 3;
        socksreq[len++] = (char) hostname_len; /* one byte address length */
        memcpy(&socksreq[len], sx->hostname, hostname_len); /* w/o NULL */
        len += hostname_len;
      }
...
    }
  }
}

14: ホスト名が255より大きい場合、ローカルで名前解決するように socks5_resolve_local を変更します
この変更は CONNECT_SOCKS_INIT という初期化状態でのみ実行されます(1回限定)

6: 一方で pending が発生すると do_SOCKS5() 関数は一度リターンして再実行されるのを待ちます。
do_SOCKS5() が実行される度に socks5_resolve_local は最初の値にリセットされてしまいます。
ステートマシンが初期チェックを行う CONNECT_SOCKS_INIT を過ぎているため、255バイト超を再検知するチェックは行われず、コードはホスト名をそのままSOCKS5接続要求に詰め込む流れになります​。

33: hostname_len が 256 以上の場合、下位8ビット値をホスト名の長さとして設定します
(例えば300だと0x2C=44)
34: しかし実際には hostname_len バイトをバッファ socksreq にコピーします
(もし hostname_len が uint8 なら問題なかったのですが size_t = uint32 だったのでダメでした)

これによりヒープ領域のメモリ破壊が発生します。

socksreq は転送がまだ準備中なので SOCKS ネゴシエーションを送受信するために再利用された一時的なダウンロードバッファ data->state.buffer を指します(5行目)

なお、data->state.buffer のサイズについてはオプションで指定する CURLOPT_BUFFERSIZE に依存します。

// lib/multi.c
CURLcode Curl_preconnect(struct Curl_easy *data)
{
  if(!data->state.buffer) {
    data->state.buffer = malloc(data->set.buffer_size + 1);
    if(!data->state.buffer)
      return CURLE_OUT_OF_MEMORY;
  }

  return CURLE_OK;
}

// lib/setopt.c
CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
{

  switch(option) {
  case CURLOPT_BUFFERSIZE:
    /*
     * The application kindly asks for a differently sized receive buffer.
     * If it seems reasonable, we'll use it.
     */
    if(data->state.buffer)
      return CURLE_BAD_FUNCTION_ARGUMENT;

    arg = va_arg(param, long);

    if(arg > READBUFFER_MAX)
      arg = READBUFFER_MAX;
    else if(arg < 1)
      arg = READBUFFER_SIZE;
    else if(arg < READBUFFER_MIN)
      arg = READBUFFER_MIN;

    data->set.buffer_size = (unsigned int)arg;
    break;
}

#define READBUFFER_SIZE CURL_MAX_WRITE_SIZE
#define READBUFFER_MAX  CURL_MAX_READ_SIZE
#define READBUFFER_MIN  1024
#define CURL_MAX_WRITE_SIZE 16384
#define CURL_MAX_READ_SIZE (10*1024*1024)

バッファサイズはデフォルトで 16384 バイトですが、アプリケーションによって異なるサイズに設定できます。例えば curl ツールはデフォルトで 102400 バイトに設定した上で –limit-rate が1秒あたり102400バイト未満に設定されている場合は、バッファサイズをより小さいサイズに設定します。

また、URL全体については 65535 より短いのが既に保証されています。

// lib/url.c
#define MAX_URL_LEN 0xffff

static CURLcode parseurlandfillconn(struct Curl_easy *data,
                                    struct connectdata *conn)
{
...
  else if(strlen(data->state.up.hostname) > MAX_URL_LEN) {
    failf(data, "Too long host name (maximum is %d)", MAX_URL_LEN);
    return CURLE_URL_MALFORMAT;
  }
...
}

そのためバッファオーバーフローが発生するのは以下の設定を利用している場合です。

libcurl を使ってる場合: CURLOPT_BUFFERSIZE < 65535 に設定している(デフォルト値を含む)
curl ツールを使ってる場合:–limit-rate で 65535 以下に設定する

エクスプロイトについて

攻撃者はホスト名をコントロールする必要があります。
例えば、ユーザは libcurl がリダイレクトに従うように設定したとします。攻撃者はロケーションヘッダ内のホスト名を制御する必要があります。

また、先述した通りステートマシンを遅延させる必要があります。
例えば、攻撃者は SOCKS サーバーを制御し、最初のサーバー hello を遅延させます。

これらに加えて、攻撃者は data->set.buffer_size の大きさと、ヒープ内の data->state.buffer の後に何が来るかといった、メモリ配置を知る必要があります。
(攻撃者が libcurl を使用しているプログラムのコピーを持っており、同様の環境でデバッグするなど)

回避策

–proxy で socks5h:// を使わない

この脆弱性は「SOCKS5プロキシにホスト名解決を任せる」設定(socks5h://)時に255文字を超えるホスト名でバッファオーバーフローが起きるものです。
そのため、socks5://(ローカルで解決するモード)を使用すればこのバグは発生しません。

非常に長いホスト名を使用しない

このバグはホスト名が256文字以上の場合にのみ発生します。
そのため、長すぎるホスト名を curl に渡さなければ脆弱性は表面化しません。

しかし、攻撃者が意図的にcurlに長いホスト名を処理させる状況(たとえばリダイレクトやHTTPヘッダ改ざんなど)を作る可能性があるため、効果は限定的です。

バッファサイズを64KB以上にする

・libcurl で CURLOPT_BUFFERSIZE > 65535 に設定する
・curl で –limit-rate を指定する場合は 65535 未満にしない(デフォルトでは100KB)
ホスト名は65535以下と保証されているので、バッファサイズをそれ以上取ればバッファオーバーフローは発生しません。

修正パッチ

CONNECT_SOCKS_INIT でホスト名が255以上の時はローカル解決に移行せず、そのままエラーを吐くようにしました(それはそう)
https://github.com/curl/curl/commit/fb4415d

参考サイト

シェアする

  • このエントリーをはてなブックマークに追加

フォローする