HexBrain

PlayStation · Report #3452696

PS4 BD-J privilege escalation using nested JAR

Validstatus
mediumseverity
$2,500bounty
86upvotes
29 Apr 2026disclosed

Summary

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

javaHome = "/app0/bdjstack/"
vfsRoot = "/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
public class BdjPolicyImpl extends Policy {
    private static String javaHome = System.getProperty("java.home");
    // javaHome = "/app0/bdjstack/"
    
    public PermissionCollection getPermissions(CodeSource paramCodeSource) {
        if (paramCodeSource != null) {
            URL uRL = paramCodeSource.getLocation();
            // uRL = "file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar"
            
            if (uRL.getProtocol().equals("file")) {
                try {
                    // CRITICAL: File canonicalization happens here!
                    String str = (new File(uRL.getFile())).getCanonicalPath();
                    // uRL.getFile() = "/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar"
                    // Canonical path = "/app0/bdjstack/lib/ext/00000.jar"
                    
                    if (str.startsWith(javaHome + "lib" + File.separator + "ext")) {
                        // "/app0/bdjstack/lib/ext/00000.jar".startsWith("/app0/bdjstack/lib/ext")
                        // TRUE - Grants AllPermission!
                        
                        Permissions permissions1 = new Permissions();
                        permissions1.add(new AllPermission());
                        return permissions1;
                    }
                } catch (Exception exception) {}
            }
        }
        // ... return normal permissions
    }
}

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

// CoreApp.loadXlet()
String basePath = getBasePath(paramLong1);
// Returns: "file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar"

// generateClasspath() detects ":" in path
if (paramString.indexOf(":") != -1) {
    return paramString;  // Returns unchanged!
}

// XletClassLoader created
URL[] arrayOfURL = { new URL("file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar") };
xletClassLoader = new XletClassLoader(arrayOfURL, false);  // isSigned = false

Step 2.2: BDJFactory Proxy Routing

// When URLClassPath tries to open the JAR
String path = url.getFile();
// path = "/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar"

if (BDJFactory.needProxy(path)) {
    // Returns TRUE for /dsm/ paths
    return factory.getZipFileDescriptor(appKey, path, mode);
}

Step 2.3: JarDescriptorFactory Path Transformation

public BDJZipFileProxy getZipFileDescriptor(int paramInt1, String paramString, int paramInt2) {
    String str = getAbsPath(paramString);
    // Input: "/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar"
    return new JarZipFile(str, paramInt2, paramString);
}

private String getAbsPath(String paramString) {
    // Branch 1: /dsm/ path transformation
    if (paramString.startsWith("/dsm/") && 
        !paramString.startsWith("/dsm/app_base/")) {
        
        // Strip "/dsm/" prefix
        String relative = paramString.substring(5);
        // relative = "00000.jar/../../app0/bdjstack/lib/ext/00000.jar"
        
        // Prepend vfsRoot + "BDMV/JAR/"
        str = vfsRoot + "BDMV/JAR/" + relative;
        // str = "/VP/BDMV/JAR/00000.jar/../../app0/bdjstack/lib/ext/00000.jar"
    }
    return str;
}

Step 2.4: JarZipFile Nested JAR Handling

public JarZipFile(String paramString1, int paramInt, String paramString2) throws IOException {
    // paramString1 = "/VP/BDMV/JAR/00000.jar/../../app0/bdjstack/lib/ext/00000.jar"
    
    // Find FIRST ".jar/" separator
    int i = paramString1.indexOf(".jar/");  // i = 19
    
    // Split into outer JAR and entry name
    String str1 = paramString1.substring(0, i + 4);
    // str1 = "/VP/BDMV/JAR/00000.jar"  ← Physical file on disc
    
    String str2 = paramString1.substring(i + 5);
    // str2 = "../../app0/bdjstack/lib/ext/00000.jar"  ← Entry name (literal!)
    
    // Open outer JAR
    this.jf = new JarFile(str1, true);
    // Opens: /VP/BDMV/JAR/00000.jar
    
    // Look for entry by EXACT name
    this.ze = this.jf.getJarEntry(str2);
    // Searches for entry: "../../app0/bdjstack/lib/ext/00000.jar"
    
    if (this.ze == null)
        throw new FileNotFoundException(str2);
    
    // Verify it's a ZIP file
    DataInputStream dis = new DataInputStream(this.jf.getInputStream(this.ze));
    if (dis.readInt() != 0x504B0304)  // ZIP magic number
        throw new ZipException("not a zip file");
}

Actual Loading Result: Classes are loaded from nested JAR entry inside /VP/BDMV/JAR/00000.jar

Step 2.5: Class Loading from Nested JAR

// JarZipFile.getEntry() - loads classes from nested JAR
public ZipEntry getEntry(String paramString) {
    // Open nested JAR as stream
    JarInputStream jarInputStream = new JarInputStream(
        this.jf.getInputStream(this.ze)  // Entry: "../../app0/bdjstack/lib/ext/00000.jar"
    );
    
    // Find the class in the nested JAR
    JarEntry jarEntry;
    while ((jarEntry = jarInputStream.getNextJarEntry()) != null) {
        if (paramString.equals(jarEntry.getName()))
            return jarEntry;
    }
    return null;
}

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:

// XletClassLoader.defineClass1()
Certificate[] arrayOfCertificate = paramResource.getCertificates();

if (arrayOfCertificate != null && !paramResource.isBDJVerified()) {
    arrayOfCertificate = null;  // Signature verification fails
}

Application ID Range

// CoreAppId.java
public boolean isSigned() {
    return (16384 <= this.aid && this.aid <= 524287);
    //      0x4000           0x7FFFF
}

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:

// XletClassLoader.needsToVerify()
private boolean needsToVerify() {
    return getAppContext().getCoreAppId().isSigned();  // Returns false
}

// XletClassLoader.getPermissions()
protected PermissionCollection getPermissions(CodeSource paramCodeSource) {
    if (needsToVerify() && !RootCertManager.inKeyStore(arrayOfCertificate)) {
        throw new SecurityException("cannot load unsigned class.");
    }
    // Skipped when isSigned() = false
}

Path Transformation Tables

Security Check Path:

Input:       file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar
             ↓
URL.getFile(): /dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar
             ↓
File():      File(/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar)
             ↓
Canonicalize: /app0/bdjstack/lib/ext/00000.jar
             ↓
Check:       startsWith("/app0/bdjstack/lib/ext") 
             ↓
Result:      AllPermission

Class Loading Path:

Input:       file:/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar
             ↓
needProxy:   TRUE (/dsm/ path)
             ↓
getAbsPath:  /VP/BDMV/JAR/00000.jar/../../app0/bdjstack/lib/ext/00000.jar
             ↓
Split:       Outer = /VP/BDMV/JAR/00000.jar
             Entry = ../../app0/bdjstack/lib/ext/00000.jar
             ↓
Open:        JarFile("/VP/BDMV/JAR/00000.jar")
             ↓
Lookup:      getJarEntry("../../app0/bdjstack/lib/ext/00000.jar")
             ↓
Result:      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.

How it was found

The write-up for this report has not been downloaded yet, or the program disclosed it without one. Read it on HackerOne.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original