diff --git a/pom.xml b/pom.xml index b3b1536..e3e64d2 100644 --- a/pom.xml +++ b/pom.xml @@ -70,7 +70,7 @@ commons-cli commons-cli - 1.4 + 1.6.0 javax.xml.bind @@ -80,12 +80,24 @@ org.javassist javassist - 3.21.0-GA + 3.30.2-GA com.sun.activation javax.activation 1.2.0 + + junit + junit + 4.13.2 + test + + + commons-codec + commons-codec + 1.15 + test + \ No newline at end of file diff --git a/src/main/java/io/zhile/crack/atlassian/agent/KeyTransformer.java b/src/main/java/io/zhile/crack/atlassian/agent/KeyTransformer.java index 1626542..e0d4fcc 100644 --- a/src/main/java/io/zhile/crack/atlassian/agent/KeyTransformer.java +++ b/src/main/java/io/zhile/crack/atlassian/agent/KeyTransformer.java @@ -1,13 +1,14 @@ package io.zhile.crack.atlassian.agent; + import javassist.*; -import java.io.File; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; import java.util.Arrays; -import java.util.Map; -import java.util.Objects; +import java.io.ByteArrayInputStream; // Import for the inserted code to resolve +import java.io.DataInputStream; // Import for the inserted code to resolve +import java.nio.charset.StandardCharsets; // Import for the inserted code to resolve /** * @author pengzhile @@ -29,7 +30,7 @@ public byte[] transform(ClassLoader loader, String className, Class classBein if (className.equals(CN_KEY_SPEC)) { return handleKeySpec(); } else if (className.equals(LICENSE_DECODER_PATH)) { - return handleLicenseDecoder(); + return handleLicenseDecoder(loader); } return classfileBuffer; @@ -74,24 +75,16 @@ private byte[] handleKeySpec() throws IllegalClassFormatException { * @return 修改过的类的字节码 * @throws IllegalClassFormatException 当某些地方出问题了就会抛出这个异常 */ - private byte[] handleLicenseDecoder() throws IllegalClassFormatException { + private byte[] handleLicenseDecoder(ClassLoader loader) throws IllegalClassFormatException { try { - // 我不知道怎么从 com.atlassian.bitbucket.internal.launcher.BitbucketServerLauncher 读取这个路径,所以我直接 HARD CODE - // Forgive me pls... - - Map osEnv = System.getenv(); - String binDir = osEnv.get("BIN_DIR"); - String path = binDir.replace("bin", "app/WEB-INF/lib"); - File libs = new File(path); - ClassPool cp = ClassPool.getDefault(); + // Use a ClassPool that defaults to the system classpath (includes JDK classes) + ClassPool cp = new ClassPool(true); - Arrays.stream(Objects.requireNonNull(libs.listFiles())).map(File::getAbsolutePath).forEach((it) -> { - try { - cp.insertClassPath(it); - } catch (Exception e) { - e.printStackTrace(); - } - }); + // Append the classpath of the loader that loaded the target class. + // This ensures we can resolve classes available to the target class (e.g. libraries in WEB-INF/lib). + if (loader != null) { + cp.appendClassPath(new LoaderClassPath(loader)); + } cp.importPackage("com.atlassian.extras.common.LicenseException"); cp.importPackage("com.atlassian.extras.common.org.springframework.util.DefaultPropertiesPersister"); @@ -120,7 +113,7 @@ private byte[] handleLicenseDecoder() throws IllegalClassFormatException { cp.importPackage("java.util.zip.InflaterInputStream"); cp.importPackage("org.apache.commons.codec.binary.Base64"); - CtClass target = cp.getCtClass(LICENSE_DECODER_CLASS); + CtClass target = cp.get(LICENSE_DECODER_CLASS); CtMethod verifyLicenseHash = target.getDeclaredMethod("verifyLicenseHash"); verifyLicenseHash.setBody("{System.out.println(\"atlassian-agent: skip license hash check\");}"); CtMethod checkAndGetLicenseText = target.getDeclaredMethod("checkAndGetLicenseText"); @@ -144,4 +137,4 @@ private byte[] handleLicenseDecoder() throws IllegalClassFormatException { } } -} \ No newline at end of file +} diff --git a/src/test/java/io/zhile/crack/atlassian/agent/KeyTransformerTest.java b/src/test/java/io/zhile/crack/atlassian/agent/KeyTransformerTest.java new file mode 100644 index 0000000..1ba9ec8 --- /dev/null +++ b/src/test/java/io/zhile/crack/atlassian/agent/KeyTransformerTest.java @@ -0,0 +1,104 @@ +package io.zhile.crack.atlassian.agent; + +import org.junit.Test; +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import static org.junit.Assert.*; + +public class KeyTransformerTest { + + @Test + public void testHandleLicenseDecoder() throws Exception { + // 1. Setup temporary directory for dummy classes + Path tempPath = Files.createTempDirectory("agent_test_classes"); + File tempDir = tempPath.toFile(); + tempDir.deleteOnExit(); + + try { + // 2. Create Version2LicenseDecoder dummy class + File packageDir = new File(tempDir, "com/atlassian/extras/decoder/v2"); + assertTrue(packageDir.mkdirs()); + + File sourceFile = new File(packageDir, "Version2LicenseDecoder.java"); + Files.write(sourceFile.toPath(), + ("package com.atlassian.extras.decoder.v2;\n" + + "public class Version2LicenseDecoder {\n" + + " public void verifyLicenseHash() {}\n" + + " public String checkAndGetLicenseText(String s) { return \"\"; }\n" + + "}\n").getBytes()); + + runJavac(sourceFile); + + // 3. Create LicenseException dummy class + File commonDir = new File(tempDir, "com/atlassian/extras/common"); + assertTrue(commonDir.mkdirs()); + + File leSourceFile = new File(commonDir, "LicenseException.java"); + Files.write(leSourceFile.toPath(), + ("package com.atlassian.extras.common;\n" + + "public class LicenseException extends RuntimeException {\n" + + " public LicenseException(Throwable t) { super(t); }\n" + + "}\n").getBytes()); + + runJavac(leSourceFile); + + // 4. Create custom ClassLoader + URL[] urls = new URL[] { tempDir.toURI().toURL() }; + // Use the current test classloader as parent so it can find commons-codec (which is in test dependencies) + ClassLoader parentLoader = getClass().getClassLoader(); + URLClassLoader appLoader = new URLClassLoader(urls, parentLoader); + + // 5. Instantiate Transformer + KeyTransformer transformer = new KeyTransformer(); + String className = "com/atlassian/extras/decoder/v2/Version2LicenseDecoder"; + + // 6. Transform + byte[] dummyBytes = new byte[0]; + byte[] result = transformer.transform(appLoader, className, null, null, dummyBytes); + + assertNotNull("Transformation result should not be null", result); + assertTrue("Transformation result should not be empty", result.length > 0); + + } finally { + // Cleanup + Files.walk(tempPath) + .sorted(Comparator.reverseOrder()) + .map(Path::toFile) + .forEach(File::delete); + } + } + + @Test + public void testHandleKeySpec() throws Exception { + // This test verifies that handleKeySpec returns transformed bytecode. + // It relies on java.security.spec.EncodedKeySpec being available in the system (it is). + // And javassist finding it (via ClassPool.getDefault()). + + KeyTransformer transformer = new KeyTransformer(); + String className = "java/security/spec/EncodedKeySpec"; + + // We pass empty byte array. The transformer uses ClassPool to read the class. + byte[] dummyBytes = new byte[0]; + + // Note: transform takes "className" with slashes + byte[] result = transformer.transform(ClassLoader.getSystemClassLoader(), className, null, null, dummyBytes); + + assertNotNull("Transformation result should not be null for EncodedKeySpec", result); + assertTrue("Transformation result should not be empty for EncodedKeySpec", result.length > 0); + } + + private void runJavac(File sourceFile) throws Exception { + Process p = Runtime.getRuntime().exec("javac " + sourceFile.getAbsolutePath()); + p.waitFor(); + if (p.exitValue() != 0) { + // Read stderr + java.util.Scanner s = new java.util.Scanner(p.getErrorStream()).useDelimiter("\\A"); + String err = s.hasNext() ? s.next() : ""; + throw new RuntimeException("Failed to compile " + sourceFile.getName() + ": " + err); + } + } +} diff --git a/src/test/java/io/zhile/crack/atlassian/agent/ManualAgentVerify.java b/src/test/java/io/zhile/crack/atlassian/agent/ManualAgentVerify.java new file mode 100644 index 0000000..093c074 --- /dev/null +++ b/src/test/java/io/zhile/crack/atlassian/agent/ManualAgentVerify.java @@ -0,0 +1,29 @@ +package io.zhile.crack.atlassian.agent; + +import java.security.spec.EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * Manual verification script. + * Usage: + * java -javaagent:target/atlassian-agent.jar -cp target/test-classes io.zhile.crack.atlassian.agent.ManualAgentVerify + */ +public class ManualAgentVerify { + public static void main(String[] args) { + System.out.println("Starting ManualAgentVerify..."); + + // This key matches the one hardcoded in KeyTransformer to trigger the "agent working" message + String keyBase64 = "MIIBuDCCASwGByqGSM44BAEwggEfAoGBAP1/U4EddRIpUt9KnC7s5Of2EbdSPO9EAMMeP4C2USZpRV1AIlH7WT2NWPq/xfW6MPbLm1Vs14E7gB00b/JmYLdrmVClpJ+f6AR7ECLCT7up1/63xhv4O1fnxqimFQ8E+4P208UewwI1VBNaFpEy9nXzrith1yrv8iIDGZ3RSAHHAhUAl2BQjxUjC8yykrmCouuEC/BYHPUCgYEA9+GghdabPd7LvKtcNrhXuXmUr7v6OuqC+VdMCz0HgmdRWVeOutRZT+ZxBxCBgLRJFnEj6EwoFhO3zwkyjMim4TwWeotUfI0o4KOuHiuzpnWRbqN/C/ohNWLx+2J6ASQ7zKTxvqhRkImog9/hWuWfBpKLZl6Ae1UlZAFMO/7PSSoDgYUAAoGBAIvfweZvmGo5otwawI3no7Udanxal3hX2haw962KL/nHQrnC4FG2PvUFf34OecSK1KtHDPQoSQ+DHrfdf6vKUJphw0Kn3gXm4LS8VK/LrY7on/wh2iUobS2XlhuIqEc5mLAUu9Hd+1qxsQkQ50d0lzKrnDqPsM0WA9htkdJJw2nS"; + byte[] key = Base64.getDecoder().decode(keyBase64); + + try { + // X509EncodedKeySpec extends EncodedKeySpec. + // Creating this instance should trigger the agent transformation if attached. + EncodedKeySpec spec = new X509EncodedKeySpec(key); + System.out.println("Created EncodedKeySpec instance."); + } catch (Exception e) { + e.printStackTrace(); + } + } +}