Published on

PCC CTF - Flag Generator Reverse Engineering Writeup

Authors
  • avatar
    Name
    Muhammad Huzaifa
    Twitter

PCC Flag Generator - Reverse Engineering Writeup

Challenge Overview

Challenge: PCC Flag Generator
URL: http://c1.arena.airoverflow.com:39357/
Type: Java Reverse Engineering
Flag Format: PCC{}

Initial Analysis

The challenge provides a Spring Boot application (application.jar) that requires a premium license key to generate flags. The application is protected with ClassFinal obfuscation.

Files Provided

  • application.jar - Main Spring Boot application
  • license.key - Existing license file
  • flag.txt - Contains test flag: PCC{fak3_fl4g_f0r_73st1ng}
  • Dockerfile - Shows the application runs with ClassFinal agent

Reverse Engineering Process

1. Understanding the Application Structure

The JAR file contains a Spring Boot application with the following key components:

  • com.ctf.premium.controller.PremiumController - Handles web requests
  • com.ctf.premium.util.LicenseManager - Manages license validation
  • com.ctf.premium.config.SecurityConfig - Security configuration

2. Bypassing ClassFinal Obfuscation

The application uses ClassFinal obfuscation with password pcc2025. I created a decryption utility to extract the real bytecode:

DecryptJar.java
// DecryptJar.java - ClassFinal bypass utility
import java.io.*;
import java.util.jar.*;
import java.nio.file.*;

public class DecryptJar {
    public static void main(String[] args) throws Exception {
        String jarPath = "application.jar";
        String password = "pcc2025";
        
        net.roseboy.classfinal.JarDecryptor decryptor = net.roseboy.classfinal.JarDecryptor.getInstance();
        
        String[] classNames = {
            "com.ctf.premium.controller.PremiumController",
            "com.ctf.premium.util.LicenseManager",
            "com.ctf.premium.config.SecurityConfig"
        };
        
        for (String className : classNames) {
            byte[] decryptedBytes = decryptor.doDecrypt(jarPath, className, password.toCharArray());
            if (decryptedBytes != null) {
                String outputPath = "decrypted_" + className.replace('.', '_') + ".class";
                Files.write(Paths.get(outputPath), decryptedBytes);
                System.out.println("Decrypted " + className);
            }
        }
    }
}

3. Analyzing the License Validation Logic

After decompiling the decrypted classes, I discovered the validation mechanism:

LicenseManager.validateLicense()

public static boolean validateLicense(String licenseKey) {
    String decrypted = decrypt(licenseKey);
    ObjectMapper mapper = new ObjectMapper();
    Map<String, Object> jsonMap = mapper.readValue(decrypted, Map.class);
    
    boolean ispremium = Boolean.TRUE.equals(jsonMap.get("ispremium"));
    boolean isnoob = Boolean.TRUE.equals(jsonMap.get("isnoob"));
    
    // Check valid_until timestamp
    Object validUntilObj = jsonMap.get("valid_until");
    boolean isValidTime = true;
    if (validUntilObj instanceof Number) {
        long validUntil = ((Number) validUntilObj).longValue();
        long currentTime = System.currentTimeMillis() / 1000;
        isValidTime = currentTime <= validUntil;
    }
    
    return ispremium && isnoob && isValidTime;
}

AES Key Discovery

The encryption key is constructed by concatenating 4 character arrays:

private static byte[] getSecretKeyBytes() {
    // Array 1: [80, 67, 67, 50] = "PCC2"
    // Array 2: [53, 80, 82, 69] = "5PRE"  
    // Array 3: [77, 73, 85, 77] = "MIUM"
    // Array 4: [35, 49, 65, 50] = "#1A2"
    // Final key: "PCC25PREMIUM#1A2"
}

4. Existing License Analysis

The provided license key decrypts to:

{
    "ispremium": false,
    "valid_until": 1792245400,
    "version": "1.0",
    "isnoob": true
}

The ispremium: false explains why it doesn't grant access.

Exploitation

License Key Generator

I created a utility to generate valid license keys:

GenerateLicense.java
// GenerateLicense.java - License key generator
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class GenerateLicense {
    public static void main(String[] args) throws Exception {
        long currentTime = System.currentTimeMillis() / 1000;
        long futureTime = currentTime + 31536000; // 1 year from now
        
        String jsonPayload = String.format(
            "{\"ispremium\":true,\"isnoob\":true,\"valid_until\":%d}", 
            futureTime
        );
        
        String encryptedLicense = encrypt(jsonPayload);
        System.out.println("Valid License Key: " + encryptedLicense);
    }
    
    private static byte[] getSecretKeyBytes() {
        return "PCC25PREMIUM#1A2".getBytes(StandardCharsets.UTF_8);
    }
    
    private static String encrypt(String plaintext) throws Exception {
        SecretKeySpec keySpec = new SecretKeySpec(getSecretKeyBytes(), "AES");
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, keySpec);
        
        byte[] encryptedBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }
}

Generated Valid License Keys

  1. Short-term: yTeTwj8e+3/t4kjREsgXZh3SfQki/lJVxafCK4NBCVUHZB1ODYnv5JEHtmmSDgyD+mCwfSU0wUgJgfTetSNkBA==
  2. Long-term: yTeTwj8e+3/t4kjREsgXZh3SfQki/lJVxafCK4NBCVUHZB1ODYnv5JEHtmmSDgyDs/+wB6cDcsTOBSNwt84DKw==
  3. Exact format: NAVYhRIvUivQJrx5sTYbpMe5+lONt9Kg/n7KEBkm0vaVXDkTZVKN7SpGwcuWkhtaJTKtJbui9s//S8QouEnRddH1qjNp7q1yScRsWpGdt+OYqLu4446nV9Tja0VUgstW

All keys decrypt to valid JSON:

{
    "ispremium": true,
    "isnoob": true,
    "valid_until": [future_timestamp]
}

Application Flow

  1. Home Page: Displays current license and form for validation
  2. License Validation: POST to /validate endpoint
  3. Success: Redirects with flash attributes containing flag
  4. Flag Display: Shows flag from flag.txt when premiumFeatures is true

Technical Details

  • Encryption: AES-128 ECB mode with PKCS5 padding
  • Key: "PCC25PREMIUM#1A2" (16 bytes)
  • Encoding: Base64
  • Validation: JSON parsing with Jackson ObjectMapper
  • Obfuscation: ClassFinal with password "pcc2025"

Conclusion

This challenge demonstrates:

  • Java bytecode reverse engineering
  • ClassFinal obfuscation bypass techniques
  • AES encryption/decryption analysis
  • JSON validation logic understanding
  • Spring Boot application structure analysis

The reverse engineering process successfully identified the validation mechanism and generated cryptographically valid license keys. The application reads the flag from flag.txt when a valid premium license is provided.

Files

  • DecryptJar.java - ClassFinal bypass utility
  • GenerateLicense.java - License key generator
  • DecryptExistingLicense.java - Existing license analyzer
  • TestLicense.java - License validation tester