Pull to refresh

ATM security analysis 3.0: PHDays 12 in review

Reading time 8 min
Views 462

Python, Java, C++, Delphi, PHP—these programming languages were used create a virtual crypto ATM machine to be tested by the participants of the $NATCH contest at Positive Hack Days 12. The entire code was written by ChatGPT and proved to be exceptionally good. This time, we had reviewed the contest concept and decided to use a report system. In addition to standard tasks (kiosk bypass, privilege escalation, and AppLocker bypass), this year's participants faced new unusual tasks. Read on below to find out which ones.

Winners of the ATM hack 3.0 contest: nikaleksey, drd0c, and s0x28
Winners of the ATM hack 3.0 contest: nikaleksey, drd0c, and s0x28

Brief summary of the contest and some statistics

This year, the ATM dispensed special banknotes to participants that could be exchanged for merchandise, such as T-shirts and souvenir bank cards.

This is how it worked: you insert the received flag in an ATM, get a bundle of banknotes, and exchange the banknotes for a T-shirt.

Contest ATM at PHDays 12
Contest ATM at PHDays 12

More than 100 people joined the contest chat over the two days of competition (some of them left after the contest, but the chat history isn't going anywhere).

Number of contest chat subscribers
Number of contest chat subscribers

The winners shared the $500 prize money.


Incredibly, we had three participants who managed to come up with the same winning result. The final decision was therefore up to the organizers, that is, us.

🥇@drd0c: 25,000 rubles + a backpack

🥈@s0x28: 12,500 rubles + a backpack

🥈@nikaleksey: 12,500 rubles + a backpack

The competitors went toe-to-toe, so we decided there would be no bronze this year. Indeed, it would be a shame to announce a third-place winner. However, drd0c's reports were certainly more interesting than the others, which is why we made him the first winner.

This year we added a virtual machine update mechanism: during the contest, participants could run the updater.py file, after which the update.zip archive with new tasks would be downloaded.

Crypto ATM being updated
Crypto ATM being updated

Task review

ATMs have issues not only with Windows. In addition to an operating system, a real ATM has plenty of custom, highly-specialized software that it uses to issue banknotes, manage its internal components, connect with processing, and conduct transactions. This software is quite diverse and requires comprehensive and thorough analysis during a penetration test, as nobody knows which particular program attackers will use to break into the ATM and withdraw money. That was the idea behind this year's contest.

Kiosk 1 bypass

The first kiosk looked as follows: a crypto ATM interface with a scannable QR code. There is a bitcoin wallet encoded in the QR code, to which you send bitcoins so that the crypto ATM can issue banknotes. Public and private keys can later be used to manage funds available in the wallet. In fact, a real bitcoin wallet available on a virtual machine was created for the contest. That's how real crypto ATMs operate, usually with Windows under the hood.

Crypto ATM interface
Crypto ATM interface

By the way, the wallets were stored in the wallets.txt file as seen in the screenshot below.

Wallets in the wallets.txt file
Wallets in the wallets.txt file

A true information security professional should be well-rounded in their field, and well-versed in how cryptocurrencies work. The file contains private keys, and some contestants noticed this and submitted relevant reports. In real life, storing funds in a crypto ATM is not exactly safe.

And now to the kiosk: sometimes, the kiosk service mode is available in ATMs (for the convenience of the ATM servicing staff), for example, the keyboard can be turned on and off using just one key. This year, this was used by the contestants as the first method to bypass the kiosk. Every time the user pressed the u or г key, the keyboard would suddenly start working. It sounds simple, but in reality, if you don't know about this feature, it's not obvious.

import sys
import keyboard
from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtWebEngineWidgets import QWebEngineView

class KioskApp(QMainWindow):
    def __init__(self):

        # Window setting
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)

        # Web browser creation and setting
        browser = QWebEngineView()

        # Gray layer creation
        self.gray_layer = QWidget(self)
        self.gray_layer.setStyleSheet("background-color: rgba(128, 128, 128, 20);") # 1% of transparency
        self.gray_layer.setGeometry(0, 0, self.width(), self.height())

        # Toggle keys (other keys are blocked by default)
        self.keys_enabled = True

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_U or (event.text() == 'г' and event.modifiers() == Qt.NoModifier):
        elif self.keys_enabled:

    def toggle_keys(self):
        self.keys_enabled = not self.keys_enabled

        if self.keys_enabled:
            for key in keyboard.all_modifiers:
            for i in range(1, 255):
                if keyboard.key_to_scan_codes(i) != keyboard.key_to_scan_codes("u") and keyboard.key_to_scan_codes(i) != 33:

    def closeEvent(self, event):
        # Unlock keys when closing the application

if __name__ == '__main__':
    app = QApplication(sys.argv)
    kiosk = KioskApp()


Kiosk 2 bypass

The second kiosk looked as follows:

Kiosk interface
Kiosk interface

It was available in the public update we announced in the Telegram chat. The basic code functionality (shown below) is to create a window containing the kiosk bypass button and intercept keyboard events. When pressed, the button moves randomly within the window. If it's pressed more than 100 times, the window closes and the keyboard bypass stops.

A little bonus for Habr readers: below is the kiosk application code, complete and unaltered, so you can see how a kiosk app can be written in different languages and how it all works. An idea away for another day: create a kiosk that no hacker can compromise.

#include <Windows.h>
#include <cstdlib>

HHOOK keyboardHook;
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
void DisableKeyboard();

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
void MoveButtonRandomly(HWND hwnd);

HWND buttonHandle; // Global variable for storing the button descriptor
int clickCount = 0; // Global variable for counting clicks

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
    // Window class registration
    WNDCLASS wc = {0};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.hbrBackground = (HBRUSH)(COLOR_BACKGROUND);
    wc.lpszClassName = TEXT("FullScreenWindow");

    // Window creation
    HWND hwnd = CreateWindowEx(
        0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN),
        NULL, NULL, hInstance, NULL);

    // Creation of the "Exiting kiosk mode" button
    buttonHandle = CreateWindow(
        TEXT("kiosk bypass"),
        10, 10, 200, 50,
        hwnd, NULL, hInstance, NULL);

    // Setting the window on top of all other windows
    SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);

    // Displaying the window
    ShowWindow(hwnd, SW_SHOW);
    UpdateWindow(hwnd); // Updating and displaying the window content

    // Setting the keyboard hook
    keyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
    if (keyboardHook == NULL) {
        // Processing an error when installing the keyboard hook

    // Starting the message processing cycle
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))

    // Exiting the program
    DisableKeyboard(); // Disabling the keyboard before exiting

    return 0;

    switch (uMsg)
        case WM_COMMAND:
            if (reinterpret_cast<HWND>(lParam) == buttonHandle) // Checking that the message was caused by clicking the button
                if (clickCount >= 100)
                    DestroyWindow(hwnd); // Closing the window
                    DisableKeyboard(); // Disabling the keyboard

        case WM_DESTROY:
            return 0;

    return DefWindowProc(hwnd, uMsg, wParam, lParam);

void MoveButtonRandomly(HWND hwnd)
    RECT rect;
    GetClientRect(hwnd, &rect);

    // Generating random coordinates for moving the button
    int newX = rand() % (rect.right - rect.left - 200) + rect.left;
    int newY = rand() % (rect.bottom - rect.top - 50) + rect.top;

    // Moving the button to new coordinates
    SetWindowPos(buttonHandle, NULL, newX, newY, 0, 0, SWP_NOSIZE | SWP_NOZORDER);

LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
    if (nCode >= 0)
        if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
            // Preventing keyboard event processing
            return 1;

    return CallNextHookEx(keyboardHook, nCode, wParam, lParam);

void DisableKeyboard()

Escalation of privileges: a password manager

Here comes a very likely situation: a password manager can easily be found on a crypto ATM. Unlikely for a real ATM, but not impossible for a virtual machine. Indeed, a Python password_manager using the Flask framework was found on a crypto ATM. It was just lying in a folder (inactive) waiting to be found and run. Privileges could be escalated by getting into the admin account (credentials were saved in Chrome). In fact, it was a full-fledged password manager with built-in vulnerabilities. If fine-tuned, it could be used to store passwords online.

Password manager on a virtual machine
Password manager on a virtual machine

I will not show you the code, as this would stretch my article to 200 pages (instead, you can download a virtual machine and do a little reverse engineering yourself). I didn't put the source code into an EXE file either, to make life more complicated for my readers. This password manager has a lot of vulnerabilities, from cleartext storage of passwords to XSS.

Database file opened in a notepad
Database file opened in a notepad

Interesting reports from participants

The virtual machine contained over 15 vulnerabilities. There were several noteworthy reports sent by our contestants. For example, s0x28 found a remote file reader. An attacker who was in the same network with the crypto ATM could, without even bypassing the kiosk, obtain the contents of any file, in our case, wallets.txt.

Remote reading of files
Remote reading of files

@nikaleksey successfully exploited the XSS vulnerability in the wallet application. Here's how it looked:

Exploitation of the XSS vulnerability in the wallet application
Exploitation of the XSS vulnerability in the wallet application

How @nikaleksey did it is a secret: this is something you have to figure out.

Let's not forget about CVEs: several exploits for well-known CVEs were successfully used on the virtual machine (for example, CVE-2020-0796). Several participants reported this almost simultaneously.

PS C:\Users\kiosk\AppData\Local\Programs\Python\Python310> .\cve-2020-0796-local.exe
-= CVE-2020-0796 LPE =-
by @danigargu and @dialluvioso_

Successfully connected socket descriptor: 192
Sending SMB negotiation request...
Finished SMB negotiation
Found kernel token at 0xffffb88770dd1060
Sending compressed buffer...
Injecting shellcode in winlogon...
Success! ;)

If you could not make it to PHDays and see all the twists and turns of the competition live, you can watch a video created by the participants.

See how tense it was? This is how real ATMs are hacked! And that's just a small fraction of the reports that we received!

Unfortunately, there were many vulnerabilities that went undiscovered. Of course, I won't tell you all about them, that would be boring. Instead, you can download the virtual machine and try to find them yourself. It's possible that by the time this article comes out, I'll have added some new tasks to the virtual machine updates that I secretly added during the contest and that no one noticed.


A big thanks to everyone who participates in the contest year after year, helping to make it better. And another big thanks to new participants as well. This year, the format was truly experimental. We received a lot of feedback (mostly positive), but we definitely have room to improve. We look forward to seeing you again next year at PHDays and our ATM contest, which will be even better: version 4.0 is on the way!

A special thanks to my constant readers! Is anybody here reading the contest review for the third year in a row?

Contest reviews from previous PHDays events:

Until next time!

Yury Ryadnina

Banking Security Assessment Specialist, Positive Technologies. He also runs a popular channel on bug hunting. Click to subscribe:👉🏻 https://t.me/bughunter_circuit

Rating 0
Comments 1
Comments Comments 1



1,001–5,000 employees