Skip to content
KitploitKITPLOIT
ツールエクスプロイトブログ
Log in
提出
ツールエクスプロイトブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

フィードお問い合わせプライバシー© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
CVE-2020-28018 — Exim の Use-After-Free (CVE-2020-28018) に対するエクスプロイト。メモリ破壊プリミティブによるリモートコード実行を実現し、任意読み書きとヒープリークを含み、オプションでローカル権限昇格チェーンを備えています。 | Kitploit
ツール/GitHubGitHub/dorkerdevil/cve-2020-28018
特権昇格メモリフォレンジック脆弱性分析エクスプロイトリモートアクセスツールペイロード開発バイナリエクスプロイト
GitHubdorkerdevil/cve-2020-28018

CVE-2020-28018

Exim の Use-After-Free (CVE-2020-28018) に対するエクスプロイト。メモリ破壊プリミティブによるリモートコード実行を実現し、任意読み書きとヒープリークを含み、オプションでローカル権限昇格チェーンを備えています。

リポジトリを見る
71125年前未レビュー

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有

CVE-2020-28018: EximのUse-after-free (UAF) によるRCE

はじめに

tls-openssl.c にUse-after-free (UAF) の脆弱性が存在し、リモートの認証されていない攻撃者が内部メモリデータを破損させ、最終的にリモートコード実行を達成することを可能にします。

プリミティブ:

  • メモリリーク
  • 任意読み取りプリミティブ
  • Write-What-Whereプリミティブ

これらすべてのプリミティブを連鎖させることで、利用可能なすべてのエクスプロイト緩和策を完全にバイパスし、最終的にeximユーザーとしてリモートコード実行に至ることが可能です。

この脆弱性は多数の脆弱性リストの中に公開されており、公式のQualysレポートでは、RCEが達成された後、CVE-2020-28008と連鎖させてローカル権限昇格(LPE)を実行しています。

前提条件

Eximは以下のように設定・コンパイルされている必要があります:

  • TLSが有効
  • OpenSSLが使用されている(GnuTLSではない)
  • Eximが脆弱なバージョンのいずれかである
  • X_PIPE_CONNECT が無効

checker.py スクリプトを使用して、リモートサーバーが脆弱なバージョンであり、エクスプロイトに必要な要件を満たしているかを確認できます。

[!] checker.py は脆弱性をトリガーせず、脆弱なバージョンかどうか、PIPELININGとTLSが有効かを確認するだけです。つまり、このチェッカーはパッチの適用有無を確認しないため、誤検出が発生する可能性があります。

脆弱性コード

すでにご存知の通り、脆弱性は tls-openssl.c にあります。

/*************************************************
*         Write bytes down TLS channel           *
*************************************************/

/*
Arguments:
  ct_ctx    client context pointer, or NULL for the one global server context
  buff      buffer of data
  len       number of bytes
  more	    further data expected soon

Returns:    the number of bytes after a successful write,
            -1 after a failed write

Used by both server-side and client-side TLS.
*/

int
tls_write(void * ct_ctx, const uschar *buff, size_t len, BOOL more)
{
int outbytes, error, left;
SSL * ssl = ct_ctx ? ((exim_openssl_client_tls_ctx *)ct_ctx)->ssl : server_ssl;
static gstring * corked = NULL;

DEBUG(D_tls) debug_printf("%s(%p, %lu%s)\n", __FUNCTION__,
  buff, (unsigned long)len, more ? ", more" : "");

/* Lacking a CORK or MSG_MORE facility (such as GnuTLS has) we copy data when
"more" is notified.  This hack is only ok if small amounts are involved AND only
one stream does it, in one context (i.e. no store reset).  Currently it is used
for the responses to the received SMTP MAIL , RCPT, DATA sequence, only. */
/*XXX + if PIPE_COMMAND, banner & ehlo-resp for smmtp-on-connect. Suspect there's
a store reset there. */

if (!ct_ctx && (more || corked))
  {
#ifdef EXPERIMENTAL_PIPE_CONNECT
  int save_pool = store_pool;
  store_pool = POOL_PERM;
#endif

  corked = string_catn(corked, buff, len);

#ifdef EXPERIMENTAL_PIPE_CONNECT
  store_pool = save_pool;
#endif

  if (more)
    return len;
  buff = CUS corked->s;
  len = corked->ptr;
  corked = NULL;
  }

for (left = len; left > 0;)
  {
  DEBUG(D_tls) debug_printf("SSL_write(%p, %p, %d)\n", ssl, buff, left);
  outbytes = SSL_write(ssl, CS buff, left);
  error = SSL_get_error(ssl, outbytes);
  DEBUG(D_tls) debug_printf("outbytes=%d error=%d\n", outbytes, error);
  switch (error)
    {
    case SSL_ERROR_SSL:
      ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
      log_write(0, LOG_MAIN, "TLS error (SSL_write): %s", ssl_errstring);
      return -1;

    case SSL_ERROR_NONE:
      left -= outbytes;
      buff += outbytes;
      break;

    case SSL_ERROR_ZERO_RETURN:
      log_write(0, LOG_MAIN, "SSL channel closed on write");
      return -1;

    case SSL_ERROR_SYSCALL:
      log_write(0, LOG_MAIN, "SSL_write: (from %s) syscall: %s",
	sender_fullhost ? sender_fullhost : US"<unknown>",
	strerror(errno));
      return -1;

    default:
      log_write(0, LOG_MAIN, "SSL_write error %d", error);
      return -1;
    }
  }
return len;
}

smtp_setup_msg() は、クライアントからのメッセージ読み取りを実行する主要関数です。

特定の状況では、smtp_reset() が呼び出され、すべてのバッファと値のクリーンアップが行われます。

これは以下のような状況で発生します:

  • HELO/EHLO 受信時
  • STARTTLS 受信時
  • RSET 受信時
  • smtp_setup_msg() の開始時

smtp_reset() の最後で、store_reset() が呼び出されます。

store_reset は store_reset_3() 関数をラップするマクロです。

store 関数は動的メモリを管理する単なる関数です。

Exim は malloc から取得したブロックに対してプールアロケータを使用します。

また、拡張可能な文字列実装という興味深い機能もあります。

gstring 構造体:

typedef struct gstring {
  int   size;           /* Current capacity of string memory */
  int   ptr;            /* Offset at which to append further chars */
  uschar * s;           /* The string memory */
} gstring;

新しい文字列を連結するためにより多くのスペースが必要な場合、gstring_grow() を呼び出します。

この関数はまず store_extend_3() を呼び出そうとします。この関数は同じプールブロック内でメモリを拡張しようとします。

入力の長さが不明な場合に便利ですが、その後にさらにメモリが割り当てられていると拡張できません。

次に gstring_grow() は store_newblock_3() を呼び出し、新しいメモリを返し、以前のメモリのバイトを新しいものにコピーします。

その後、g->s ポインタが gstring_catn() から復元されます。

関数 tls_write() では、more という BOOL 変数があります。

これは、データをユーザーに返す前に、文字列バッファにコピーするデータがまだあるかどうかを示します。

ある場合、ポインタは NULL にされません。

ない場合、文字列バッファに含まれるデータがユーザーに返されます。

この機能により、Use-after-Free をトリガーするための興味深い方法がいくつか開かれます。

まず、gstring 構造体へのポインタは静的変数に格納されます。これは、将来 tls_write() を呼び出す際に使用できることを意味します。

どのようにしてバッファを解放した後も使用できるようにするのでしょうか?

server_corked が(NULL にされていない)状態で smtp_setup_msg() に smtp_reset() を呼び出させる必要があります。

リセット後、何らかの方法で tls_write() を呼び出すと、ポインタはまだ存在するため、メモリが解放された後も使用することができます。

smtp_reset() は、バッファが含まれている POOL_MAIN のすべてのメモリを解放します。

Use-After-Free のトリガー

Use-After-Free を制御するには、まず新しい接続を初期化する必要があります。

tls_write() を悪用したいので、まず新しい TLS セッションを開始する必要があります。

そのため、最初に EHLO コマンドを送信し、続いて STARTTLS を送信して TLS 接続を開始します。

次に、more を 1 にするために、コマンドをパイプライン化し、最後のコマンドを NOOP の半分にします。

TLS 接続を閉じ、NOOP コマンドの残りを送信します。

ここで再び EHLO を送信すると、smtp_reset が呼び出され、バッファが解放されます。

次に、再び tls_write() を使用できるように、別の TLS 接続を開始する必要があります。

STARTTLS を送信します。

これで、サーバーに任意のコマンドを送信すると、応答を返すために tls_write() が呼び出されます。

しかし... server_corked には、解放されたメモリ上のどこかを指すポインタがまだ含まれています。

そして、そのメモリは解放されているため、他の関数によって使用される可能性があり、gstring 構造体はランダムなバイナリデータで破損します。

これが UAF をトリガーした結果です:

gef➤  p *corked
$1 = {
  size = 0x54595c9c, 
  ptr = 0xa7e800ba, 
  s = 0x7e35043433160bd3 <error: Cannot access memory at address 0x7e35043433160bd3>
}
gef➤  p corked
$2 = (gstring *) 0x555ad3be1b58
gef➤  

この構造体は、STARTTLS に続くコマンドで tls_write() に入った直後の状態です。

明らかに、corked->s にアクセスしようとすると SIGSEGV 割り込みが発生します。

エクスプロイト

Qualys が述べているように、脆弱性を悪用するには3つのステップがあります:

  1. メモリがすでに解放されているため、header_line などの構造体からヒープポインタをバッファに書き込ませることができます。そうすれば tls_write() が呼び出されたときに、そのデータがユーザーに返されます。これにより、エクスプロイトを続行するためのメモリリークが得られます。
  2. ヒープメモリアドレスがわかったら、任意読み取りプリミティブを作成し、Exim 設定が見つかるまでヒープを読み取ります。
  3. 最後のステップは、write-what-where プリミティブを作成することです。これにより、ステップ2で見つけたバッファにカスタム設定を注入できます。${run{<command>}} を注入します。ここで <command> は攻撃者が実行したい任意のコマンド(netcat を使用したリバースシェルなど)です。この設定は string_expand() によって解釈され、最終的にコマンドが実行されます。

Use-After-Free 条件の制御

Use-After-Free のトリガーに成功しました。

次に、プリミティブを正常かつ確実に作成するために、UAF を適切に制御する必要があります。

残念ながら、POOL_MAIN のバッファが解放された後、ブロックは直接 free() に渡されます。

つまり、メモリは store_get_3() や store_newblock_3() だけでなく、malloc() を使用する任意の関数(CRYPTO_zalloc() など)からもアクセスされる可能性があります。

この場合、tls_server_start() のどこかで、malloc() を通じてメモリが要求されます。

そして、そこにバイナリデータがコピーされ、gstring 構造体が破損します。

これを防ぐ方法を見つけ、有効なメモリアドレスを指す健全な gstring 構造体で tls_write() に到達できるようにする必要があります。そうしないと、SIGSEGV 割り込みが発生します。

Exim プールアロケータの動作を理解し、デバッグを行い、いくつかのコマンドを試してヒープ側での動作を確認した結果、このデータが gstring 構造体に書き込まれるのを回避できるようになりました。

メモリリーク

Use-After-Free が正常にトリガーされ、構造体が破損されていない状態になったら、関数がヒープアドレスを文字列の途中(g->ptr より前の任意の位置)に書き込むようにヒープを移動させる必要があります。

幸運なことに、応答はプレーンテキスト(バイナリプロトコルではない)ですが、クライアントに NULL バイトを送信することができます。

なぜこれが可能なのでしょうか?

応答は SSL_write() で送り返されますが、NULL バイトに問題はありません。

文字列はどうでしょうか? string_catn() は NULL バイトをカットしません。データのコピーに memcpy を使用するからです。

制限を設定する唯一の方法は g->ptr ですが、アドレスは g->ptr インデックスの前に書き込まれるため、そのインデックスまでのすべてのデータが返され、貴重なヒープアドレスが漏洩します。

PoC でメモリリークを行った結果:

メモリリーク

任意読み取り

これでヒープベースが明らかになりました...

そして、アドレスは接続間で変化しません... そのため、RCE への道を開始できます。

しかし、gstring 構造体をどのように上書きするのでしょうか?

Qualys のテクニックを使用すれば、非常に簡単であることがわかりました。

ESMTP は SMTP プロトコルにいくつかのものを追加し、MAIL FROM コマンドのパラメータなどがあります。

最後の STARTTLS の後に大きなパラメータを使用するだけで、構造体を上書きするのに十分です。

結果:

gef➤  p *corked
$1 = {
  size = 0x42424242, 
  ptr = 0x42424242, 
  s = 0x4242424242424242 <error: Cannot access memory at address 0x4242424242424242>
}

gstring 構造体を完全に制御できました。

次に任意読み取りプリミティブを作成します。

ツールをダウンロード