Converting from Apache1-style to (Debian-style) Apache2-style vhosts

Yeah, some of us are still doing that migration.

Anyway, historically Apache vhosts are all in one file at /etc/apache/httpd.conf or if you're really lucky something like /etc/apache/vhosts.conf.

Apache2 in Debian uses two directories - /etc/apache2/sites-available and /etc/apache2/sites-enabled. sites-available contains one file for each vhost and in order to enable them they're linked to from sites-enabled. This is all fairly nice an elegant and human friendly, but tedious to migrate to from Apache1.

Since this one's coincided with a feeling that I should know more awk here's how I just did this one:

cp /etc/apache/vhosts.conf /etc/apache2/sites-available
awk '/^"vhost" n }' vhosts.conf
for i in $(ls vhost*); do name=$(grep -i ^ServerName $i | awk '{print $2}'); mv $i $name ; done
rm /etc/apache2/sites-available/vhosts.conf

Yeah, the name should be doable in the initial awk, but by that point I sort-of just needed to get it done.

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

  1. < ?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
file=$1
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.

Unattended Virtualmin installs

A while ago I was asked to concoct a fire-and forget script to install Virtualmin without prompting.

It's really easy:

#!/bin/bash
if [ -z $1 ]; then
        echo "Usage"; echo "  $0 [hostname]"; echo ""; exit
fi
wget http://software.virtualmin.com/gpl/scripts/install.sh -O install.sh
export VIRTUALMIN_NONINTERACTIVE="1"
chmod +x install.sh
./install.sh -f -host $1
rm install.sh

And then you call it like so:

./virtualmin.sh virtualmin.vm.avi.co

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:

  1.  
  2. $wgDebugLogGroups = array(
  3. 'SomeExtension' => '../logs/wiki_SomeExtension.log',
  4. );
  5.  

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

  1.  
  2. $wgDebugLogGroups = array(
  3. 'SimpleCaptcha' => '../logs/wiki_SimpleCaptcha.log',
  4. );
  5.  

But not so! What I actually wanted was

  1.  
  2. $wgDebugLogGroups = array(
  3. 'captcha' => '../logs/wiki_confirmedit.log',
  4. );
  5.  

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:

  1.  
  2. function log( $message ) {
  3. wfDebugLog( 'captcha', 'ConfirmEdit: ' . $message . '; ' . $this->trigger );
  4. }
  5.  

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:

  1. wiki:/home/wiki/public_html# diff includes/upload/UploadBase.php includes/upload/UploadBase.php.bak
  2. 447,455c447,454
  3. < // ## Avi Commented this out so that we can upload whatever we like to our server. That was nice of him
  4. < // // Check whether the file extension is on the unwanted list
  5. < // global $wgCheckFileExtensions, $wgFileExtensions;
  6. < // if ( $wgCheckFileExtensions ) {
  7. < // if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) {
  8. < // $warnings['filetype-unwanted-type'] = $this->mFinalExtension;
  9. < // }
  10. < // }
  11. < //
  12. ---
  13. > // Check whether the file extension is on the unwanted list
  14. > global $wgCheckFileExtensions, $wgFileExtensions;
  15. > if ( $wgCheckFileExtensions ) {
  16. > if ( !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) {
  17. > $warnings['filetype-unwanted-type'] = $this->mFinalExtension;
  18. > }
  19. > }
  20. >
  21. 557,570c556,569
  22. < // ## Avi Commented this out so that we can upload whatever we like to our server. That was nice of him
  23. < // /* Don't allow users to override the blacklist (check file extension) */
  24. < // global $wgCheckFileExtensions, $wgStrictFileExtensions;
  25. < // global $wgFileExtensions, $wgFileBlacklist;
  26. < // if ( $this->mFinalExtension == '' ) {
  27. < // $this->mTitleError = self::FILETYPE_MISSING;
  28. < // return $this->mTitle = null;
  29. < // } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
  30. < // ( $wgCheckFileExtensions && $wgStrictFileExtensions &&
  31. < // !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) {
  32. < // $this->mTitleError = self::FILETYPE_BADTYPE;
  33. < // return $this->mTitle = null;
  34. < // }
  35. < //
  36. ---
  37. >
  38. > /* Don't allow users to override the blacklist (check file extension) */
  39. > global $wgCheckFileExtensions, $wgStrictFileExtensions;
  40. > global $wgFileExtensions, $wgFileBlacklist;
  41. > if ( $this->mFinalExtension == '' ) {
  42. > $this->mTitleError = self::FILETYPE_MISSING;
  43. > return $this->mTitle = null;
  44. > } elseif ( $this->checkFileExtensionList( $ext, $wgFileBlacklist ) ||
  45. > ( $wgCheckFileExtensions && $wgStrictFileExtensions &&
  46. > !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) ) ) {
  47. > $this->mTitleError = self::FILETYPE_BADTYPE;
  48. > return $this->mTitle = null;
  49. > }
  50. >

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:

  1. wiki:/home/wiki/public_html# diff includes/Setup.php includes/Setup.php.bak
  2. 296,298c296,297
  3. < // ## Avi Commented this out so we can upload whatever we like to our server. That was nice of him
  4. < //# Blacklisted file extensions shouldn't appear on the "allowed" list
  5. < //$wgFileExtensions = array_diff ( $wgFileExtensions, $wgFileBlacklist );
  6. ---
  7. > # Blacklisted file extensions shouldn't appear on the "allowed" list
  8. > $wgFileExtensions = array_diff ( $wgFileExtensions, $wgFileBlacklist );
  9.  

Tagging images by path in Shotwell

I've finally decided to use an image manager, and since it comes with Ubuntu this week I've gone with Shotwell. I've got a directory hierarchy containing most of my images which is sort-of sorted already, and I'm probably going to keep adding to it, if for no other reason than force of habit.

I know that one of the wonderful features of these photo managers is that you can tag photos, and obviously a photo can be in more than one tag rather more easily than it can be in several directories. That said, all photo managers seem to have decided that an easy, fast way to tag photos isn't what's needed.

Additionally, shotwell's got this weird thing for hiding the fact that there's a filesystem from you, and I can't find any way to tag files by directory. So I've poked around the database and written a script to do it, which is below and here and pasted below in case I change my mind about file hierarchies later.

The oddest bit is the way the filenames are linked to the tags. The TagTable table has a field `photo_id_list` which contains a list of photo IDs in a format that I've not found anywhere else in the (admittedly not very extensive) db.

They're created by taking the id of the image (its value in the `id` field of the PhotoTable table), converting it to a hex value, padding it out to 16 characters with leading zeroes, and then concatenating it onto the string 'thumb':

  1. my $hexPhotoId = sprintf("%x", $photoId);
  2. my $thumbString = "thumb".sprintf('%016s', $hexPhotoId);

Anyway, the script's a bit simple because bash is quite good at handling loads of files; usage is like this to tag the contents of ~/Pictures/2011-france/ with the tag 'morzine':

avi@brilliant:~$ find ~/Pictures/2011-france/ -type f -exec ./shotwell-tag {} morzine \;
Creating tag morzine
tagged /home/avi/Pictures/2011-france/R0012810.JPG with morzine
tagged /home/avi/Pictures/2011-france/R0012850.JPG with morzine
tagged /home/avi/Pictures/2011-france/R0012911.JPG with morzine
tagged /home/avi/Pictures/2011-france/R0012931.JPG with morzine
tagged /home/avi/Pictures/2011-france/R0012921.JPG with morzine
tagged /home/avi/Pictures/2011-france/R0012794.JPG with morzine
tagged /home/avi/Pictures/2011-france/R0012883.JPG with morzine
tagged /home/avi/Pictures/2011-france/R0012881.JPG with morzine

I've no idea if it breaks anything - I wrote it about an hour ago, have tagged ~500 photos with it since, and Shotwell doesn't seem to be annoyed. YMMV. Here's the script:

  1.  
  2. #! /usr/bin/perl
  3.  
  4. # shotwell-tag
  5. #
  6. # Tags files specified by filename in shotwell. Handy for
  7. # getting round shotwell's attempts at hiding the filesystem.
  8. #
  9. # Avi 2011
  10.  
  11. use strict;
  12. use DBI;
  13.  
  14. my $file = shift;
  15. my $tag = shift;
  16.  
  17. if ($tag !~ /.+/){
  18. print "Usage:\n\n\tshotwell-tag [file] [tag]\n\n";
  19. print "Tags [file] with [tag] in shotwell's db\n";
  20. exit 1;
  21. }
  22.  
  23. my $dbfile = $ENV{'HOME'}."/.shotwell/data/photo.db";
  24. my $dbh = DBI->connect("dbi:SQLite:dbname=$dbfile","","");
  25.  
  26. # Each tag has a string of photo 'ids'. These are generated
  27. # by taking the ID of the photo from PhotoTable, representing
  28. # it in hex, padding that out to 16 characters with leading
  29. # zeroes and then appending it to the string 'thumb'
  30. my $sth = $dbh->prepare("select id from PhotoTable where filename='$file'");
  31. $sth->execute();
  32. my $row = $sth->fetch;
  33. my $photoId = $row->[0];
  34. unless($photoId =~ /\d+/){print "$file is not in shotwell library\n"; exit 0;}
  35. my $hexPhotoId = sprintf("%x", $photoId);
  36. my $thumbString = "thumb".sprintf('%016s', $hexPhotoId);
  37.  
  38. $sth = $dbh->prepare("select id from TagTable where name='$tag'");
  39. $sth->execute();
  40. $row = $sth->fetch;
  41. my $tagId = $row->[0];
  42. unless($tagId =~ /\d+/){
  43. print "Creating tag $tag\n";
  44. my $sth = $dbh->prepare("insert into TagTable (name) values('$tag')");
  45. $sth->execute;
  46. }
  47.  
  48. $sth = $dbh->prepare("Select photo_id_list from TagTable where name='$tag'");
  49. $sth->execute();
  50. $row = $sth->fetch;
  51. my $photoList = $row->[0];
  52. if($photoList !~ /,$/ && $photoList =~ /._/){
  53. $photoList.=',';
  54. }
  55. if($photoList =~ /$thumbString/){
  56. print "$file is already tagged with $tag\n";
  57. exit 0;
  58. }else{
  59. $photoList.=$thumbString.',';
  60. $sth = $dbh->prepare("update TagTable set photo_id_list = '$photoList' where name='$tag'");
  61. $sth->execute;
  62. print "tagged $file with $tag\n";
  63. exit 0;
  64. }
  65.  

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 config.inc.php file, which I hope are well enough explained by the comments:

  1.  
  2. // cleartext
  3. // Do you want to store cleartext passwords for email accounts?
  4. // true = store cleartext passwords (need to have a password_clear column in the mailbox table)
  5. // false = don't store cleartext passwords
  6. $CONF['cleartext'] = false;
  7. // and the same for admins:
  8. $CONF['cleartext_admin'] = false;
  9.  

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:

  1.  
  2. ALTER TABLE mailbox ADD `password_clear` varchar(255);
  3. ALTER TABLE admin ADD `password_clear` varchar(255);
  4.  

Next, apply my patch:

avi@amazing:/var/www/postfixadmin$ wget -q http://avi.co/stuff/postfixadmin_plaintext-passwords.txt
avi@amazing:/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.

Fail2Ban and date formats

Fail2Ban is utterly daft in at least one respect. Here's me testing a regex on a date format it doesn't recognise:

# fail2ban-regex '2010-12-14 15:12:31 - 80.87.131.48' ' - <HOST>$'
Found a match but no valid date/time found for 2010-12-14 15:12:31 - 80.87.131.48. Please contact the author in order to get support for this format
Sorry, no match

And on one that it does:

fail2ban-regex '2010/12/14 15:12:31 - 80.87.131.48' ' - <HOST>$'

Success, the following data were found:
Date: Tue Dec 14 15:12:31 2010
IP  : 80.87.131.48

Date template hits:
0 hit: Month Day Hour:Minute:Second
0 hit: Weekday Month Day Hour:Minute:Second Year
1 hit: Year/Month/Day Hour:Minute:Second
0 hit: Day/Month/Year:Hour:Minute:Second
0 hit: TAI64N
0 hit: Epoch

Benchmark. Executing 1000...
Performance
Avg: 0.10257935523986816 ms
Max: 0.125885009765625 ms (Run 8)
Min: 0.10085105895996094 ms (Run 780)

Ignoring for the moment the fact that it doesn't recognise 2010-12-14 15:12:31 (Seriously?)1 , the only way to get that list of date formats is by happening to pick a correct one. As soon as you no longer need a list of date formats you may use, it presents you with one.

What?

So, as an attempted fix for this situation, see above for a list of compatible date formats.

  1. It's worth noting, too, that the author is of the opinion that specifying your own date format is too much like hard work, so if you want support for any date format other than those already supported, you've to patch it yourself. Which is obviously way easier than just having a date regex in the config file []