Skip to content
Minidlna Unbounded Vla To Rce

Minidlna Unbounded Vla To Rce

www.kr3bz.wtf • November 3, 2026

I always wanted to find a memory corruption vulnerability when I started doing vulnerability research, but somehow grep password was sufficient to achieve RCE. This time I decided to poke at my own router, the ASUS RT-AX86U . The router is running on ARM, which I haven’t played with so far, so this looked like a good learning experience.

What I’ve found are multiple vulnerabilities in ReadyMedia / MiniDLNA which can be exploited to crash the service, leak sensitive data, or achieve remote command execution. The one I dug into and exploited on my router hits the DFF file parser.

ReadyMedia (formerly known as MiniDLNA) is a simple media server. It is a lightweight, free piece of software that turns your device into a media server, streaming your movies, music and photos to a Smart TV, console or phone.

It is available in repositories for various Linux distributions (check out the Cross-Platform Confirmation section at the end), confirmed present on ASUS, OpenWrt, and DD-WRT firmware; historically bundled as an add-on package on some NAS devices (Synology, QNAP, ReadyNAS).

It is being replaced by better alternatives, but it is still widely present. A quick Shodan shows its external exposure, but it is far more often exposed only internally.

A plain minidlna Shodan returns 48,476 results but that number can’t be read as 48,476 vulnerable instances. A quick look at the top hits shows most are running versions well below 1.3.0 ( 1.1.4 , 1.0.24 , even 1.0 ), which predate the DFF parser entirely and are therefore unaffected by this specific bug. Since the DFF parser only exists in versions 1.3.0 - 1.3.3, the exposure is a narrower subset of that total and drops down to approximately 7,400 instances. Also, as you will see, there are prerequisites for exploiting this vulnerability and I only exploited it on the ASUS router.

As you can see in the config, it uses inotify . Rather than wasting CPU cycles continuously scanning the disk for modifications, MiniDLNA hooks into inotify , a native Linux kernel subsystem that watches the filesystem in real time. The moment you drop a new movie or song into a shared folder, inotify fires an event, waking MiniDLNA to parse only that one new file and inject its metadata into the database.

To visualize how your device discovers the ReadyMedia / MiniDLNA service on the network, here is a Wireshark network traffic capture:

Your device uses the Simple Service Discovery Protocol ( SSDP ) protocol to discover MiniDLNA on the network via multicast (in this example, I used VLC Media Player to browse the local network). It sends an M- discovery packet to the SSDP multicast address ( 239.255.255.250:1900 for IPv4, [FF0x::C]:1900 for IPv6) (bottom-left). The MiniDLNA server replies with a 200 OK as a unicast to the multicast query, containing a LOCATION header that points to its device description XML: in this example, (bottom-left). Your device then makes an actual HTTP GET request to that LOCATION URL and receives the device’s metadata (bottom right). Note that the server banner in the response ( Server: 4.1.52 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.3.3 ) displays the exact MiniDLNA version.

It sends an M- discovery packet to the SSDP multicast address ( 239.255.255.250:1900 for IPv4, [FF0x::C]:1900 for IPv6) (bottom-left). The MiniDLNA server replies with a 200 OK as a unicast to the multicast query, containing a LOCATION header that points to its device description XML: in this example, (bottom-left). Your device then makes an actual HTTP GET request to that LOCATION URL and receives the device’s metadata (bottom right). Note that the server banner in the response ( Server: 4.1.52 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.3.3 ) displays the exact MiniDLNA version.

The MiniDLNA server replies with a 200 OK as a unicast to the multicast query, containing a LOCATION header that points to its device description XML: in this example, (bottom-left). Your device then makes an actual HTTP GET request to that LOCATION URL and receives the device’s metadata (bottom right). Note that the server banner in the response ( Server: 4.1.52 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.3.3 ) displays the exact MiniDLNA version.

Your device then makes an actual HTTP GET request to that LOCATION URL and receives the device’s metadata (bottom right). Note that the server banner in the response ( Server: 4.1.52 DLNADOC/1.50 UPnP/1.0 MiniDLNA/1.3.3 ) displays the exact MiniDLNA version.

I already had an SMB set up and a USB HDD mounted on the router, so I decided to take a look and copied the minidlna binary (32-bit) from the router (64-bit kernel) to my machine. Note: I chose SMB as an attack vector, but if you want to attack the router via WAN, AiCloud is your friend.

I used Semgrep to scan the ReadyMedia / MiniDLNA source which is available here . I will omit all other vulnerabilities that it found, and will leave only relevant information for this blog post.

Let’s look at the tagutils/tagutils-dff.c at line 130 to see what is going on:

propckDataSize appears only in tagutils/tagutils-dff.c :

For clarity, I think it is best to drop the source of tagutils-dff.c here, up to the vulnerable part:

The code parses the DFF file via the _get_dfffileinfo function by first opening the file, fread ing the first 32 bytes, and verifying that it contains the appropriate magic chunks, specifically FRM8 , DSD , and FVER at offsets 0, 12, and 16.

fread now points to the ckbuf array, which should start with the PROP chunk, as checked on line 125. If it exists, at offset ckbuf + 4 (right after the PROP magic), the code uses the GET_DFF_INT64 macro to obtain 8 bytes. This macro is used to combine eight separate bytes from a file into a single 64-bit integer.

At line 130 - it uses the propckDataSize , which is entirely under our control, to create a propckData Variable Length Array (VLA) with no bounds checks. Let’s check this out.

The DFF file structure looks like this:

More the DFF file structure can be found on Sonic Studio .

First I created a valid DFF file:

Then I changed the PROP chunk size to good old 0x41414141 (1094795585 in decimal, ~1GB):

Copied it over to my SMB file , and looked at the dmesg output on my router:

W00t! The x1 register holds 0000000041414141 - attacker-controlled PROP chunk size. What is potentially unexpected fatal signal 11. ?

So, the prerequisites to crash the service are:

SMB service needs to be network-reachable (LAN attack vector) / AiCloud enabled (WAN attack vector) user that has permissions to write via an SMB / AiCloud a DFF file with an oversized PROP Chunk Size

user that has permissions to write via an SMB / AiCloud a DFF file with an oversized PROP Chunk Size

a DFF file with an oversized PROP Chunk Size

Until that day, I never really looked at ARM instructions nor did I have any experience with ARM exploitation. But registers are registers, memory is memory, instructions are instructions.

First, let’s check the binary protections. For this I used ExeCheck from my colleague Kost.

So: partial RELRO, NX enabled, no PIE, no stack canaries.

Well, that is not nice. That means full ASLR - stack, heap, and mmap (libraries) are all randomized, and I do not have any information disclosure vulnerabilities that would leak some memory addresses. The one thing it doesn’t touch is the base of a non-PIE binary like this one: its code, PLT and any gadgets stay at fixed addresses.

I copied over gdb-static and modified the poc.dff file a bit by adding a bunch of 0x41 s in the file to see how that would affect the registers, and set the PROP Chunk size to 0xFFFFFFFF . So, why 0xFFFFFFFF ? While trying to exploit the vulnerability it just seemed to work reliably. But it did bug me why this worked, specifically? I didn’t actually get it until I sat down to write this blog post.

Every time the program needs space for something, it keeps a running count of how much room is left, like an odometer with a fixed number of digits. Ask for space, and the counter ticks down. Ask for more space than the counter can actually represent, though, and instead of going negative, it wraps back around near the top, the same way an odometer rolls from all-9s back to zero. Our “odometer” is the stack pointer sp . sp is a 32-bit register, so it can only hold values from 0 up to 0xFFFFFFFF (4,294,967,295 in decimal). That’s the entire range, every bit set to 1. 0xFFFFFFFF isn’t just “a big number close to the wraparound point”, it is the wraparound point.

That’s the difference between my two test values. 0x41414141 is a big number, but not big enough to hit that wraparound point, so the program just tries to grab 1 GB of space and crashes the boring, ordinary way and nothing interesting happens. 0xFFFFFFFF is the exact number that makes this particular counter wrap all the way back. That’s the number that turns an ordinary out-of-memory crash into the actual bug: the program thinks it reserved almost no space on the stack, while still believing it’s allowed to write nearly 4 GB of data.

The other thing is that fread() call (at line 132) sounds like it only “reads,” but reading and writing are two sides of the same call. fread(dest, size, count, file) reads bytes from the file, and writes them into dest buffer in memory.

After copying the file to the network :

Hm, pc still points into strlen() , but my input is already landing in r0 and r1 . YOLO mode on: I started adding more padding to the file, which ultimately resulted in control over the pc register:

This is the PoC file which gave control over the $pc and other registers:

This is the offset from the PROP Size Chunk:

1968 / 2 (to convert to bytes) - 4 (to take the 0x42424242 off the count) = 980. So, 980 bytes land before the pc overwrite. That’s the offset I need from the start of the PROP chunk data. More padding? Let’s add a bunch of 0x43 ’s after the $pc overwrite.

Nice. The stack pointer points to padded data added right after the $pc overwrite.

OK, let’s analyze the memory layout.

So, MiniDLNA runs in two threads. The second thread stack frame is at 0xf6b05d38 which corresponds to:

Since the stack grows downward and propckDataSize is attacker-controlled, the VLA subtracts a value of my choosing from sp , walking it far down through the thread’s stack mapping (capped at ~4 GiB, since size_t is 32-bit). To summarize: I have control over the $pc , $r0 and $r4-$r11 registers.

To confirm where it actually breaks, I rebuilt MiniDLNA with debug symbols and ran it under pwndbg on a Raspberry Pi:

Reading it bottom to top: inotify_thread picks up the file I dropped into the watched directory, walks down through insert_file -> GetAudioMetadata -> readtags and into _get_dfffileinfo , which calls fread at tagutils-dff.c:132 - the read into propckData .

Two things in frame #0 are worth pointing out. size=1094795585 is just 0x41414141 in decimal: my PROP chunk size flowing straight into the allocation. And buf=0x2d18f3f0 is propckData itself - a low, out-of-place address, because the oversized VLA has already moved sp far down before a single byte is written. So the crash lands inside fread as it tries to write into that displaced buffer.

I have to deal with ASLR and NX, but on my side is the big one: no PIE. Add no stack canaries on top, and the binary’s own code stays at fixed, predictable addresses.

If you remember the output from Semgrep, we have a friend in the MiniDLNA binary.

Let’s try it out and change the 42424242 in the PoC DFF file with static system() address 000138c4 :

Bingo. However, I ran into an issue - the $r0 register is used as argument to the system() function ( system(const char *command); ), and the $r0 register contains the PROP Chunk Size. Also, NX is enabled so I need to find ROP gadgets. The gadget I need needs to put something I have control over (stack, $r4-$r11 ) into the $r0 register.

I used ROPgadget for this, but I did not find any gadgets that would help me. And then, once again, I decided to use grep and look around the system() call.

Look at this bad boy:

This little snippet loads $r0 with a pointer 8 bytes ahead of the stack pointer and then falls straight into system@plt . Let’s modify the PoC DFF file.

Nice. Now $r0 holds the memory address of $sp+8 , and I have control over the contents. Time to get a shell.

As the prerequisite is to have write access via an SMB / AiCloud - I can already transfer all the binaries I need, as ASUS router does not have any I can use out of the box to get a reverse shell (at least from my experiments). I have command execution as root (as I can put any command on the stack which system() will execute), but a shell is a shell.

It will find the copied netcat binary in /tmp/mnt , run it with the arguments I baked into the DFF file, and then restart minidlna, as it SIGSEGVs right after the payload fires, and I didn’t bother making it stable post-exploitation.

Since ReadyMedia / MiniDLNA ships identically across a dozen platforms, I spun up the same MiniDLNA 1.3.3 codebase in Docker across six distributions to confirm the DFF parser crash.

Same PoC file everywhere:

A minimal FRM8 / DSD / FVER header followed by a PROP chunk with ckDataSize = 0x0000000200000000 (~8 GB).

Ubuntu 24.04 (minidlna 1.3.3, apt)

I also rebuilt this one from source with debug symbols and caught it live in gdb:

Same call chain as the ARM backtrace above: inotify_thread -> monitor_insert_file -> insert_file -> GetAudioMetadata -> readtags -> _get_dfffileinfo .

Debian 12 (minidlna 1.3.3, apt)

Fedora 44 (minidlna 1.3.3, dnf)

Arch Linux (minidlna 1.3.3-7, pacman)

Alpine Linux (minidlna 1.3.3-r3, apk)

Raspbian 11 (compiled from the source with debug symbols)

I tried exploiting the vulnerability on Ubuntu, but I could not corrupt anything. I only had a reliable crash. If you pull MiniDLNA 1.3.3 from Ubuntu’s apt repo, you get a binary hardened with PIE, full RELRO, stack canaries, FORTIFY_SOURCE, and -fstack-clash-protection .

On other embedded/router-class builds without these mitigations, this memory corruption can lead to code execution. On anything built with a modern, default-hardened toolchain, it degrades to a denial-of-service condition. But that is just my experience.

VulnCheck ASUS Security Advisory ASUS Firmware Update 3.0.0.4.388_24437 ASUS Hall of Fame NIST

ASUS Security Advisory ASUS Firmware Update 3.0.0.4.388_24437 ASUS Hall of Fame NIST

ASUS Firmware Update 3.0.0.4.388_24437 ASUS Hall of Fame NIST

ASUS Hall of Fame NIST

I reported the vulnerability through VulnCheck in March, who handled the coordination. ASUS was responsive, and over the following months they sent me several firmware builds to test, and I kept throwing my PoCs at each one until the vulnerability was properly fixed by adding a bounds check for the Property chunk size.

What started as me poking at my own router ended up as my first memory corruption bug and I’ve learned a lot in the process.

0x41414141 the planet!

Extracted Entities