セキュリティ
Telegram Desktopの脆弱性により、ユーザーのファイルが盗まれる可能性
Telegram Desktop vulnerability allowed any user's file to be stolen (beaksec.github.io)
要約
Telegram Desktopの脆弱性(CVE-2026-107181)により、細工されたリンクをクリックするだけで、攻撃者はユーザーのローカルファイルを読み取り、それを攻撃者の管理するチャットに送信できるようになります。この脆弱性は、IPC(プロセス間通信)におけるコマンドインジェクションと、認証なしのファイル読み取り機能の組み合わせによって発生します。この問題はTelegram Desktop 7.2.9で修正されました。
全文翻訳
Telegram Desktop: IPCインジェクションによるワンクリックアカウント乗っ取り
目次
Telegram Desktop: IPCインジェクションによるワンクリックアカウント乗っ取り
はじめに
誰かがあなたをTelegramグループに追加します。
チャットにリンクが表示されます。
それをクリックすると、あなたのTelegramアカウントはもはやあなただけのものではなくなります。
どうやって?
Telegram Desktopは、クリックされたリンクを、コマンドを区切るために使用される文字をエスケープせずに、テキストとして、既に実行中のインスタンスにローカルソケット経由で渡します。
そのため、細工されたリンクは1つの命令としてではなく、複数の命令として到着します。
私が発見したチェーンには2つの欠陥があります。
最初のものはインジェクションです。
2番目のものは、インジェクションされたコマンドが到達するものです:ファイル名を指定して実行する内部URIスキーム、interpret:。これは、誰がそれを要求したかを確認せず、確認なしに、命令ファイルの名前のファイルを読み取り、それをチャットに送信します。
これらを組み合わせると、クリックされたリンクは任意のファイル読み取りに変わります。
この記事では、チェーンを詳しく説明し、それを使って被害者のログインファイルであるファイルを盗みます。
影響を受けるもの
Telegram Desktop 7.2.8まで、Windows (6.9.3)で確認済み
影響
リモートからの任意のローカルファイル読み取り、攻撃者制御のチャットへの漏洩;アカウント乗っ取り
CVE
CVE-2026-107181
修正済み
7.2.9、コミット db3405699f
深刻度
8.1 High, CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N
1つのリンク、2つのプロセス
オペレーティングシステムは、プログラムがURIスキームを登録できるようにします。これにより、その種のリンクに遭遇したときにどのアプリケーションを起動するかを知ることができます。
Telegram Desktopはtgを登録します。
それ以降、システムはtg://...リンクがTelegramに属することを知り、URLをコマンドライン引数として渡して起動します。
Telegramが実行されていない場合、プロセスは開始され、パラメータとして文字列を受け取り、それをURLオブジェクトに変換して内部で処理します:1つのプロセス、通信するものはありません。
しかし、Telegramが既に実行されている場合はどうでしょうか?
オペレーティングシステムはそれを知らず、チェックもしません:とにかく新しいプロセスを起動します。これは最初のプロセスと同一です。
Telegram自体は、それが冗長であることを認識する必要があります。その認識方法は、ローカルソケットに接続しようとすることです。
既に実行中のインスタンスがサーバーです:起動以来、そのソケットでリッスンしています。
新しいプロセスがクライアントです。
接続に成功した場合、インスタンスは既に生きています。そのため、リンクを渡し、終了します。
ソケットはオブジェクトを運ぶのではなく、バイトを運びます。
メモリ内の新しいプロセスが持つURLオブジェクトは、そのチャネルを通過できません。そのため、テキストの行に平坦化する必要があります。
その操作には名前があります:シリアライゼーション。
その逆、テキストからオブジェクトを再構築することは、デシリアライゼーションです。
構造化データが境界を越えなければならないときはいつでも、両方とも避けられず、両方とも境界がデータ内の構造によって保持されなくなり、テキスト内の文字になる正確な時点です。
Telegramは独自のフォーマット、シンプルなものを使用します。
各命令はキーワード、その引数、そしてそれを閉じるセミコロンです。
開くリンクは次のようになります:
1 OPEN:tg://x?a=1;
tg://x?a=1 はTelegram内のどのハンドラにも一致しないため、それ自体では何も行いません。
それは単なるキャリアです。
その行はここで、開くURLごとに構築されます:
1 2 3 4
// sandbox.cpp:295-297
for (const auto &url : cRefStartUrls()) {
commands += u"OPEN:"_q + url.toString(QUrl::FullyEncoded) + ';';
}
反対側では、実行中のインスタンスがデシリアライズします:受信したバイトを読み取り、各セミコロンで切り取り、各部分をそれ自体で命令として扱います。
OPEN:で始まる各部分について、それに続くものを取得し、コマンドラインに到着したかのようにURLを再構築します。
1 2 3 4 5 6
// sandbox.cpp:453-463 (省略)
for (int32 to = cmds.indexOf(QChar(';'), from); to >= from; ...) {
auto cmd = base::StringViewMid(cmds, from, to - from);
...
} else if (cmd.startsWith(u"OPEN:"_q)) {
startUrls.append(cmds.mid(from + 5, to - from - 5).mid(0, 8192));
}
エスケープされていない区切り文字
では、送信された値のいずれかに、フォーマットが区切り文字として使用する文字であるセミコロン自体が含まれている場合はどうなりますか?
前のリンクを取り、何かを追加してみましょう:
1 tg://x?a=1;CMD:quit
新しいプロセスはそれを単一のURLとして扱います。なぜなら、それにとってそのセミコロンはクエリ内の単なる文字だからです。
それを平坦化してソケットに書き込みます:
1 OPEN:tg://x?a=1;CMD:quit;
実行中のインスタンスは各セミコロンで切り取り、1つの命令ではなく2つの命令を取得します:
1 2
OPEN:tg://x?a=1
CMD:quit
これがインジェクションであり、2つの欠陥のうちの最初のものです。
interpret: URIスキーム
上記の例はCMD:をインジェクトしましたが、その名前で誤解しないでください:showとquitのみを受け入れるため、最悪の場合でもアプリを閉じることができます。
合計で4つのコマンドが受け入れられますが、そのうち3つは無害です。
4番目はOPEN:であり、そこに詳細があります:スキームのフィルタリングなしで、任意のURLを受け入れます。
コードを掘り下げると、Telegram内にinterpret:と呼ばれる別のURIスキームが見つかります。
オペレーティングシステムは、interpret:で始まるリンクをどう扱ってよいかわかりません。なぜなら、それはプロトコルハンドラとしてどこにも登録されていないからです:それはTelegram自身のコード内にのみ存在し、他のものと同様に、スタートURLリストからスキームを取得します。
1 2 3 4
// application.cpp:1162-1164
if (url.scheme() == u"interpret"_q) {
interprets.append(url.path());
return false;
}
OPEN:経由で、到達可能になります:
1 tg://x?a=1;OPEN:interpret:instructions.txt
では、interpret:は何のためにあるのでしょうか?
それはTelegramが自身のリリースを公開するために使用したツールでした。
新しいバージョンが出荷されると、ビルドアーカイブは変更ログをキャプションとしてチャネルに投稿する必要がありました。
手作業で行う代わりに、スクリプトはチャネル、送信するファイル、書き込むテキストを指定する小さなテキストファイルを作成し、Telegramをそのファイルへのパスで起動しました。
1 2 3
# Telegram/build/updates.py:206
subprocess.call(... 'Telegram -sendpath interpret://' + scriptPath + '/.../command.txt', shell=True)
命令ファイルは次のようになります:
1 2 3 4 5 6 7
from: 1234567890
channel: 1987654321
file: out/Release/deploy/6.9.3/tsetup.6.9.3.exe
caption: TDesktop at 12.06.26: - Fixed a crash in the media viewer. - Added a new sticker pack.
from:の値は、現在ログインしているアカウントのIDと比較されます:これにより、オペレーターが間違ったアカウントからリリースを公開するのを防ぎます。
その行が存在する場合にのみチェックが実行されるため、省略するとスキップされます。
宛先はchannel:によってのみ設定され、チャネルまたはスーパーグループである必要があります。
InterpretSendPathという関数がその作業を行います。
では、バグはどこにあるのでしょうか?
interpret:は特権的なアクションを実行します。ディスク上の任意のファイルを読み取り、誰にも確認を求めず、誰がそれを要求したかを確認せずにチャットに送信します。
その関数は承認チェックを実行しません。
1 2 3 4 5 6 7 8 9
// support_helper.cpp:673-680
QString InterpretSendPath(not_null<Window::SessionController*> window, const QString &path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
return "App Error: Could not open interpret file: " + path;
}
const auto content = QString::fromUtf8(f.readAll());
コマンドラインから来る場合、つまりリリーススクリプトがそれを呼び出す方法ですが、問題はありません:攻撃者は既にマシンに足場を持っている必要があり、それがあれば自分でファイルを読み取ることができます。
しかし、同じアクションがソケット経由で、したがってインジェクション経由で到達可能になると、被害者がクリックしたリンクから危険な関数が利用可能になります。
これは認証の欠落であり、2つの欠陥のうちの2番目のものです。
命令ファイルをディスクに配置する
攻撃者が被害者のディスク上に命令ファイルを配置し、file:を盗む価値のあるパスに、channel:を自分のチャネルに向けさせることができれば、クリックされたリンクだけでそのマシンから任意のファイルを漏洩させることができます。
では、攻撃者はどのようにして予測可能なパスにテキストファイルを配置するのでしょうか?
最も明白な方法は、チャットの添付ファイルとして送信することです。
偶然ですが、Telegram Desktopはデフォルト設定で、グループで受信したファイルを最大8 MiBまで自動的にダウンロードしますが、ブロードキャストチャネルでは自動ダウンロードはオフになっています。
ファイルは、送信者が選択したのと同じ名前で、標準フォルダに配置され、