AX3600 - Post #1

In my previous post I gave you an intro about AX3600 and how awesome it is. This post will focus on how to gain persistent ssh on the device.

First off, I recommend following the OpenWRT effort on porting AX3600: Adding OpenWrt support for Xiaomi AX3600 For Developers. Until then, follow this guide.

All the scripts, firmwares, configuration files, etc’ mentioned in this post are available on GitHub: odedlaz/ax3600-files. Please feel free to open issues or better yet - pull requests!!

gain SSH access

All you need to know is that early versions of the router had a command injection vulnerability that is now closed. In order to gain ssh access, you first need to downgrade your router’s firmware then run the commands listed in OpenWRT’s page. But what is the command injection? taking closer look at the source…

We want to extract the firmware. Thankfully, there’s a tool just for that: ubi_reader.
Follow the instruction and get it up and running. Once you do, download the firmware and run:

$ ubireader_extract_images -o xioamifw -w miwifi_r3600_firmware_5da25_1.0.17.bin
$ unsquashfs -f -d xiaomifw xioamifw/miwifi_r3600_firmware_5da25_1.0.17.bin/img-1696626347_vol-ubi_rootfs.ubifs

The command injection URL looks like this:;stok=<STOK>/api/misystem/set_config_iotdev?bssid=gallifrey&user_id=doctor&ssid=-h%0Anvram%20set%20ssh%5Fen%3D1%0A

How does it work? let’s find set_config_iotdev:

$ grep! -R "set_config_iotdev"
xioamifw/miwifi_r3600_firmware_5da25_1.0.17.bin/output/usr/lib64/lua/luci/controller/api/misystem.lua: entry({"api", "misystem", "set_config_iotdev"}, call("setConfigIotDev"), (""), 221)

Ok, it looks like there’s a file called misystem.lua that contains all the controller apis. Also, the set_config_iotdev parameter is tied to the setConfigIotDev. easy enough… let’s look at it:

function setConfigIotDev()
local XQFunction = require("xiaoqiang.common.XQFunction")
local LuciUtil = require("luci.util")
local result = {
["code"] = 0

local ssid = LuciHttp.formvalue("ssid")
local bssid = LuciHttp.formvalue("bssid")
local uid = LuciHttp.formvalue("user_id")

XQLog.log(debug_level, "ssid = "..ssid)
XQLog.log(debug_level, "bssid = "..bssid)
XQLog.log(debug_level, "uid = "..uid)
if XQFunction.isStrNil(ssid)
or XQFunction.isStrNil(bssid)
or XQFunction.isStrNil(uid) then
result.code = 1523
if result.code ~= 0 then
result["msg"] = XQErrorUtil.getErrorMessage(result.code)
XQFunction.forkExec("connect -s "..ssid.." -b "..bssid.. " -u "..uid)

Very straight forward and classic! look at the line starting with forkExec: there’s no input validation, thus parameters supplied to the api are used on the router itself and passed directly to exec. The injection basically add a \n to the end of the last parameter, following by the command we really want to run, which is nvram set ssh_en=1.

By the way, on later firmwares the misystem.lua is compiled and encrypted. While trying to figure out how to decrypt it I came across a great deck called Exploit (Almost) all Xiaomi Routers Using Logical Bugs which you might like.

Enough! we just want to gain SSH! Let’s downgrade. It’s pretty easy, all you need to do is:

  1. access the setting page:;stok=<STOK>/web/setting/upgrade
  2. press “Manual upgrade” (I’ve got Google Translate turned on)
  3. upload the firmware (miwifi_r3600_firmware_5da25_1.0.17.bin). I backed it up since I’m a bit paranoid. You can find it under the firmwares directory in my repo or the official link.

Afterwards, you can follow the guide supplied by OpenWRT or just import the Postman collection I created. Don’t forget to update the stok variable in the collection. You can grab it by logging in to the router web interface and getting the value of “stok=” from the URL.

gain persistent SSH

A guy called didiaoing wrote a nice tutorial which explains how to modify the routers bdata partition in order to persistenly turn on ssh. The problem? it’s manual, error prone and in chinese.

Enough talkin’! let’s get flashin’!

Clone my repo and create a bdata directory:

$ git clone
$ cd ax3600-files
$ mkdir -p bdata

The router’s uses UBIFS as it’s filesystem. UBIFS workes on top of UBI, which in turn works on top of MTD.

You can ssh to your router and see the mtd partitions for yourself, they’re all there for the taking:

$ cat /proc/mtd
dev: size erasesize name
mtd0: 00100000 00020000 "0:SBL1"
mtd1: 00100000 00020000 "0:MIBIB"
mtd2: 00300000 00020000 "0:QSEE"
mtd3: 00080000 00020000 "0:DEVCFG"
mtd4: 00080000 00020000 "0:RPM"
mtd5: 00080000 00020000 "0:CDT"
mtd6: 00080000 00020000 "0:APPSBLENV"
mtd7: 00100000 00020000 "0:APPSBL"
mtd8: 00080000 00020000 "0:ART"
mtd9: 00080000 00020000 "bdata"
mtd10: 00080000 00020000 "crash"
mtd11: 00080000 00020000 "crash_syslog"
mtd12: 023c0000 00020000 "rootfs"
mtd13: 023c0000 00020000 "rootfs_1"
mtd14: 01ec0000 00020000 "overlay"
mtd15: 00080000 00020000 "rsvd0"
mtd16: 0041e000 0001f000 "kernel"
mtd17: 016c4000 0001f000 "ubi_rootfs"
mtd18: 01876000 0001f000 "data"

We’re interested in the bdata partition. go ahead and dump it:

$ nanddump -f /tmp/bdata_mtd9.img /dev/mtd9

then copy it to your machine. If you’re using linux, then the following command should work:

$ scp [email protected]:/tmp/bdata_mtd9.img /path/to/ax3600-files/bdata

If you open it up in a hex editor, you’ll notice a few weird bytes in the beginning then a few ASCII encoded strings, a looooot of zeroes and some other bytes in the end. the first four bytes are the CRC32 checksum for the bdata partition and the strings indicate the devices configuration. you probably recognize some of them, since you’ve updated them when turning on ssh on the device!

You can also look at the header by running the provided script. just cd to the scripts directory and issue the following command:

$ ./ extract ../bdata/bdata_mtd9.img
CRC32: 17 27 E1 B2
color: 101
CountryCode: CN
SN: 11233/A0C123456
model: R3600
miot_did: 112233444
miot_key: aap0blsq5aQbVFmi
telnet_en: 0
ssh_en: 0
uart_en: 0
wl0_ssid: Xiaomi_EA69_5G
wl1_ssid: Xiaomi_EA69
wl2_ssid: Xiaomi_EA69

We want to update these values to enable ssh on boot. Since dropbear is not turned on by default, you need to enable telnet as well. Actually, if we’re going to mess with the router, why not turn on the router’s serial port so you could flash the entire thing if you break it? Darell Tan’s got a nice post on customizing the firmware through serial. great read!

Moreover, in the next post I’ll write about the weird stuff I found on the router. Some of these stuff only run if the router’s Country Code is set to CN, so why not change it to US? this is not random! when you change the country code to something other than CN, the router behaves a bit differently. for instance, it performs online checks against google & microsoft instead of baidu & taobao. both are configured in /etc/config/system and accessed by running uci -q get system.netdt.world_domain and uci -q get system.netdt.cn_domain respectively. Don’t believe me? take a look at the check_gateway function in usr/sbin/pppoe-check.

Once you update the headers, you also need to update the checksum. Don’t worry, does it all for you.
First let’s do a test run to make sure we don’t break anything:

$ ./ modify --test ../bdata/bdata_mtd9.img ../bdata/bdata_mtd9.img.modified
successfully re-assembled header without modifications

If you got any response other than the above, please open an issue! if everything went fine, you can run:

$ ./ modify --country US ../bdata/bdata_mtd9.img ../bdata/bdata_mtd9.img.modified
$ ./ extract ../bdata/bdata_mtd9.img
CRC32: 27 21 A1 D2
color: 101
CountryCode: US
SN: 11233/A0C123456
model: R3600
miot_did: 112233444
miot_key: aap0blsq5aQbVFmi
telnet_en: 1
ssh_en: 1
uart_en: 1
wl0_ssid: Xiaomi_EA69_5G
wl1_ssid: Xiaomi_EA69
wl2_ssid: Xiaomi_EA69
boot_wait: on

You’ll notice that the checksum is now updated and CountryCode, telnet_en, ssh_en, uart_en were all changed. Moreover, a new boot_wait config has been added.

Unfortunately, the bdata partition is read only. You need to make it writable prior to flashing the bdata partition or you’ll get a readonly error:

$ mtd write /tmp/bdata_mtd9.img bdata
Could not open mtd device: bdata
Can't open device for writing!

How? flash a new crash partition. Doing so opens the bdata partition for writing. I was (and still am) quite afraid to do that, since I have no clue what that crash partition contains. A random guy named barnamacko uploaded a crash partition that works on the OpenWRT forum. Flashing an unknown binary from an unknown source on your home router? why not! on a more serious note, I wasn’t keen to do that. I’m not sure what that binary contains, but I do know that it doesn’t modify the filesystem (I checked).

I recommend you physically connect to the router before performing any of these commands. Some people lost Wi-Fi connectivity when they flashed the crash partition (don’t worry, after removing it Wi-Fi works again!)

Go ahead and run the following on your computer:

$ scp ../crash/crash_unlock.img [email protected]:/tmp

Then login to the router and flash it:

$ mtd write /tmp/crash_unlock.img crash
$ reboot

post reboot, upload the modified bdata partition you created:

$ scp ../bdata/bdata_mtd9.img.modified [email protected]:/tmp

and flash it:

$ mtd write /tmp/bdata_mtd9.img.modified bdata
$ reboot

After reboot, login to your router again and remove the crash partition:

$ mtd erase crash

Then perform a factory reset on the device by surfing to:;stok=<STOK>/web/setting/upgrade. This step is important because otherwise you might bump into upgrade verification issues.

After the reset, you can upgrade to the latest firmware! If you do so, you’ll lose ssh connectivity. Don’t worry, you can connect to the device via telnet:

$ telnet 23
XiaoQiang login: root

Oh snap! what’s the password? well, after you patched the router and upgraded the firmware, the ssh password you set beforehand got reset. dear didiaoing created a website that generates the password for you! all you need to do is fill in the router’s SN number and get the default password. There’s nothing bad about sending your routers SN to a random website which performs all the calculation on the backend, right? I had to do some digging in order to find the algorithm that’s being usedd to generate those :)

thankfully, I found another chinese guy that goes by the alias zhoujiazhao, that wrote the algorithm in php. who does that these days?! PHP? really? me to the rescue, I ported it to python. Just run calc_passwd and you’re good to go:

$ ./ ../bdata/bdata_mtd9.img.modified

By the way, I haven’t encountered password issues, only read about them online. Once I got ssh acsess and reset the password, I flashed the international firmware and was able to connect with telnet.

Once you’ve got shell access, you can turn on ssh by running postman steps (4) to (7), or just reset the password to admin:

$ sed -i 's/channel=.*/channel=\"debug\"/g' /etc/init.d/dropbear
$ /etc/init.d/dropbear start
$ echo -e 'admin\nadmin' | passwd root

Moreover, if you do decide to flash the international firmware, you probably want to change the interface language to English:

$ uci set luci.main.lang='en'
$ uci commit

Don’t forget to turn off telnet after you verify ssh is working:

$ nvram set telnet_en=0
$ nvram commit

That’s it folks! I hope you won’t need to do any of this once the guys at OpenWRT finish porting it to AX3600 :)