Printing Fake Fiscal Receipts - An Italian Job p.2

TL;DR

The ItalRetail RistorAndro app installed on the SpiceT fiscal printer is affected by a pre-authentication remote arbitrary file write and an arbitrary app installation.

Moreover, the Android OS version installed is affected by two known vulnerabilities, namely CVE-2017-13156 (Janus), that allows to esclate the privileges to system, and CVE-2016-5195 (DirtyCOW) that allows to escalate the privileges to root in the vold SELinux context.

Rewind ⏮

In the first post we analyzed the fiscal unit and its local attack surface. We discovered how it’s possible for any installed Android app to abuse the fiscal features.

We also introduced the Android OS used by the SpiceT printer that makes up the remote attack surface which vulnerabilities we will now explore in detail one-by-one, and finally build a full exploit-chain.

Meet RistorAndro, the Order-Taking App 🍔

RistorAndro is a B4A framework based application installed by default on the SpiceT device. It has a very complex UI and it is used as a Point of Sale (PoS) in restaurants and bars.

The peculiarity of RistorAndro is that you can connect waiters’ smartphones with the WPalm app installed, on the same Wi-Fi as the SpiceT device and use the smartphones to take orders. This feature is implemented in a very very very very very … very strange fashion.

Basically, both RistorAndro and WPalm create an unauthenticated webserver on port 12312 and, through the /write endpoint, exchange ZIP archives. These archives contain text files with the data to be trasmitted, being the clients’ orders, the receipts, information of the tables, etc. There are also other endpoints but not quite as interesting.

The /write endpoint takes two multipart field:

  • matricola that contains an integer.
  • upfile that contains the ZIP file content and its file name.

When receiving the files, the endpoint executes the following Java code:

1
2
3
4
5
6
7
8
9
String upfile_name = request.GetUploadedFile("upfile");
String matricola = request.GetParameter("matricola");
StringBuilder path = new StringBuilder();
String matricola_path = path.append("/storage/emulated/0/Ristorandro/ftproot").append("/").append(matricola).toString();
// [...]
path = new StringBuilder();
anywheresoftware.b4a.objects.streams.File.Copy(server.TempFolder, upfile_name, path.append(matricola_path).append("/VENDUTO").toString(), upfile_name);
// [...]
anywheresoftware.b4a.objects.streams.File.Delete(server.TempFolder, upfile_name);

Since the attacker controls both the matricola and the upfile parameters that are used in the File.Copy method, they can exploit a path traversal resulting in an pre-authentication remote arbitrary file write scenario.

It’s worth reminding that the SpiceT uses Android 6, therefore there is no Scoped Storage and since RistorAndro has the WRITE_EXTERNAL_STORAGE permission it’s possible to write anywhere in the whole /storage/emulated/0 mountpoint.

Auto-Updates Done Wrong

In order to be compliant with the ever changing electronic invoice XML format accepted by the Agenzia delle Entrate, RistorAndro had to implement an auto-update mechanism without the need of a technician action.

This auto-update feature steps follow:

  1. An APK is downloaded from the backend into the device external storage, specifically inside the /storage/emulated/0/Updater/WebApi/ folder.
  2. When the RistorAndro app restarts, the download folder is scanned for APK files with common names.
  3. The APK name is checked to contain a higher version number than the one installed.
  4. The APK is finally installed on the device.

As an example here are some valid filenames:

  • InvioFE_31337.apk
  • Spooler_31337.apk
  • wPalmRistorAndro_31337.apk
  • RistorAndro_31337.apk

By placing an APK file in the /storage/emulated/0/Updater/WebApi/ folder and naming it correctly it is possible to force the SpiceT device to install it automatically - an arbitrary app installation.

Faking APK signatures with Janus

CVE-2017-13156 (Janus) is a High severity Escalation of Privileges (EoP) vulnerability in the Android v1 APK Signature Scheme that allows to bypass the signature verification during the installation of an APK file.

To understand how it works we need to look into the APK file format and how the Android OS parses it.
Since APK files are just ZIP archives, they are parsed the same way. The ZIP format works by using two main structures:

  • a series of local file descriptors that define the various compressed files and their metadata.
  • a central directory that references every file descriptor contained inside the archive by a relative offset from the start of the archive.

The central directory is placed at the end of the archive, this means that ZIP and APK files can start with arbitrary content before the first referenced local file descriptor.

When installing an application, if the v1 APK Signature Scheme is used, the signature of every file referenced inside the ZIP central directory is verified. If a file is not referenced by the central directory it will not be verified, but it should also not be used. Right?

By analyzing the Android Runtime (ART), we can learn that it’s possible to load and execute both DEX and ZIP files, based on the file’s Magic bytes.

In this way, a valid APK file starting with DEX bytecode will sucessfully pass the APK signature verification and at the same time load and execute the unsigned DEX payload.
This is the reason this vulnerability was named after Janus, the Roman god of duality.

Notice that it’s only possible to inject DEX / Java classes, the Android manifest cannot be modified in any way.

By forging an APK using the Janus vulnerability, we could overwrite an already installed application and abuse their privileges. The most privileged applications on the SpiceT run as system or shell, so that’s the highest level of privileges we could achieve.

🏎-ing to Root

By looking at the kernel version and the patch level we can notice that the device is vulnerable to one of the most famous Linux EoP, DirtyCOW.
DirtyCOW exploits a race condition in the implementation of the copy-on-write mechanism in the kernel’s memory-management subsystem. In this way it is possible to turn a read-only mapping of a file into a writable one.

If you want a deep-dive technical explaination of CVE-2016-5195 I suggest to check out this article by Chao-tic.

While on most of the operating systems being able to write files as root is a game over itself, on Android there is SEAndroid (SELinux), which is a dream breaker. In fact we are limited by a context and its rules.

Creating the Ultimate SpiceT Exploit ⛓⛓⛓™️

It’s time to gather together all the vulnerabilities we’ve found:

  1. We connect to the same LAN of the SpiceT.
  2. We craft a backdoored and Janus-powered version of com.fsl.ethernet a pre-installed app which has the system privileges and gives us persistence since it is executed at every reboot.
  3. We upload the backdoored app in the APK installation folder with a proper filename using the remote file upload and the path traversal.
  4. We upload a broken RistorAndro config file abusing the same vulnerability, which will cause a crash and a restart of RistorAndro, triggering the malicious app installation.
  5. We now have a Remote Code Execution as system.
  6. We use Janus once more on com.android.shell to downgrade our privileges to shell as we need write and execute access into /data/local/tmp/ folder to run DirtyCOW over some binaries in /usr/bin/.
  7. We trigger com.android.shell through the com.android.shell/.BugreportReceiver Intent via com.fsl.ethernet as we need system privileges to invoke it.
  8. We run DirtyCOW over /usr/bin/ntfsfix which will be automatically called from root in the vold SEAndroid context if an NTFS USB key is connected to the SpiceT.
  9. We wait an NTFS USB key to be connected to the SpiceT.
  10. We get a root shell in the vold context.
  11. We mount an ext4 image over the /sbin mountpoint and replace the request-key binary.
  12. We call the request-key syscall, finally gaining the init SEAndroid context for the root user!

Finally, our chain as seen from Logcat:

03-18 11:24:16.937   517  1696 I ActivityManager: START u0 {dat=file:///storage/emulated/0/Android/data/cassandraRisto.ital/files/shared/Spooler_99999999.apk cmp=com.android.packageinstaller/.InstallAppProgress (has extras)} from uid 10016 on display 0
03-18 11:24:17.024  2895  2895 W InstallAppProgress: Replacing package:com.fsl.ethernet
03-18 11:24:17.299   517   535 I ActivityManager: Displayed com.android.packageinstaller/.InstallAppProgress: +320ms
03-18 11:24:27.716  2909  2920 D DefContainer: Copying /storage/emulated/0/Android/data/cassandraRisto.ital/files/shared/Spooler_99999999.apk to base.apk
03-18 11:24:28.173   517   530 I ActivityManager: Force stopping com.fsl.ethernet appid=1000 user=-1: replace sys pkg
03-18 11:24:28.177   517   541 W PackageManager: Trying to update system app code path from /data/app/com.fsl.ethernet-2 to /data/app/com.fsl.ethernet-1
03-18 11:24:28.179   517   541 W PackageManager: Code path for com.fsl.ethernet changing from /data/app/com.fsl.ethernet-2 to /data/app/com.fsl.ethernet-1
03-18 11:24:28.179   517   541 W PackageManager: Resource path for com.fsl.ethernet changing from /data/app/com.fsl.ethernet-2 to /data/app/com.fsl.ethernet-1
03-18 11:24:28.187   517   530 I ActivityManager: Force stopping com.fsl.ethernet appid=1000 user=-1: replace pkg
[...]
03-18 11:24:34.142   517  1634 I ActivityManager: START u0 {act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 pkg=com.fsl.ethernet cmp=com.fsl.ethernet/.MainActivity} from uid 10016 on display 0
03-18 11:24:34.355   517  1903 I ActivityManager: Start proc 3060:com.fsl.ethernet/1000 for activity com.fsl.ethernet/.MainActivity
03-18 11:24:35.125  3060  3060 D EthernetManager: We will operate iface:eth0
03-18 11:24:35.133  3060  3060 D EthernetManager: Static IP =172.16.42.110
03-18 11:24:35.139  3060  3060 E EthernetManager: Exception in setting Static IP
03-18 11:24:35.178  3060  3060 D EthernetManager: set ip manually com.fsl.ethernet.EthernetDevInfo@d86fab9
03-18 11:24:35.687   517   535 I ActivityManager: Displayed com.fsl.ethernet/.MainActivity: +1s410ms (total +6s327ms)
[...]
03-18 11:24:37.733  3078  3078 D AndroidRuntime: Calling main entry com.android.commands.am.Am
03-18 11:24:37.767   517   530 I ActivityManager: Start proc 3091:com.android.shell/2000 for broadcast com.android.shell/.BugreportReceiver
03-18 11:24:38.145  3110  3110 I cowhard : Welcome to cowhard! (/data/local/tmp/dirtycow)
03-18 11:24:38.145  3110  3110 I cowhard : ------------
03-18 11:24:38.145  3110  3110 I cowhard : uid 2000
03-18 11:24:38.146  3110  3110 I cowhard : Current selinux context: u:r:shell:s0
03-18 11:24:38.146  3110  3110 I exploit : warning: new file size (9900) and file old size (165668) differ
03-18 11:24:38.147  3110  3110 I exploit : [*] mmap 0xb6d51000
03-18 11:24:38.147  3110  3110 I exploit : [*] exploit (patch)
03-18 11:24:38.156  3110  3110 I exploit : [*] currently 0xb6d51000=464c457f
03-18 11:24:38.156  3110  3111 I exploit : [*] madvise = 0xb6d51000 165668
03-18 11:24:51.012  3110  3111 I exploit : [*] madvise = 0 1048576
[...]
03-18 11:41:38.513   517   586 D UsbHostManager: Added device UsbDevice[mName=/dev/bus/usb/002/002,mVendorId=2316,mProductId=4096,mClass=0,mSubclass=0,mProtocol=0,mManufacturerName=General,mProductName=USB Flash Disk,mVersion=2.16,mSerialNumber=0356115070002381,mConfigurations=[
03-18 11:41:40.056   197   208 V vold    : /dev/block/vold/public:8:1: UUID="101E2D2D1E2D0CF2" LABEL="ShielderUSB" TYPE="ntfs" 
03-18 11:41:40.089   197   208 E Vold    : Filesystem check failed (not a FAT filesystem)
03-18 11:41:40.125   197   208 I Vold    : Ntfs filesystem existed
03-18 11:41:40.152  3647  3647 I cowhard : Welcome to cowhard! (/system/bin/ntfsfix)
03-18 11:41:40.152  3647  3647 I cowhard : ------------
03-18 11:41:40.152  3647  3647 I cowhard : uid 0
03-18 11:41:40.153  3647  3647 I cowhard : Current selinux context: u:r:vold:s0
03-18 11:41:40.153  3647  3647 I cowhard : Executing '/data/media/0/cmd.sh'
03-18 11:41:40.298   197   208 I Vold    : Ntfs filesystem check completed OK
03-18 11:41:40.299   197   208 W Vold    : The SD card is world-writable because the 'persist.sampling_profiler' system property is set to '1'.
03-18 11:41:40.299   197   208 D Vold    : Mounting ntfs with options:utf8,uid=1023,gid=1023,fmask=0,dmask=0,shortname=mixed,nodev,nosuid,dirsync,noexec
[...]
03-18 11:41:53.611  3780  3780 I cowhard : Welcome to cowhard! (/sbin/request-key)
03-18 11:41:53.611  3780  3780 I cowhard : ------------
03-18 11:41:53.611  3780  3780 I cowhard : uid 0
03-18 11:41:53.611  3780  3780 I cowhard : Current selinux context: u:r:init:s0

Conclusions

A good mix of spaghetti code and outdated software granted us a nice ride which led to an old fashioned remote root exploit chain.
Ah - in case you forgot along the journey - the first 4 steps of our exploit chain are enough to takeover the fiscal printer remotely and print fiscal receipts / tamper the DGFE, the other steps to get a root shell were just a pure exploitation exercise 🏋️‍♂️.

Pitch 🗣️

If you are developing your own Android-powered product, make sure to do a proper threat modelling and design to prevent similar vulnerabilities to exist and make their way into production.
Get in touch with us to work arm-in-arm with your development team to make security part of the life cycle of your products: https://www.shielder.com/contacts/

8 min

Date

16 May 2022

Author

thezero

Security Researcher and Senior Penetration Tester at Shielder.
In the office I’m the one with the soldering iron.