PlayStation · Report #3452696
PS4 BD-J privilege escalation using nested JAR
Summary
Affected PS4 system software: version 13.00 to latest (13.02).
Core Issue:
- Security Policy (
BdjPolicyImpl): Canonicalizes the URL path usingFile.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:
-
Security Policy Path: Canonicalizes to
/app0/bdjstack/lib/ext/00000.jar- Matches trusted
lib/extdirectory - Result: AllPermission granted
- Matches trusted
-
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
- Located inside
Complete Code Flow Analysis
- 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:
-
URL contains path traversal:
/dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar -
File canonicalization resolves
../../:/app0/bdjstack/lib/ext/00000.jar -
Canonicalized path starts with
javaHome + "lib/ext":/app0/bdjstack/lib/ext -
Code is deemed trusted →
AllPermissiongranted -
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
| Stage | Path Used | Result |
|---|---|---|
| Security Check | /dsm/00000.jar/../../app0/bdjstack/lib/ext/00000.jar | Canonicalized → /app0/bdjstack/lib/ext/00000.jar |
| Permission Decision | Matches javaHome + "lib/ext" | AllPermission Granted |
| Actual Loading | Entry ../../app0/bdjstack/lib/ext/00000.jar in /VP/BDMV/JAR/00000.jar | Untrusted 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:
0x0000to0x3FFF(0 to 16,383)0x80000to0xFFFF(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:
-
Security Policy (
BdjPolicyImpl.getPermissions()):- Canonicalizes URL path using
File.getCanonicalPath() - Resolves
../../sequences - Checks against trusted
lib/extpath - Grants
AllPermissionbased on canonicalized path
- Canonicalizes URL path using
-
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
-
Result:
- Security check sees:
/app0/bdjstack/lib/ext/00000.jar(trusted) - Actual loading from: Entry
../../app0/bdjstack/lib/ext/00000.jarin/VP/BDMV/JAR/00000.jar(untrusted) - Time-of-Check/Time-of-Use (TOCTOU) vulnerability
- Untrusted code obtains
AllPermission
- Security check sees:
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.