The CVE Needed a Patch. The Platform Needed a Redesign.

A public Redis exploit targeted the wrong build. Shinobi adapted it, proved remote command execution, and uncovered a tenant-isolation failure that a Redis upgrade alone would not fix.

Table of Contents

A public exploit targeted the wrong Redis build. Shinobi adapted it, debugged it locally, and proved that access to a database on an internal platform-as-a-service could expose the platform's own secrets.

The target was an internal platform-as-a-service (PaaS) operated by the customer. The part Shinobi tested let internal users create and manage Redis databases through a self-service web console. A user could sign in, create a database, obtain its password and connect to it.

The platform grouped users and their databases into tenants: logical groups within the internal PaaS, not separate external customers. Access to a tenant's own databases was expected. Access to other tenants' resources or the platform's management application was not.

During the test, Shinobi found that the platform was running a Redis version affected by RediShell, CVE-2025-49844. The vulnerability could allow remote code execution (RCE): a user could turn Redis scripting access into commands running on the underlying operating system. But knowing the version was affected did not establish whether an exploit would work on this deployment, or what the compromised process could access.

That became the investigation. Shinobi evaluated public exploit code, identified a matching Redis binary, adapted the exploit and built a local debugging environment. Once it proved command execution, it followed the access further and found a separate problem with how the application isolated its tenants.

This is the work between identifying a known vulnerability and delivering a reproduction that shows what it means for the application.

The code, commands and output below are selected excerpts from the recorded test. Customer addresses, credentials and tenant identifiers are omitted. Customer-specific account names are replaced with {SANITISED}. Complete exploit payloads are not included.

Start with the database a tenant is allowed to use

Shinobi initially tested the internal PaaS web console for command injection. Because the platform created database processes and managed their configuration, the agent checked whether inputs such as database names and access-control rules could cause the server to execute shell commands. Those probes did not establish command execution.

The databases themselves were another route to investigate. Using an account on the internal PaaS, Shinobi could create a test database through the normal self-service workflow, obtain its credentials and query the Redis server directly. The response identified the software actually running:

redis_version:7.4.2
os:Linux 6.8.0-124-generic x86_64
arch_bits:64

The web console's database metadata said 8.6, but the process answering Redis commands was 7.4.2. For the exploit investigation, the running process was what mattered.

Redis supports Lua scripts, allowing clients to run logic inside the database. The intended restriction is that those scripts operate inside Redis's Lua sandbox, not as unrestricted programs on the underlying operating system.

Several sandbox restrictions were present in this deployment. The Lua functions and libraries normally useful for accessing files or starting commands, including io, package, require and os.execute, were unavailable. Redis's MODULE LOAD and DEBUG commands were disabled. Protected configuration values such as dir and dbfilename could not be changed, and attempts to load binary Lua bytecode failed. The tenant could still run Lua source through EVAL and EVALSHA.

RediShell was relevant because it exploited a use-after-free (UAF): a memory-management error inside the Lua interpreter. It did not depend on os.execute being available. The question was whether Shinobi could use that error to get outside the sandbox on this particular server.

Give the Redis investigation its own agent

The Redis work was becoming a different job from testing the web console. It needed research, binary inspection and potentially exploit development. The main agent therefore delegated it to a specialist subagent named redis-rce, while continuing the web tests itself.

The main agent recorded the decision:

Offload the complex RediShell UAF exploit development to a dedicated pro agent while I close remaining web surface; this is the top remaining RCE avenue and requires deep iterative exploit work

The assignment included the Redis version, the sandbox restrictions already discovered and the objective: demonstrate command execution on a test instance. The subagent created a fresh Redis database through the console and obtained its password through the normal password-rotation action.

From here, the roles were straightforward. The main agent continued examining the application. The redis-rce subagent concentrated on making the Redis exploit work.

Inspect the exploit, not just the README

The subagent's assignment suggested a starting point: lastvocher/redis-CVE-2025-49844, a public GitHub repository containing a RediShell lab and a Python script named exploit_poc.py.

Shinobi downloaded the repository and read both its README and the script. That inspection mattered. The repository described the script as a simplified demonstration. It did not implement the native command execution Shinobi needed to prove. One section ended with:

  -- The real exploit would now execute arbitrary code

  return "Memory corruption pattern completed"

Returning that string would not show that an operating-system command had run. The code itself said the real exploitation step was missing. Shinobi did not use it as the basis for a successful finding.

Instead, the subagent examined Redis's security patch and other public implementations. One of those repositories, saneki/cve-2025-49844, contained an exploit implementation for two x86-64 Docker builds: redis:8.2.1-alpine and redis:8.2.1-bookworm.

That was a useful starting point, but it introduced the next problem. The customer's Redis server was version 7.4.2, not either of those 8.2.1 builds. Shinobi would need to establish that the underlying bug was present, then adapt the build-specific parts of the exploit.

Confirm the Lua bug before adapting the full exploit

To understand the failure, the subagent downloaded the source-code changes between Redis 7.4.5 and 7.4.6. Among them was a change to luaY_parser, the function that parses Lua code.

The relevant object was the chunk name: a string Lua uses to identify a piece of code being parsed. In the vulnerable implementation, the parser could still be using that string when the garbage collector reclaimed its memory. Continuing to use an object after its memory has been freed is the use-after-free in RediShell.

This excerpt shows how the patch changed the handling of that string:

-  luaX_setinput(L, &lexstate, z, luaS_new(L, name));
+  TString *tname = luaS_new(L, name);
+  setsvalue2s(L, L->top, tname);
+  incr_top(L);
+  luaX_setinput(L, &lexstate, z, tname);

The added lines place the string on the Lua stack before passing it to the lexer. That keeps it visible to the garbage collector as an object still in use. The patch removes the temporary stack reference after parsing finishes.

Shinobi then adapted a small demonstration from saneki's repository to test this behaviour on its remote test database. The check attempted to reclaim the chunk-name memory and replace its contents. Without that replacement, the expected name was =(load). The result was:

[+] Flushing scripts
[+] Uploading script
[+] Replaced chunkname -> b'v000001'

The returned name was now the replacement value, v000001. That gave Shinobi evidence that the underlying memory-reuse behaviour worked on the customer's Redis build. It still had not executed an operating-system command. The next job was to turn that behaviour into controlled native execution.

Match the build, then adapt the exploit

The public exploit depended on details of the executable it had been written for. Among them were the locations of native functions and short machine-code sequences used to redirect execution. Those locations could not simply be copied from Redis 8.2.1 into an exploit for 7.4.2.

Shinobi needed a matching copy of the customer's Redis executable to inspect. The remote server supplied a useful identifier through INFO:

redis_build_id:b648dcb34d0d916f

The agent downloaded candidate Redis Stack packages and extracted the redis-server executable from each. Redis Stack package revisions can contain different Redis server builds, so the agent compared more than the package name or the server's version number.

It read how Redis calculated its build identifier, ported the CRC64 checksum routine to Python and checked the routine against a known test vector. It then used that routine on the build strings embedded in the candidate executables. Two package revisions produced:

v2: raw='7.4.2d2528019ee7d-1736235236000000000' crc64=9d20e270b397bf67  MATCH=no
v3: raw='7.4.2081887c6024e-1738939766000000000' crc64=b648dcb34d0d916f  MATCH=YES

Both contained Redis 7.4.2. Only v3 matched the identifier reported by the target. The matching package was redis-stack-server-7.4.0-v3.jammy.amd64.deb. This was a match of Redis's build identifier, not a byte-for-byte comparison with a file downloaded from the remote server.

With that executable available locally, Shinobi used nm to inspect its symbols and objdump to inspect its machine instructions. It also confirmed that it was a position-independent executable, meaning the exploit needed to work out where it was loaded in the running process rather than assume a fixed address.

The subagent created redis_7_4_2_stack.py, a new target-specific module for the adapted exploit. It supplied the locations and memory-layout information needed for this build, and constructed a jump-oriented programming (JOP) chain: a sequence of existing machine-code instructions arranged to redirect execution toward the desired operating-system command.

It also wrote run_exploit.py, a driver that tested the exploit in stages. This let it check memory disclosures, calculate the executable's runtime base address from a leaked native function pointer, and verify the prepared data before attempting command execution. A failure in one stage could be investigated separately from the rest.

At this point, Shinobi had adapted public exploit code to a build it did not originally support. It still needed to prove the complete exploit worked.

When the remote attempt fails, bring it into the debugger

The first full attempt against the remote test database caused the Redis connection to disappear. No command output reached Shinobi's evidence receiver. The result was inconclusive: a dropped connection could mean a crash, not successful command execution.

To see what was happening, the subagent set up a second environment on Shinobi's own testing machine. This local environment used the matching Redis executable, along with the RedisJSON and RediSearch modules found in the target's Redis Stack installation.

The purpose was specific: reproduce the exploit locally, where the agent could inspect the running process with the GNU debugger, GDB. The agent identified single-stepping the pivot as the fastest way to investigate which instruction sequence was failing. Here, the "pivot" was the point where the exploit tried to redirect execution away from normal Lua handling and into its chosen native instructions.

Shinobi started the local Redis instance under GDB and set a breakpoint around that transition. The following part of its debugger script repeatedly printed the next instruction, inspected the CPU registers and executed one instruction at a time:

set $n = 0
while $n < 14
  x/i $rip
  info registers rax rdi rsi rdx
  si
  set $n = $n + 1
end

The captured output reached a native call inside Lua's luaD_precall function:

=> 0x55ed3d025f3a <luaD_precall+170>: call *0x20(%rax)

This instruction calls a function through a pointer stored in memory. It was a point where the exploit's control over Lua objects could affect which native code ran next. GDB let Shinobi inspect that transition and the instructions that followed, instead of guessing from a closed network connection.

For the local proof, the agent had configured the command to write its output into /tmp/rce/pwned.txt on the testing machine. The first instrumented run did not produce the expected file. After inspecting the debugger output, Shinobi ran another local test without the debugger catchpoint. That run produced the file containing the command output.

The trace does not establish a definitive cause for the earlier unsuccessful remote attempt. It does establish a successful local reproduction. That local process ran as root in Shinobi's testing environment, not on the remote target. Shinobi could now return to a fresh remote test database and check whether it produced the same kind of evidence there.

Collect proof from the remote database

For the remote test, a local output file was not enough: Shinobi needed evidence returned from the customer's environment. It therefore used an out-of-band callback, an HTTP request from the target to a receiver controlled by Shinobi, carrying the output of the proof commands.

Those commands included id, to identify the operating-system account; hostname, to identify the environment; and ls /runtime/databases, to inspect the database storage directory. The payload also collected cgroup and operating-system information. The directory listing alone was not proof that every tenant's dataset had been read.

This time the callback arrived. Its decoded output included:

uid=999({SANITISED}) gid=999({SANITISED}) groups=999({SANITISED})

That line came from the remote system's id command. Shinobi had gone from running permitted Lua scripts inside a tenant's database to running an operating-system command as the database process's user, UID 999.

The proof was not a root compromise, and the test was disruptive. The exploit used execve, which replaced the Redis process with the proof command. The disposable database instance consequently stopped serving Redis. The result demonstrated command execution, with that availability impact.

The next question was what this non-root account could access beyond its own database.

Follow the access back to the control plane

The main agent now had the subagent's working exploit and evidence. It used them to inspect the environment around the compromised Redis process.

The PaaS control plane, the Next.js management application behind the self-service console, managed the platform's tenants and their Redis databases. It was running in the same container as those databases. More importantly, the control plane and the tenant databases all ran as the same operating-system user: {SANITISED}, UID 999.

That explained why getting a command to run as UID 999 mattered. Code execution in a tenant database was now running under the same operating-system account as the internal platform's management application. The boundary that failed was between a tenant's database workload and the control plane managing the wider platform.

Shinobi demonstrated that access by reading /proc/8/environ. PID 8 was the control-plane process; its environ file exposed the environment variables supplied to that process. The returned data included SESSION_SECRET, DATABASE_URL, provisioning configuration and seeded login credentials for other tenants.

The agent also inspected /app, the directory holding the control-plane application. Its source and runtime files were owned by the same {SANITISED} user. The finding therefore identified not just readable secrets, but an application tree owned by the compromised account.

Some container hardening was present: the process was non-root, had no effective Linux capabilities, and ran with seccomp filtering. But those controls did not separate processes already sharing the same account and container. No host escape was demonstrated or needed to reach the control-plane secrets. Secret disclosure was established; session forgery was not demonstrated.

This was a second vulnerability with a different fix. Updating Redis would remove the demonstrated way to obtain command execution. It would not separate tenant database processes from the PaaS control plane. Shinobi reported the missing isolation separately from the Redis CVE. An internal user being allowed to provision a database did not mean that database should be able to read the platform's secrets or reach other tenants' resources.

Reproduce the result with a new tenant

A separate validation agent within Shinobi then repeated the investigation's key result using a fresh account on the PaaS. In the test environment, it registered a new tenant, created two disposable databases through the normal workflow, obtained their passwords and reproduced command execution.

Fresh callbacks returned the control-plane environment and process-ownership evidence. The validator also checked that the executing shell's process ID matched the database process ID reported by the console API. That connected the evidence to the newly created tenant instance.

The validator recorded:

The claim was reproduced independently and completely.

This was a separate replay inside Shinobi, not an external audit. It showed that the result did not depend on the original agent's existing tenant account or local debugging environment.

Two findings, two different fixes

The final result was more useful than a ticket saying "Redis 7.4.2 is vulnerable."

The investigation showed how an account on the internal PaaS could use the intended database-provisioning workflow to obtain Redis credentials, exploit its own database process and then read the PaaS control plane's secrets. The issue was not that users could create databases; it was that a compromise inside one database could cross into the platform's management layer.

The remediation had to address both findings:

  • Patch the vulnerable Redis runtime. Remove the demonstrated route from permitted Lua scripting to operating-system command execution.
  • Isolate tenant workloads from the PaaS control plane. A database process should not share an operating-system identity and container with the application managing the wider platform.
  • Rotate exposed secrets. Patching and isolation do not invalidate credentials already disclosed.

In the tested deployment, a related finding also showed that the raw Redis ports were reachable outside the access restrictions applied to the HTTPS console. Those restrictions needed to cover the database access path as well.

The distinctive part was the work Shinobi put into reaching that proof. It did not discover RediShell; Wiz reported the vulnerability, and public research, including saneki's implementation, supplied an important foundation. Shinobi evaluated that material, adapted it to the deployed build, debugged the difficult execution step and connected the result back to the application's security boundaries.

And while the specialist worked through Redis, the main agent kept testing the web application. The team receiving the report got both the reproduction and the reason a Redis upgrade alone would not address everything the test had found.

The CVE needed a patch. The platform needed a redesign.


Table of Contents