32年前のバグがTelnetサーバーに侵入
A 32-Year-Old Bug Walks into a Telnet Server (labs.watchtowr.com)
要約
GNU inetutilsのTelnetdに存在する、1994年(32年前)に発見されたバッファオーバーフローの脆弱性CVE-2026-32746について解説しています。この脆弱性はLINEMODE SLCネゴシエーションハンドラに存在し、認証なしでリモートからのコード実行を可能にする可能性があります。多くのLinuxディストリビューションを含む広範なシステムに影響を与え、2005年には同様の脆弱性がクライアント側でも発見されていました。
全文翻訳
32年前のバグがTelnetサーバーに侵入 (GNU inetutils Telnetd CVE-2026-32746 認証前RCE)
この調査は、watchTowr Platformの能力の一端を示すものです。watchTowr Platformは、プロアクティブなエクスポージャー管理ソリューションであり、組織が自律的にリスクを検証・軽減し、インサイチューでの悪用前に脅威にさらされるのを防ぎます。
遠い昔、バイナリ脆弱性緩和策が存在しない時代、Unixがまだ大地を闊歩していた頃、認証前のTelnetd脆弱性が生まれました。
この脆弱性は、なんと1994年、つまりあなたよりも古いかもしれません。その時代背景を説明すると、画期的な映画『Hackers』が公開された年と同じです。
RISCがまだ遠い夢だった頃です。
もしかしたら、Zero Cool自身が生み出したものかもしれませんね?
[画像: 1994年頃のコンピュータやネットワーク機器のイメージ]
さて。最近、この脆弱性は無残にも終焉を迎えました。
これは一体何なのか?
Telnetに馴染みがない方もいるかもしれませんが、問題ありません。
Telnetは、TCP/IP経由でリモートサーバーと通信するためのコマンドラインインターフェースを提供するネットワークプロトコルです。言い換えれば、リモートコード実行サービスです。通常の設定では認証の壁があり、システムシェルにアクセスする前にログインが必要です。また、プレーンテキストで動作するため、ネットワーク経由でユーザー名とパスワードが平文で送信されます。
現在ではSSHが事実上の後継となっており、Telnetはますます一般的でなくなっています。
CVE-2026-32746とは?
DREAM Security Research Teamによって発見されたCVE-2026-32746は、BSSベースのバッファオーバーフローであり、攻撃者は約400バイトの隣接する変数を破損させることができます。
これはLINEMODE SLC(Set Linemode Characters)ネゴシエーションハンドラに存在します。厳密にはGNU inetutilsのみに影響しますが、多くのベンダーがTelnetdの実装を同じコードベースに基づいているため、影響範囲は広大で、正確な推定は困難です。これには、主要なLinuxディストリビューションのすべてが含まれることは間違いありません(我々が確認した限りでは)。
このような脆弱性があれば、インターネットが興奮で爆発すると予想していましたが、優れた分析がないまま1週間近くが経過しました。そこで、我々が到達した地点を公開することにしました。
脆弱性をどのように特定したか、攻撃者が何(そしてどのような状況下で)を実行できるか、そしてこの特定の脆弱性が予想以上にエクスプロイトするのが難しい「パンドラの箱」である理由について説明します。
しかし、まず最も明白な疑問から始めましょう。レガシーテクノロジーが存在しない魔法のユニコーンランドに住むすべての人々の唇に浮かんでいるであろう疑問です。「そもそもなぜTelnetを使うのか?!」
何が影響を受けるのか?
これは難しい問題です。パッチはinetutils-telnetdに適用されましたが、多くのフォークが存在し、長年にわたって変更が加えられ、この脆弱性がシステムからシステムへとコピー&ペーストされてきました。
我々は、少なくとも以下のシステムでこのCVEを特定しました。
- inetutils-telnetd自体
- Ubuntu
- Debian
- FreeBSD 13 / FreeBSD 15 Port
- NetBSD 10.1
- Citrix NetScaler
- Apple Mac Tahoe
- Haiku
- TrueNAS Core
- uCLinux
- libmtev
- DragonFlyBSD
[画像: 脆弱性のあるコードスニペットの例]
2026年なのに、なぜTelnet? MCPはどこ?
Telnetは古くから存在します。多くの読者は画面に向かって叫んでいるでしょう。「そんなに安全でないプロトコルを誰が使うんだ?!」
驚くかもしれませんが、古参のTelnetは本番システムに依然として存在しており、その理由は驚くほど多岐にわたります。ベンダーがサポートする唯一のものかもしれません(「このCNCマシンが故障した場合、1分あたりX千ドルのダウンタイムが発生します。そして、あなたは…SSHクライアントを追加したいのですか?!おい、それは8ビットマイクロコントローラーで動作するんだ!」)。移行が不可能な、非常に技術的な理由があるのかもしれません。
事実はこうです。主要なディストリビューションのリポジトリに強く存在していることが証明しているように、人々は依然としてTelnetを実行しています。まさに時の試練に耐えてきたのです。
[画像: Telnetがまだ使われている状況を示すグラフやアイコン]
Telnetに脆弱性? TelnetはNetcatと同じじゃないの?
一部の読者は、Telnetがプロトコルレベルでは単なるTCPストリームであるという印象を持っているかもしれません。確かに、一部の実装はその通りです。シェルに接続されたTCPソケットです。シンプルで、壊すのは不可能(?)です。
しかし、そうではありません。Telnetは、ターミナル制御(エコーをオンにする?オフにする?)、クライアントウィンドウサイズのネゴシエーション、認証、さらには暗号化といった、さまざまな機能をサポートしています。どこかで、誰か不幸な魂が、これらのすべての機能にわたるエンタープライズセキュリティの維持を担当していることでしょう。
誰もが知っているように、「機能が多い」ということは「攻撃対象領域が多い」ということです。皆さんはCVE-2026-24061を覚えていますよね? そのバグでは、サーバーと環境変数を共有するために意図されたTelnetプロトコルの機能が悪用され、RCEにつながる可能性がありました。非常に痛いバグで、今日の脆弱性ほど広範囲ではありませんでしたが、悪用はかなり容易でした。
この特定の脆弱性は、Telnetプロトコルの「LINEMODE」機能に存在します。RFC 1184が述べているように:
ローカル側で編集が有効になっているLinemodeでは、ネットワークトラフィックは、タイプされた文字ごとのパケット数ではなく、コマンドラインごとのパケット数に削減されます。これは、ユーザーがコマンドライン入力時にローカル応答時間を持ち、ネットワーク遅延はコマンドが入力された後にのみ発生するため、遅延の長いネットワークで非常に役立ちます。また、パケットごとに課金されるネットワークのコストを削減するのにも役立ちます...
そうです、この脆弱性は、ネットワークが「パケット単位で課金」されていた時代にまで遡るほど古いのです。
機能自体の詳細は私たちにとってそれほど重要ではありません(私たちの優先事項は単に「すべてをハックする」ことです)。しかし、脆弱なコードに到達するには、それを有効にする方法を知る必要があります。つまり、Telnetが接続パラメータをどのようにネゴシエートするかについて少し学ぶ必要があります。
緊迫した交渉
相互運用性のために、Telnetクライアントとサーバーはデフォルトでこれらの高度なオプションを有効にしません。接続が確立されると、IAC、つまり「Interpret As Command」バイト(16進数で0xFF)を介して、インバンドシグナリングが行われます(そんなことがうまくいったことがあったでしょうか?)。
ローカルエコーや、テレタイプライターの速度(覚えていますか?我々は覚えていません)など、ネゴシエートできることはたくさんあります。しかし、前述したように、私たちが関心のある機能は「LINEMODE」オプションです。
この機能によって定義されているSLC(または「Set Linemode Characters」)というものは、特に興味深いです。これにより、サーバーはクライアントと通信し、「バックスペース」のような新技術の特定の特殊文字を特定の制御コードで表現する必要があることをクライアントに通知できます。
少し詳しく見てみましょう。たとえば、仮説的なネゴシエーションを見てみましょう。まず、TCP経由でTelnetサーバーに接続します。サーバーはすぐに以下のデータを送信します。
IAC DO LINEMODE (または16進数で0xFF 0xFD 0x22)
ここで、「DO」はサーバーがその機能性を要求していることを示しています。それを有効にするために、クライアントは「WILL」で応答します。
IAC WILL LINEMODE (0xFF 0xFB 0x22)
それが完了すると、サーバーは3バイトのトリプレットのリストを送信します。各トリプレットは、置換される特殊文字、サポートレベル、および実際の文字値を示します。クライアントはそれを見て、変更したいものがあるかどうかを判断し、新しい値を含む応答を送信できます。これも3バイトのトリプレットのリストです。
IAC SB LINEMODE LM_SLC <トリプレット> IAC SE (0xFF 0xFA 0x03 <トリプレット> 0xFF 0xF0)
サーバーは、境界チェックを行わずに、固定サイズのグローバル配列にこれらの値をすべて格納します。え、何だって?
そうです、それが脆弱性です。1994年から検出されていません。このパッチは、情報セキュリティが到達できる現代アートに最も近いものです。
[画像: パッチが適用されたコードの比較]
しかし待ってください! さらに「面白い」ことがあります!
何がもっと面白いのか、とあなたは尋ねるかもしれません。
サーバー側ではなく、クライアント側にまったく同じ脆弱性が2005年に存在していたらどうなるでしょうか?
はい、本当です。CVE-2005-0469は、この脆弱性のドッペルゲンガーです。基本的に同じですが、クライアント側で、slc_add_replyという関数に境界チェックが欠けていました。修正は、今日の脆弱性に対する境界チェックパッチとまったく同じです。
幸いなことに、20年後、誰かがサーバー側で同じ脆弱性をチェックすることを思いつきました。
歴史は繰り返さないと言いますが、確かに韻を踏んでいます。
[画像: CVE-2005-0469とCVE-2026-32746の関連性を示す図]
エクスプロイトへ
もちろん、多くの読者が知っているように、これは物語の終わりではありません。いや、これは単なる苦悩の旅の始まりにすぎません。
- はい、これは新しい脆弱性です。
- はい、CVSSスコアは3兆です。
しかし、悪意のある攻撃者は実際にそれを悪用して何か有用なことをできるのでしょうか?
さて。それが私たちが答えるべき質問です。
残念ながら、答えはやや曖昧で、少しニュアンスが必要です。はい、グローバル変数を制御可能なデータでオーバーフローさせ(それによって後続のメモリ位置を破損させ)ることができます。しかし、そこが楽しい時間の終わりであり、現実世界が始まります。
まず、サーバーに送信できるデータはいくらか制限されています。前述のように、トリプレットの形式です。関数、フラグ、値で、それぞれ1バイトです。これらのそれぞれには独自の制限があり、主にprocess_slc関数で見つかります。
見てみましょう。
void
process_slc (register unsigned char func, register unsigned char flag,
register cc_t val)
{
register int hislevel, mylevel, ack;
/*
* この関数について何か知っていることを確認する
*/
if (func > NSLC)
{
add_slc (func, SLC_NOSUPPORT, 0);
return;
}
ここに最初の制限があります。funcバイトがNSLC定数(0x1e)より大きい場合、トリプレットの残りは破棄されます。最初のバイトは無傷で通過しますが、残りのバイトはSLC_NOSUPPORT(ゼロ)に設定され、ゼロ自体も同様です。このトリプレットはadd_slcを介してグローバル変数に追加されます。
理想的ではありませんが、対処できますよね?
関数の残りの部分はどのようになっていますか?
/*
* 0 SLC_DEFAULT 0
* および 0 SLC_VARIABLE 0 の特別なリクエストを処理する。
* ここでは少し寛容に、値が実際に0であるかどうかを気にしない。
*/
if (func == 0)
{
if ((flag = flag & SLC_LEVELBITS) == SLC_DEFAULT)
{
default_slc ();
send_slc ();
}
else if (flag == SLC_VARIABLE)
{
send_slc ();
}
return;
}
ああ。これもかなり厄介です。関数がゼロの場合、トリプレットはグローバルバッファにも到達しません。うーん。しかし、トリプレットの残りの部分はどうでしょうか? 彼らにも同様の制約があるはずですよね?
process_slc関数はそれ以上の問題を引き起こしません。しかし、それはchange_slcに処理を委譲します。これは、私たちの以前は非常にきれいにトリプレットだったものをさらに改変します。
クライアントから送信された値をサーバーの機能に基づいて変換することを意図していますが、サーバーは受け入れる前にいくつかの変更を加えます。
hislevel = flag & SLC_LEVELBITS;
mylevel = slctab[func].defset.flag & SLC_LEVELBITS;
/*
* クライアントが関数をNOSUPPORT
* または DEFAULT に設定している場合、
* 要求を簡単に直接受け入れることができます。
*/
if (hislevel == SLC_NOSUPPORT)
{
slctab[func].current.flag = flag;
slctab[func].current.val = (cc_t) _POSIX_VDISABLE;
flag |= SLC_ACK;
add_slc (func, flag, val);
return;
}
これはどういうことでしょうか? サーバーは、トリプレットの2番目のバイトである「フラグ」の下位2ビットを取得し、それを使用して「hislevel」を設定します。これはクライアント(Telnetサーバーが男性と判断したようです)が持つサポートレベルを示します。これがSLC_NOSUPPORT(ゼロ)の場合、フラグにはSLC_ACK(0x80)が設定され、SLC配列に追加されます。
結果として、フラグバイトの下位2ビットがゼロの場合、最上位ビットが設定されます。
うーん。
0xFFも特別な処理を受けます。祝福か呪いかは、その時点での必要性によります。ご覧のとおり、トリプレットの各バイトがチェックされ、0xffが見つかると、0xff 0xffに倍増されます。これにより、トリプレットを3バイトから4、5、あるいは6バイトに拡張できます。
これは、脆弱性のエクスプロイト可能性に重大な影響を与えます。
if ((*slcptr++ = (unsigned char) func) == 0xff)
*slcptr++ = 0xff;
if ((*slcptr++ = (unsigned char) flag) == 0xff)
*slcptr++ = 0xff;
if ((*slcptr++ = (unsigned char) val) == 0xff)
*slcptr++ = 0xff;
これにより、奇数個の連続した0xffバイトをこっそり挿入できなくなります。0x11 0xff 0x22を表すことはできませんが、0x11 0xff 0xff 0x22や、さらには0x11 0xff 0xff 0xff 0xff 0x22を表すことはできます。これは、特に64ビットx86システムでは、特定の場所で繰り返される0xffバイトが多いため、大きな障害となります。
しかし、この雲には銀の裏地があります。アライメントです。トリプレットの3つのバイトはそれぞれ異なる制約セットを持っているため、0xffでバッファをパディングしてトリプレットをわずかに異なるようにアラインし、必要とするデータを目的の場所に押し込むことができるかもしれません。
| 0xFF 入り | トリプレットサイズ | 書き込まれたバイト数 | アライメントシフト |
|---|---|---|---|
| なし | 3 | [F, flag, V] | +0 |
| func | 4 | [0xFF, 0xFF, flag, V] | +1 |
| val | 4 | [F, flag, 0xFF, 0xFF] | +1 |
| func + val | 5 | [0xFF, 0xFF, flag, 0xFF, 0xFF] | +2 |
このテクニックは、隣接する変数の特定のオフセット内に特定のfunc、flag、またはvalをアラインする際に非常に役立ちます。
例えば、valバイトは、ポインタの最下位バイト(LSB)を上書きして部分的なオーバーフローを行うようにアラインできます。
そこから、コードの説明は少し難しくなりますが、最終的な結果は、悪意のある入力がすべての要件を満たすようにするために、かなりの創意工夫が必要であることを保証できるということです。
そして、さらに物事はさらに難しくなります。
送信するデータは、単一のTelnet「サブネゴシエーション」パケット(サイズは0x200バイト)に収まる必要があります。オーバーフローさせるslcbuf変数自体は、サイズが0x6Cバイトです。幸いなことに、すでに4バイトのヘッダーが含まれていますが、これにより、直接の0x190バイトのみを上書きできます。
それは多くのようですが、それが私たちに何をもたらすか見てみましょう。
我々が試したこと
ここから先は、すべて特定のTelnetdバイナリに固有です。コンパイルごとに、コンパイラはグローバル変数を好きなように配置します。つまり、あるシステムで上書きウィンドウ内に収まるグローバル変数が、別のシステムでは異なる場合があります。手元にある実装をレビューするのに時間を費やしました。
私たちが発見したことは興味深いものでしたが、厳しい現実でもありました。最初に述べておく価値があるのは、テストしたシステムでは直接的なエクスプロイトルートを見つけられなかったことです。ただし、さまざまなレベルのエクスポージャーは観察されました。また、前述の制約により、64ビットx86システムでのエクスプロイトは他のシステムよりも大幅に困難であることがわかりました。
例えば、64ビットシステムでは、ポインタには通常、多くの連続したNULLバイトが含まれています(例: 0x0000000802215180)。この場合、ポインタを完全に上書きするには、3つの連続したNULLバイトを書き込む必要がありますが、これはアラインされたfunc/flag/valでは不可能です。したがって、64ビットシステムでポインタを上書きする唯一の方法は、リトルエンディアンをターゲットにする場合に部分的な上書きを行うことです。
Telnetdがコンパイルされた他のケースでは、オーバーフロー範囲内の隣接する変数はセキュリティ上の影響がありませんでした。使用されない値が変更されたか、またはサーバーによって使用前に上書きされました。
各OSの相対的な脆弱性を説明しようとする代わりに、最も興味深いと思われたものに焦点を当てます。それはややビンテージですが。
Debian - 32ビット版
64ビットx86システムでのRCEのエクスプロイトを試みるのに苦労した後(写真なし)、私たちは疑問に思い始めました。Telnetを実行している多くのレガシーシステムは32ビットx86である可能性が高いのではないでしょうか? それらでのエクスプロイトはより便利でしょうか?
Debianに目を向けました。Debianは興味深いことに、しばらく前に32ビットx86システムのサポートを終了しました。これは、この脆弱性が永遠に存在する可能性があることを意味します。私たちはbookworm、比較的古いバージョンですが、32ビット環境をサポートする最新のものをインストールしました。Telnetdバイナリをお気に入りの逆アセンブラに投入し、物事がさらに興味深くなっていることを発見しました。
[画像: IDAによるslcbufおよび隣接変数の逆アセンブル]
IDAによるslcbufおよび隣接変数の逆アセンブル
ここで、オーバーフロー可能なslcbuf変数と、それに続くいくつかの変数(上記には表示されていませんが、私たちの目的には無関係です)を見ることができます。slcptr(slcbufへのポインタ)と、比較的無害なブール値であるslcchangeがあります。長さ変数の後には、無害に見えるdef_slcbufという名前の変数があります。ソースを見て、この変数が何をするか見てみましょう。
void do_opt_slc (unsigned char *ptr, int len)
{
...
if (terminit ())
{
/* 実際の処理ロジックは省略 */
}
else
{
/*
* 最初のバッファであればこのslcバッファを保存し、
* そうでなければ破棄する。
*/
if (def_slcbuf == (unsigned char *) 0)
{
def_slclen = len;
def_slcbuf = (unsigned char *) malloc ((unsigned) len);
if (def_slcbuf == (unsigned char *) 0)
return; /* 残念 */
memmove (def_slcbuf, ptr, len);
}
}
}
void deferslc (void)
{
if (def_slcbuf)
{
start_slc (1);
do_opt_slc (def_slcbuf, def_slclen);
end_slc (0);
free (def_slcbuf);
def_slcbuf = (unsigned char *) 0;
def_slclen = 0;
}
}
まあ、かなり明白です(あるいは、このコードベースを長時間見つめすぎたのかもしれませんか?)。terminitが設定されていないときにSLCトリプレットを受信した場合、実際の処理は行われず、代わりにdef_slcbufバッファが(malloc経由で)割り当てられ、パケットがコピーされます。後で、terminitが満たされ、ターミナルが初期化されると、deferslcが呼び出され、処理が「本番」で行われ、def_slcbufがfreeされます。
かなり平凡に聞こえますよね?
実際には、そうではありません。これは、ほぼエクスプロイトへのゲートウェイです。
最初のステップは、この「遅延」動作をトリガーすることです。これは簡単です。サーバーがサポートされている機能をアドバタイズするとき、TTYPE機能に応答する前に、LINEMODE機能に応答を送信するだけで済みます。サーバーがTTYPEデータを受信すると、ターミナル初期化が完了したと見なされ、terminitはtrueを返し、LINEMODE(オーバーフローを含む!)が処理され、最後にdef_slcbufがfreeされます。
[SERVER] ← IAC DO TTYPE
[SERVER] ← IAC DO LINEMODE
[CLIENT] → IAC WILL LINEMODE
[CLIENT] → IAC WILL TTYPE
[SERVER] ← IAC SB LINEMODE 0110 IAC SE
[SERVER] ← IAC SB TTYPE 01 IAC SE
[CLIENT] → IAC SB LINEMODE (…) IAC SE <save SLC into def_slcbuf>
[CLIENT] → IAC SB TTYPE (…) IAC SE <_terminit = 1, do_opt_slc(def_slcbuf)>
アハ、気づきましたか? 技術の核心をあなたの鼻先の下にこっそり忍び込ませました。def_slcbufはfreeによって解放されます。def_slcbufは私たちが制御できる(SLCトリプレットで上書きしました)ので、任意のメモリ位置でfreeを呼び出すことができます。これは非常に強力なプリミティブです。
[画像: def_slcbufがオーバーフローされ、freeが呼び出される様子を示す概念図]
もちろん、ここからのエクスプロイトは、ターゲットで使用されているlibcに大きく依存します。多くの軽量組み込みシステムでは、これはすぐに任意の書き込み(arbitrary write)によるRCEにつながります。しかし、Debianシステムでは、たとえ12.13.0のような古いものであっても、ヒープはそれほど単純ではなく、チャンクを実際にアンリンクする前にデータに対してかなりのチェックを実行します。
フル機能のコードベースでは、ヒープチャンクに便利に配置された関数ポインタが豊富にあり、上書きされるのを待っているでしょう。しかし、Telnetdのソースコードは異なる時代の遺物であり、それがコーディングスタイルに表れています(お願いですから、私たちを攻撃しないでください、Unixの古参の皆さん、私たちはあなたを愛しています)。動的割り当てはここではそれほど頻繁に見られず、見られる場合でも、現代のJavaScriptエンジニアが想像もできないような厳格さで防御的にチェックされています。おそらく、これが多くの追加を必要とせずにコードが長期間生き残った理由の一部でしょう。
しかし、前述したように、プロトコルのより高度な機能により、複雑さが徐々に忍び寄ります。運悪くTelnetサーバーを維持していて、Kerberos認証を使用している場合(はい、それは実在します!)、攻撃者がエクスプロイトできる機会ははるかに頻繁になります。libcによって導入された複雑さでさえ、十分に熟練した攻撃者がアプリケーションをこじ開けるのに十分かもしれません。
これは、この脆弱性に関わるニュアンスの良い例です。数年前に実行されたコンパイラの気まぐれによって行われた、一見無害な選択が、私たちの脆弱性を「ふむ、それは難しそうだ」から「おお、すごい、任意解放!」へと変え、RCEのフル機能ステータスに大きく近づけました。
検知アプローチ
watchTowrでは、関心のある関係者が自身のシステムのステータスを証明するために使用できる、エンドツーエンドの検知アーティファクトジェネレーターを提供することを楽しんでいます。しかし、この脆弱性はそれほど単純ではありません。テストしたシステムで機能的なエクスプロイトを見つけられず、たとえ見つかったとしても、他のシステムにはおそらく価値がないでしょう。
しかし、私たちは検知アーティファクトジェネレーターを提供できます。これは、ほぼ同じことを行います。そのためには、プロセスの「ネゴシエーション」部分を活用できます。
上記のパッチをもう一度見ると、オーバーフロー条件が非常にミニマリストな方法で処理されていることがわかります。提供されたデータがslcbufバッファをオーバーフローさせる場合、それは単に無視されます。これは効果的でエクスプロイトを防ぎますが、クライアントにフィードバックを与えません。サーバーが何らかのエラーメッセージを送信したり、接続を終了したりした場合、検出は簡単ですが、残念ながらここではその贅沢はありません。
しかし、以前ネゴシエーションについて議論した際に、1つの比較的マイナーな詳細を省略しました。クライアントがSLCデータをサーバーに送信すると、サーバーはクライアントの要求された入力から生成され、自身の環境とマージされた独自のSLCリストを送信して応答します。サーバーはバッファに収まらないものをすべて静かに無視するため、バッファを埋めるためにジャンクデータを送信し、その後、異常な値を送信できます。脆弱なサーバーは、バッファをオーバーフローしても異常なデータを格納しますが、パッチが適用されたサーバーはそれを静かにドロップします。応答で「異常な値」を確認した場合、ターゲットが脆弱であることがわかります。
検知アーティファクトジェネレーターを指示する前に、1つの点を指摘する必要があります。私たちは、slcbufを超えて単一バイトを上書きする、最も最小限で非侵襲的なオーバーフローを実行するように検知アーティファクトジェネレーターを慎重に設計しました。本当に不運な個人であれば可能ですが、サーバーが実際に脆弱である場合、この迷子のバイトがセグメンテーション違反、またはそれ以上の何かを引き起こす可能性があります(この投稿を読んでいる古参の皆さんは、私たちの新しいインターネット検索エンジンで「nasal demons」という用語を検索するように指示してください。可能な結果の例として)。
サーバープロセスは通常(「常に」と言いたいですが、それほど愚かではありません)、inetdの管理下で起動されるため、セグメンテーション違反であっても目に見える害はありません。Telnetセッションは切断されますが、デーモン自体(および他の同時ユーザー)は影響を受けません。
もちろん、このすべてのロジックは、サーバーがLINEMODE機能をサポートしていることを前提としています。多くの組み込みアプリケーションは単純すぎて、この機能を提供していません。一部はネゴシエーションの形式さえ提供していません。これは、エステートのエクスポージャーを非常に迅速に評価するための良い「最初のテスト」です。確かに、200台のホストでTelnetが開いているのを見ると、CNCショップのフロアは恐ろしく見えますが、それらがすべて怪しいCアプリケーションにパイプしているだけなら、この特定の脆弱性にはさらされません(ただし、彼ら自身も素晴らしい脆弱性をいくつか持っていると確信しています)。
これらの2つの理由から、ターゲットバッファを実際にオーバーフローさせる機能に加えて、簡単な「プローブモード」を検知アーティファクトジェネレーターに装備しました。この「プローブモード」は、単にサーバーに接続し、LINEMODE機能が広告されているかどうかを確認するのを待ちます。その後、機能が有効かどうかを示すメッセージを表示し、オーバーフロー試行をターゲットに送信せずにすぐに終了します。これは、Telnetを搭載した古い産業機械の最もぐらついているものさえ満足させるのに十分でしょう(しかし、火災が発生した場合の修理費用はまだ払いかねます)。
デモンストレーションとして、テストが実行されているときのスクリーンショットを示します。脆弱な32ビットx86 Debianインストールを検出しています。
[画像: 検知アーティファクトジェネレーターの実行例]
舞台裏で何が起こっているかを知りたい技術者のために、--verboseオプションを付けてスクリプトを実行し、基盤となるバイトを確認してください。
python3 watchtowr-vs-Telnetd-CVE-2026-32746.py --rhost 192.168.0.154 --verbose
...
[#] Triggering overflow with SLC subnegotiation...
[>] IAC SB LINEMODE (
010041 010041 010041 010041 010041 010041
010041 010041 010041 010041 010041 010041
010041 010041 010041 010041 010041 010041
010041 010041 010041 010041 010041 010041
010041 010041 010041 010041 010041 010041
010041 010041 010041 010041
) IAC SE
...
[<] IAC SB LINEMODE (
03
018041 018041 018041 018041 018041 018041
018041 018041 018041 018041 018041 018041
018041 018041 018041 018041 018041 018041
018041 018041 01800d 0a4c69 6e7578 20362e
312e30 2d3434 2d3638 362d70 616520 286465
626961 6e3131 292028 707473 2f3129 0d0a0d
0a6465 626961 6e3131 206c6f 67696e 3a20
) IAC SE
...
--------------------
[+] LINEMODE is supported
[!] Telnetd vulnerable
ここで、ターゲットバッファをわずかにオーバーフローさせるのに十分な大きなSLCテーブルを送信したことがわかります。しかし、予期しないことが起こったことに注意してください。期待していたSLCテーブルの代わりに、ランダムに見えるバイトがたくさん返ってきました。それは私たちが送信したものでさえありません!何が起こったのでしょうか?
これは、古参の皆さんが言及した「nasal demons」の良い例です。別のコードパスにヒットし、内部値がSLCテーブルに追加される原因となりました。
一般的に見られる他の動作には、SLCデータの代わりにヒープポインタを返すことがあります(通常はslcptrグローバルが上書きされているため)。これは非常に便利な別のプリミティブであり、攻撃者がエクスプロイトを構築する際に非常に役立つものです。
[画像: ヒープポインタがリークしている様子を示す概念図]
$ python3 watchTowr-vs-Telnetd-CVE-2026-32746-leak.py --rhost 192.168.0.154
[+] Leaked Heap slcptr: 0x004b1a38
[+] Leaked Heap slcptr: 0x00468a38
[+] Leaked Heap slcptr: 0x004f4a38
[+] Leaked Heap slcptr: 0x00478a38
前述のように、これはシステム上のバイナリに固有のものです。Telnetdの(非常に多様な)インストールベースの成功したエクスプロイトに関わる複雑さのさらなる実証です。
結論
残念ながら、切望されていたRCEを達成することはできませんでしたが、おそらく理想的ではない多くの動作を発見しました。たとえば、便利な任意解放やポインタリークなどです。この脆弱性について、さらに多くのTelnetdビルドを分析する時間を費やしたかったのですが、残念ながら、誰も永遠に生きることはできず、皆には他にやるべきことがあります。
この脆弱性で最も印象的なのは、その広範なリーチです。何らかのTelnetサーバーを実行している膨大な数のシステムのかなりの部分に、この脆弱なコードが含まれています。それは永遠に存在していました。ヘック、カート・コバーンがまだ生きていた頃にこの穴が導入されたのです!- そのため、気にかける価値のあるネットワークのあらゆる隅々にまで入り込むのに十分な時間がありました。
私たちは、この標本が今後何年も現れ続けると確信しています。
もう一つの興味深い特性は、SSLVPNアプライアンスやオペレーティングシステムのような均質なインストールベースとは対照的に、このミスを含んだ無数のコードベースの無数のビルドが存在することです。
脆弱性のクラス(メモリ破損)を考慮すると、成功したエクスプロイトは、ターゲットが実行されている環境に合わせて調整する必要があります。
これは、私たちの大半にとっては良いニュースです。私たちは、オフィスのテレビが脆弱かどうかを簡単に確認できます(ネタバレ: していません)。しかし、誰かがそれのために機能的なエクスプロイトを作成するために時間と労力を費やす可能性は低いです。しかし、これは、Telnetデーモンを受け入れる必要がある機器を維持している防御者にとっては慰めにはならず、まさにそのような攻撃者クラスの標的になっている可能性があります。
もちろん、人気のある製品がクリーンなエクスプロイトにちょうど良いメモリレイアウトを持っていることが判明する可能性は常にあります。もしそれが公開されれば、すべてが台無しになるでしょう。しかし、そのような主流の機器を使用する人々は、信頼できない(または任意の)Telnetトラフィックを受け入れる必要がある人々と、それほど重複していません。
もちろん、本当の答えは「すぐにパッチを適用してください。特にダウンタイムが高すぎてマシンをパッチのために停止できない場合は」ということです。パッチが適用されたソフトウェアのバージョン番号を提供したいのですが、これは2つの理由(1つはもう1つよりはるかに合理的)で不可能です。
影響を受けるソフトウェアの広範さを考えると、これは少し冗長になります。脆弱なバージョンのinetutilsに基づいたものはすべて影響を受けますが、これは列挙するには長すぎます。
残念ながら、inetutilsプロジェクトは(少なくとも公開時点では)修正バージョンをリリースしていません。ダウンロード可能な最新バージョン2.7はまだ脆弱です。gitから修正されたコミット(これ以降)をクローンし、ソースからビルドする必要があります。
この怠惰なリードに続いて、あるいは実際にはセキュリティ対応の欠如に続いて、ほとんどのLinuxディストリビューションはまだパッケージを出荷していません。
彼らを誰が責めることができるでしょうか? メンテナーがリリースを正当化するほど重要だと考えていないなら、それは些細なことですよね?
[画像: Debianのパッケージング状況を示すスクリーンショット]
執筆時点では、sid/forkyトラックのみが修正を含んでいます。他のすべてのDebianリリースは脆弱なままです。
watchTowr Platformで研究に早期アクセスし、エクスポージャーを理解する
watchTowr Labsが公開する研究は、watchTowr Platformの背後にある同じエンジンによって支えられています。これは、スキャナーベンダーからの次の満足のいくアドバイザリを待つことを拒否するエンタープライズ向けのプロアクティブなエクスポージャー管理ソリューションです。
watchTowr Platformは、外部攻撃サーフェス管理と継続的自動レッドチーミングを組み合わせて、実際の攻撃者が実際に悪用している、重要度の高い脆弱性と技術に対する防御をテストします。