Coding a backdoor using Grafana Cloud as covered channel (part 2)
In this series of articles, I will detail how I developed a backdoor using various public APIs for communication.
This series of articles is intended solely for educational purposes. The goal is to understand the entire cycle of a cyber-attack, including the design and development of the tools. It is also worth noting that the use of this tool may violate the Terms of Service (ToS) of different providers, and it is your responsibility to check and control it before any usage.
Introduction
Consider for a moment that an attacker could use legit services to exfiltrate data from your server. You’ve done everything right — strict ingress & egress filtering rules, and even deployed remote logs, all within a Grafana Cloud stack. But what if the attacker could also use Grafana? Would you notice? A process like /usr/local/sbin/promtail running and sending logs to Grafana seems perfectly legitimate, right? Except that, by default, promtail (the collector in the Grafana stack) is located in /usr/local/bin/promtail. The additional process running on your machine could be a backdoor. Impossible, you say?
Before we can explore what we can do with Grafana, we need to address some issues with shameleon v0.1.0:
- Encryption layer: No way we send data in plain text to a third party, even for a proof of concept
- Allow defining a maximum size for our payloads to avoid rejection of our API calls.
- Configure all of this through a profiling system.
- Have a builder that embeds the configuration into the backdoor.
- Manage tunnel timeouts and implement a garbage collector.
Encryption
When it comes to encryption, I go with KISS principles. So, we’ll opt for symmetric encryption with a key embedded in the binary. It may not be very resilient to forensic analysis, but that’s okay for now.
In general, I advise always using off-the-shelf algorithms. Never attempt to implement your own algorithm, even using primitives. Cryptography is a tough topic, and you could quickly introduce vulnerabilities into your code, such as a padding oracle.
So let’s go with Fermet encryption.
Payload Management
Up to this point, we were taking all the data and sending it naively to the API. However, APIs often have a maximum size for payloads (characters, size, etc.). On a shell, you are unlikely to encounter issues (except when dealing with large files). However, when we want to implement a SOCKS (teasing!!!) or port forwarding, it becomes problematic.
Since we’ve added an encryption layer, we need to consider the overhead generated. We have roughly two possible approaches:
- Estimate the encryption overhead (e.g., 33% for base64), limit the maximum size of useful data, and then cut it.
- Encrypt and cut the encrypted data.
The first approach requires accounting for padding and may result in a significantly higher number of calls (padding being unnecessary data). The second approach, on the other hand, requires all packets to be received and concatenated to be decrypted together.
In the long run, we want to implement SOCKS, and the throughput will already be catastrophic. Let’s be frugal, and go for option one.
So, we encrypt the payload, cut it into payloads of size n-1 (n being the maximum allowed). Why n-1? Because I decided to add a “!” at the end of incomplete packets. This way, I know that I need to buffer this data until the next packet without “!”.
Configuration Management
On the client side, the issue is quite straightforward. We simply introduce the concept of profiles, YAML files that we can choose via an argument on our client.
On the server side, it’s a bit trickier, and several solutions are available:
- Have a configuration file next to the backdoor.
- Pass parameters as arguments or environment variables.
- Embed the configuration in the binary.
The first option is hard to deploy and risky (a simple grep on a server, and your backdoor is discovered). The second is visible, without additional measures, upon inspecting the running processes. The third is by far the easiest to use, but embedding configuration can be challenging.
Embedding configuration in a binary can quickly become a headache, especially if the binary is already compiled. You will have to patch an existing string anddoing so, you need to manage padding, size, etc., to avoid corrupting your binary.
The advantage of GO is the ease of building. It is possible to pass variables to the build with a simple argument. So, let’s not miss this opportunity and provide a JSON encoded in base64 that will be embedded during the build and decoded at the runtime of the backdoor.
Builder
We need to embed a configuration, but more importantly, we want to handle a modular backdoor. GO lead to big binaries, so we won’t include all the modules, but only the selected provider.
Therefore, let’s create a small Python builder that:
- Chooses the right .go file for the selected provider.
- Embeds the configuration.
- Removes println statements from the source code (except in the case of a build for debugging).
And there you have it! No need to worry about building our backdoor anymore.
Timeout Management
Tunnels are created on the client’s demand and kept in memory on the backdoor side. So far, we’ve never bothered to delete them. In operations, this can pose a significant long-term memory consumption problem.
Therefore, we will implement the concept of “lastseen” and timeouts on the tunnels (all configurable, of course!). Then, a small garbage collector, and there we are a bit cleaner.
The Cherry on the Cake
Now that we have improved our POC a bit, let’s look at Grafana, specifically Loki.
Loki has a REST API allowing writing and reading logs, precisely what we want, through two endpoints:
The format is quite simple; we can push JSON containing our encrypted payload and retrieve it. To distinguish between the upstream and downstream tunnel, we will use labels (by default, the severity level).
Two methods to implement on the client and the backdoor, and there you go! Our backdoor is functional!