Skip to main content  >
Hackerone logo
Hacktivity
Opportunities
Directory
Leaderboard
Learn more about HackerOne
Log in
#3452696
PS4 BD-J privilege escalation using nested JAR
  • Share:
Summary by PlayStation
Affected PS4 system software: version 13.00 to latest (13.02).
Core Issue:
  • Security Policy (BdjPolicyImpl): Canonicalizes the URL path using File.getCanonicalPath(), which resolves .. sequences
  • Class Loader (JarZipFile): Uses the URL path as a literal JAR entry name, where .. are just characters
Result: Code can appear to load from the trusted lib/ext directory while actually loading from an untrusted nested JAR on the Blu-ray disc.

Environment Configuration

CodeCode
Code•71 Bytes
1javaHome = "/app0/bdjstack/" 2vfsRoot = "/VP" (which is proxy for /disc)

The Vulnerability

Base Path: file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar
This URL triggers two different interpretations:
  1. Security Policy Path: Canonicalizes to /app0/bdjstack/lib/ext/00000.jar
    • Matches trusted lib/ext directory
    • Result: AllPermission granted
  2. Actual Loading Path: Nested JAR entry ../../app0/bdjstack/lib/ext/00000.jar
    • Located inside /VP/BDMV/JAR/00000.jar
    • Result: Loads from untrusted disc content

Complete Code Flow Analysis

  1. BdjPolicyImpl Security Check
CodeCode
Code•1.38 KiB
1public class BdjPolicyImpl extends Policy { 2 private static String javaHome = System.getProperty("java.home"); 3 // javaHome = "/app0/bdjstack/" 4 5 public PermissionCollection getPermissions(CodeSource paramCodeSource) { 6 if (paramCodeSource != null) { 7 URL uRL = paramCodeSource.getLocation(); 8 // uRL = "file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar" 9 10 if (uRL.getProtocol().equals("file")) { 11 try { 12 // CRITICAL: File canonicalization happens here! 13 String str = (new File(uRL.getFile())).getCanonicalPath(); 14 // uRL.getFile() = "/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar" 15 // Canonical path = "/app0/bdjstack/lib/ext/00000.jar" 16 17 if (str.startsWith(javaHome + "lib" + File.separator + "ext")) { 18 // "/app0/bdjstack/lib/ext/00000.jar".startsWith("/app0/bdjstack/lib/ext") 19 // TRUE - Grants AllPermission! 20 21 Permissions permissions1 = new Permissions(); 22 permissions1.add(new AllPermission()); 23 return permissions1; 24 } 25 } catch (Exception exception) {} 26 } 27 } 28 // ... return normal permissions 29 } 30}
Security Check Result: AllPermission Granted
The security policy believes the code is loaded from the trusted lib/ext directory because:
  1. URL contains path traversal: /dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar
  2. File canonicalization resolves ../../: /app0/bdjstack/lib/ext/00000.jar
  3. Canonicalized path starts with javaHome + "lib/ext": /app0/bdjstack/lib/ext
  4. Code is deemed trusted → AllPermission granted
  5. Actual Class Loading Path
Meanwhile, the actual class loading follows a completely different path:
Step 2.1: URL Registration
CodeCode
Code•467 Bytes
1// CoreApp.loadXlet() 2String basePath = getBasePath(paramLong1); 3// Returns: "file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar" 4 5// generateClasspath() detects ":" in path 6if (paramString.indexOf(":") != -1) { 7 return paramString; // Returns unchanged! 8} 9 10// XletClassLoader created 11URL[] arrayOfURL = { new URL("file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar") }; 12xletClassLoader = new XletClassLoader(arrayOfURL, false); // isSigned = false
Step 2.2: BDJFactory Proxy Routing
CodeCode
Code•270 Bytes
1// When URLClassPath tries to open the JAR 2String path = url.getFile(); 3// path = "/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar" 4 5if (BDJFactory.needProxy(path)) { 6 // Returns TRUE for /dsm/ paths 7 return factory.getZipFileDescriptor(appKey, path, mode); 8}
Step 2.3: JarDescriptorFactory Path Transformation
CodeCode
Code•820 Bytes
1public BDJZipFileProxy getZipFileDescriptor(int paramInt1, String paramString, int paramInt2) { 2 String str = getAbsPath(paramString); 3 // Input: "/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar" 4 return new JarZipFile(str, paramInt2, paramString); 5} 6 7private String getAbsPath(String paramString) { 8 // Branch 1: /dsm/ path transformation 9 if (paramString.startsWith("/dsm/") && 10 !paramString.startsWith("/dsm/app_base/")) { 11 12 // Strip "/dsm/" prefix 13 String relative = paramString.substring(5); 14 // relative = "00000.jar/../../app0/bdjstack/lib/ext/00000.jar" 15 16 // Prepend vfsRoot + "BDMV/JAR/" 17 str = vfsRoot + "BDMV/JAR/" + relative; 18 // str = "/VP/BDMV/JAR/00000.jar/../../app0/bdjstack/lib/ext/00000.jar" 19 } 20 return str; 21}
Step 2.4: JarZipFile Nested JAR Handling
CodeCode
Code•1.10 KiB
1public JarZipFile(String paramString1, int paramInt, String paramString2) throws IOException { 2 // paramString1 = "/VP/BDMV/JAR/00000.jar/../../app0/bdjstack/lib/ext/00000.jar" 3 4 // Find FIRST ".jar/" separator 5 int i = paramString1.indexOf(".jar/"); // i = 19 6 7 // Split into outer JAR and entry name 8 String str1 = paramString1.substring(0, i + 4); 9 // str1 = "/VP/BDMV/JAR/00000.jar" ← Physical file on disc 10 11 String str2 = paramString1.substring(i + 5); 12 // str2 = "../../app0/bdjstack/lib/ext/00000.jar" ← Entry name (literal!) 13 14 // Open outer JAR 15 this.jf = new JarFile(str1, true); 16 // Opens: /VP/BDMV/JAR/00000.jar 17 18 // Look for entry by EXACT name 19 this.ze = this.jf.getJarEntry(str2); 20 // Searches for entry: "../../app0/bdjstack/lib/ext/00000.jar" 21 22 if (this.ze == null) 23 throw new FileNotFoundException(str2); 24 25 // Verify it's a ZIP file 26 DataInputStream dis = new DataInputStream(this.jf.getInputStream(this.ze)); 27 if (dis.readInt() != 0x504B0304) // ZIP magic number 28 throw new ZipException("not a zip file"); 29}
Actual Loading Result: Classes are loaded from nested JAR entry inside /VP/BDMV/JAR/00000.jar
Step 2.5: Class Loading from Nested JAR
CodeCode
Code•532 Bytes
1// JarZipFile.getEntry() - loads classes from nested JAR 2public ZipEntry getEntry(String paramString) { 3 // Open nested JAR as stream 4 JarInputStream jarInputStream = new JarInputStream( 5 this.jf.getInputStream(this.ze) // Entry: "../../app0/bdjstack/lib/ext/00000.jar" 6 ); 7 8 // Find the class in the nested JAR 9 JarEntry jarEntry; 10 while ((jarEntry = jarInputStream.getNextJarEntry()) != null) { 11 if (paramString.equals(jarEntry.getName())) 12 return jarEntry; 13 } 14 return null; 15}

The Vulnerability

Time-of-Check / Time-of-Use (TOCTOU) Issue

StagePath UsedResult
Security Check/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jarCanonicalized → /app0/bdjstack/lib/ext/00000.jar
Permission DecisionMatches javaHome + "lib/ext"AllPermission Granted
Actual LoadingEntry ../../app0/bdjstack/lib/ext/00000.jar in /VP/BDMV/JAR/00000.jarUntrusted nested JAR

Why Unsigned JAR is Required

The BD-J stack has a bug in signature verification for nested JARs. When classes are loaded from a nested JAR accessed through JarInputStream:
CodeCode
Code•236 Bytes
1// XletClassLoader.defineClass1() 2Certificate[] arrayOfCertificate = paramResource.getCertificates(); 3 4if (arrayOfCertificate != null && !paramResource.isBDJVerified()) { 5 arrayOfCertificate = null; // Signature verification fails 6}

Application ID Range

CodeCode
Code•138 Bytes
1// CoreAppId.java 2public boolean isSigned() { 3 return (16384 <= this.aid && this.aid <= 524287); 4 // 0x4000 0x7FFFF 5}
Signed Range: 0x4000 (16,384) to 0x7FFFF (524,287)
To bypass signature verification, the applicationId in the BDJO file must be outside this range:
  • 0x0000 to 0x3FFF (0 to 16,383)
  • 0x80000 to 0xFFFF (524,288 to 65,535)
When isSigned() returns false, signature verification is skipped:
CodeCode
Code•448 Bytes
1// XletClassLoader.needsToVerify() 2private boolean needsToVerify() { 3 return getAppContext().getCoreAppId().isSigned(); // Returns false 4} 5 6// XletClassLoader.getPermissions() 7protected PermissionCollection getPermissions(CodeSource paramCodeSource) { 8 if (needsToVerify() && !RootCertManager.inKeyStore(arrayOfCertificate)) { 9 throw new SecurityException("cannot load unsigned class."); 10 } 11 // Skipped when isSigned() = false 12}

Path Transformation Tables

Security Check Path:
CodeCode
Code•410 Bytes
1Input: file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar 2 ↓ 3URL.getFile(): /dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar 4 ↓ 5File(): File(/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar) 6 ↓ 7Canonicalize: /app0/bdjstack/lib/ext/00000.jar 8 ↓ 9Check: startsWith("/app0/bdjstack/lib/ext") 10 ↓ 11Result: AllPermission
Class Loading Path:
CodeCode
Code•521 Bytes
1Input: file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar 2 ↓ 3needProxy: TRUE (/dsm/ path) 4 ↓ 5getAbsPath: /VP/BDMV/JAR/00000.jar/../../app0/bdjstack/lib/ext/00000.jar 6 ↓ 7Split: Outer = /VP/BDMV/JAR/00000.jar 8 Entry = ../../app0/bdjstack/lib/ext/00000.jar 9 ↓ 10Open: JarFile("/VP/BDMV/JAR/00000.jar") 11 ↓ 12Lookup: getJarEntry("../../app0/bdjstack/lib/ext/00000.jar") 13 ↓ 14Result: Load from nested JAR entry

Summary

This vulnerability demonstrates a critical flaw where:
  1. Security Policy (BdjPolicyImpl.getPermissions()):
    • Canonicalizes URL path using File.getCanonicalPath()
    • Resolves ../../ sequences
    • Checks against trusted lib/ext path
    • Grants AllPermission based on canonicalized path
  2. Class Loading (JarZipFile):
    • Treats URL path as literal JAR entry name
    • ../../ are just characters, not path operators
    • Loads from nested JAR entry on Blu-ray disc
    • Content is untrusted
  3. Result:
    • Security check sees: /app0/bdjstack/lib/ext/00000.jar (trusted)
    • Actual loading from: Entry ../../app0/bdjstack/lib/ext/00000.jar in /VP/BDMV/JAR/00000.jar (untrusted)
    • Time-of-Check/Time-of-Use (TOCTOU) vulnerability
    • Untrusted code obtains AllPermission

Impact

Complete privilege escalation from sandboxed Blu-ray application to full system access with AllPermission.
Show more
Timeline
gezine
Clear-verified badgeHacker that has completed both ID-verification and a background check.
gezine
submitted a report to PlayStation.
December 5, 2025, 7:47am UTC
h1_analyst_dallas
HackerOne triage
 changed the status to
Pending program review
. 
December 6, 2025, 4:34pm UTC
gezine
 posted a comment. 
January 30, 2026, 12:42am UTC
hacker-01
PlayStation staff
 changed the status to Triaged. 
February 23, 2026, 11:53pm UTC
hacker-01
PlayStation staff
 updated the severity to medium. 
March 20, 2026, 11:48pm UTC
PlayStation
 rewarded gezine with a $2,500 bounty. 
March 20, 2026, 11:49pm UTC
gezine
 posted a comment. 
March 21, 2026, 12:37am UTC
shoshin_cup
PlayStation staff
 closed the report and changed the status to Resolved. 
March 31, 2026, 6:57pm UTC
shoshin_cup
PlayStation staff
 posted a comment. 
March 31, 2026, 7:13pm UTC
gezine
 requested to disclose this report. 
March 31, 2026, 7:17pm UTC
gezine
 posted a comment. 
March 31, 2026, 7:19pm UTC
hacker-01
PlayStation staff
 posted a comment. 
April 1, 2026, 11:44pm UTC
hacker-01
PlayStation staff
 agreed to disclose this report. 
4 days ago
 This report has been disclosed. 
4 days ago
gezine
 posted a comment. 
4 days ago
hacker-01
PlayStation staff
 posted a comment. 
4 days ago
shoshin_cup
PlayStation staff
 posted a comment. 
3 days ago
gezine
 posted a comment. 
2 days ago
Reported on
December 5, 2025, 7:45am UTC
Reported by
gezine
gezine
Reported to
PlayStation
Managed
Participants
gezine
shoshin_cup
hacker-01
h1_analyst_dallas
Report Id
#3452696
Resolved
Severity
Medium (4 ~ 6.9)

Disclosed
April 29, 2026, 5:09am UTC
Weakness
Privilege Escalation
CVE ID
None

Bounty
$2,500
Retest
None
Total reward
None

Account details
None

Validation Agent Reasoning

No validation agent reasoning available for this report.
Disagree with the reasoning?

Exploit Agent Reasoning

No exploit agent reasoning available for this report.

Linear Agent Reasoning

No linear agent reasoning available for this report.