Finding exploited wordpress pages

WordPress seems to be hilariously easy to compromise (this might be a bad place to write that) and the general form of an exploit is to inject code like this

< ?php $a = base64_decode(YSBsb25nIHN0cmluZyBvZiBiYXNlNjQgdGV4dAo=.......);

right at the top of a script. base64_decode is rarely used by the Good Guys outside of mailers and doing tricks with images, but it's almost never found right at the top of a script. I did write a really convoluted script that found calls to base64_decode and exec and guessed whether they were nefarious (generally, for example, base64_decode is called with a variable (base4_decode($mailBody)), not just a string (base64_decode(dGV4dAo=)) but that just ate all my I/O and didn't really work.

So I came up with a much cruder way of doing it. Have a script called ~/bin/base64_in_head

#! /bin/bash
head $file | grep base64 2>&1 >/dev/null || exit 1;
echo $file
exit 0;

And then run it like this:

$ ionice -c3 find /home/user/public_html/ -name \*.php -exec ~/bin/base64_in_head {} \;

I’ve not yet had a situation where that’s missed a file that later manual greps have found.

Per-extension logging in MediaWiki

This is another of those things that took me rather longer to work out than I would have liked, so hopefully this’ll appear in the sorts of searches I should have done.

MediaWiki has this nifty feature where you can split the logging for particular extensions out into individual files by doing things like this:

$wgDebugLogGroups = array(
        'SomeExtension'     => '../logs/wiki_SomeExtension.log',

What’s not made overly clear (well, with hindsight, it is implied by the manual) is that the keys of the hash don’t necessarily have anything to do with the name of the extension. I assumed that, in debugging SimpleCaptcha, what I wanted was

$wgDebugLogGroups = array(
        'SimpleCaptcha'     => '../logs/wiki_SimpleCaptcha.log',

But not so! What I actually wanted was

$wgDebugLogGroups = array(
        'captcha'     => '../logs/wiki_confirmedit.log',

And, as far as I can find, this isn’t documented *anywhere*. For other extensions lacking in documentation so, you can find this out by poking around in the code, and looking for where the extension does this sort of thing:

function log( $message ) { 
      wfDebugLog( 'captcha', 'ConfirmEdit: ' . $message . '; ' .  $this->trigger );

That first argument to wfDebugLog is what you want as the key in the hash. Why it can’t just use the name of the class invoking it, which is the name used to configure the rest of the extension, I’ve no idea.

Allowing uploads of arbitrary files in MediaWiki

I did RTFM and I did what it said, and still my Mediawiki complained when I tried to upload executable files and things with funny file extensions or mime types. if $wgFileExtensions is empty but $wgEnableUploads = true and $wgStrictFileExtensions = false it should just let me upload anything. I can’t think what other behaviour one would expect there, but set like that I can’t upload my dodgy files.

So I’ve removed the code it uses to check.

Here’s a pair of diffs if you’d also like to do this. These are on version 1.17.0 but I suspect it’s not changed very much.

This just comments out the two blocks of code in UploadBase.php which check whether files are considered safe and warn if they’re not – it prevents the checking and the warning:

wiki:/home/wiki/public_html# diff includes/upload/UploadBase.php includes/upload/UploadBase.php.bak
< // ## Avi Commented this out so that we can upload whatever we like to our server. That was nice of him
< //            // Check whether the file extension is on the unwanted list
< //            global $wgCheckFileExtensions, $wgFileExtensions;
< //            if ( $wgCheckFileExtensions ) {
< //                    if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) {
< //                            $warnings['filetype-unwanted-type'] = $this->mFinalExtension;
< //                    }
< //            }
< //
>               // Check whether the file extension is on the unwanted list
>               global $wgCheckFileExtensions, $wgFileExtensions;
>               if ( $wgCheckFileExtensions ) {
>                       if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) {
>                               $warnings['filetype-unwanted-type'] = $this->mFinalExtension;
>                       }
>               }
< // ## Avi Commented this out so that we can upload whatever we like to our server. That was nice of him
< //            /* Don't allow users to override the blacklist (check file extension) */
< //            global $wgCheckFileExtensions, $wgStrictFileExtensions;
< //            global $wgFileExtensions, $wgFileBlacklist;
< //            if ( $this->mFinalExtension == '' ) {
< //                    $this->mTitleError = self::FILETYPE_MISSING;
< //                    return $this->mTitle = null;
< //            } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
< //                            ( $wgCheckFileExtensions && $wgStrictFileExtensions &&
< //                                    !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) {
< //                    $this->mTitleError = self::FILETYPE_BADTYPE;
< //                    return $this->mTitle = null;
< //            }
< //
>               /* Don't allow users to override the blacklist (check file extension) */
>               global $wgCheckFileExtensions, $wgStrictFileExtensions;
>               global $wgFileExtensions, $wgFileBlacklist;
>               if ( $this->mFinalExtension == '' ) {
>                       $this->mTitleError = self::FILETYPE_MISSING;
>                       return $this->mTitle = null;
>               } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
>                               ( $wgCheckFileExtensions && $wgStrictFileExtensions &&
>                                       !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) {
>                       $this->mTitleError = self::FILETYPE_BADTYPE;
>                       return $this->mTitle = null;
>               }

And this just stops Setup.php making-safe the $wgFileExtensions array by removing whatever’s in $wgFileBlacklist from it, which I think wouldn’t complain had I not already done Bad Things to those two variables, but it’s late and it can’t hurt to turn this off, too:

wiki:/home/wiki/public_html# diff includes/Setup.php includes/Setup.php.bak
< // ## Avi Commented this out so we can upload whatever we like to our server. That was nice of him
< //# Blacklisted file extensions shouldn't appear on the "allowed" list
< //$wgFileExtensions = array_diff ( $wgFileExtensions, $wgFileBlacklist );
> # Blacklisted file extensions shouldn't appear on the "allowed" list
> $wgFileExtensions = array_diff ( $wgFileExtensions, $wgFileBlacklist );

Postfixadmin with clear-text passwords

One of my projects at the minute is converting vpopmail mail servers to postfixadmin. One _really_ handy thing about some of these vpopmail machines is that they store a cleartext copy of all the users’ passwords, so I can feed them straight into the new system.

So, I’ve now got a postfixadmin system that stores cleartext passwords, and in case you want to do it, too, I’ve put a patch up. It gives you an extra couple of options in the file, which I hope are well enough explained by the comments:

// cleartext
// Do you want to store cleartext passwords for email accounts?
// true = store cleartext passwords (need to have a password_clear column in the mailbox table)
// false = don't store cleartext passwords
$CONF['cleartext'] = false;
// and the same for admins:
$CONF['cleartext_admin'] = false;

If you do want this (and are aware of the problems with storing cleartext passwords) it’s quite easy to do. First, add a couple of columns to the MySQL db:

alter table mailbox add `password_clear` varchar(255);
alter table admin add `password_clear` varchar(255);

Next, apply my patch:

[email protected]:/var/www/postfixadmin$ wget -q
[email protected]:/var/www/postfixadmin$ patch < postfixadmin_plaintext-passwords.txt

Lastly, configure it; my patch sets both the config variables to 'false' because I like safeguards like that :)

It's worth noting that if you're using cleartext passwords, and then turn it off, the cleartext columns wont be affected - you'll need to update them with nulls or something if you want to get rid of the data in them.

Massive dumps with MySQL

hurr. *insert FLUSH TABLES joke here*

I have a 2.5GB sql dump to import to my MySQL server. MySQL doesn’t like me giving it work to do, and the box it’s running on only has 3GB of memory. So, I stumbled across bigdump, which is brilliant. It’s a PHP script that splits massive SQL dumps into smaller statements, and runs them one at a time against the server. Always the way: 10 lines into duct-taping together something to do the job for you, you find that someone else has done it rather elegantly.1

In short, we extract the directory to a publicly http-accessible location, stick the sql dump there and tell it to go.

In long, installation is approximately as follows:

[email protected]:~$ cd www
[email protected]:~/www$ mkdir bigdump
[email protected]:~/www$ chmod 777 bigdump
[email protected]:~/www$ cd bigdump/
[email protected]:~/www$ wget -q
[email protected]:~/www$ unzip
[email protected]:~/www/bigdump$ ls

Where ~/www is my apache UserDir (i.e. when I visit http://localhost/~avi, i see the contents of ~/www). We need permissions to execute PHP scripts in this dir, too (which I have already). We also need to give everyone permissions to do everything – don’t do this on the internet!2

Configuration involves editing bigdump.php with the hostname of our MySQL server, the name of the DB we want to manipulate and our credentials. The following is lines 40-45 of mine:

// Database configuration
$db_server   = 'localhost';
$db_name     = 'KBDB';
$db_username = 'kbox';
$db_password = 'imnottellingyou';

Finally, we need to give it a dump to process. For dumps of less than 2Mb3, we can upload through the web browser, else we need to upload or link our sql dump to the same directory as bigdump:

[email protected]:~/www/bigdump$ ln -s /home/avi/kbox/kbox_dbdata ./dump.sql

Now, we visit the php page through a web browser, and get a pretty interface:

BigDump lists all the files in its working directory, and for any that are SQL dumps provides a ‘Start Import’ link. To import one of them, click the link and wait.

  1. Yes, you Perl people, it’s in PHP. But it’s not written by me. So on balance turns out more elegant. []
  2. Those permissions aside – anyone can execute whatever SQL they like with your credentials through this page. Seriously, not on the internet! []
  3. Or whatever’s smaller out of upload_max_filesize and post_max_size in your php.ini []

PHP error on fresh install of PHPWiki:
Non-static method _PearDbPassUser::_PearDbPassUser() cannot be called statically

I’ve just installed PHPWiki 1.3.14-4 from the debian repositories and out of the box I got the following message on trying to log in to it:

Fatal error: Non-static method _PearDbPassUser::_PearDbPassUser() cannot be called statically, assuming $this from incompatible context in /usr/share/phpwiki.bak.d/lib/WikiUserNew.php on line 1118

The problem appears to be that, as of PHP 5.something, you’re not allowed to have a function with the same name as a class. Apparently it’s been a failure in E_STRICT mode for a while.

Anyway, the solution is to rename _PearDbPassUser() to something else, and then replace all calls to it with this new name.

I’ve done this and, so far, everything appears to work.

The function is defined in /usr/share/lib/phpwiki/lib/WikiUser/PearDb.php:

jup-linux2:/usr/share$ diff phpwiki.bak.d/lib/WikiUser/PearDb.php phpwiki/lib/WikiUser/PearDb.php
< function _PearDbPassUser($UserName='',$prefs=false) {
>     function _PearDbPassUserFoo($UserName='',$prefs=false) {

and is called in /usr/share/WikiUserNew.php:

jup-linux2:/usr/share$ diff phpwiki.bak.d/lib/WikiUserNew.php phpwiki/lib/WikiUserNew.php 
< _PearDbPassUser::_PearDbPassUser($this->_userid, $this->_prefs);
>                 _PearDbPassUser::_PearDbPassUserFoo($this->_userid, $this->_prefs);
< _PearDbPassUser::_PearDbPassUser($this->_userid, $prefs);
>                 _PearDbPassUser::_PearDbPassUserFoo($this->_userid, $prefs);
< function PearDbUserPreferences ($saved_prefs = false) {
>     function PearDbUserPreferencesFoo ($saved_prefs = false) {