I wanted to address why the update to the AskApache Password Protection plugin didn't happen pre-2009 as I had hoped.. Mostly due to my job but I thought I could at least fill you in. Oh and this is going to get very boring very fast, unless you're ready to rumble in the zone.
The problem is happening because when you login to your FTP server with your username and password, the files that you upload are then owned by that username and password, which is almost always an actual user account on the server system. But the Apache Server is an executable file itself, and it is not owned by your FTP username, for security reasons. Apache controls the PHP Interpreter, which parses and executes the WordPress and plugin files as a separate user. ( SuEXEC, Apache Security Tips )
So what happens is theaskapache-password-protect.php
file saved on your server and is owned by the user that created it (if you downloaded it to your computer then used ftp to transfer, your ftp user owns it.. if you used a php downloader script, then the php process owner owns it) So when you click on the Run Tests button from the WordPress administration website what you are doing is sending a request via HTTP to your Apache Server process, which sees the requested file is .php so it then runs the php interpreter to execute the askapache-password-protect.php file, then that file uses programming to attempt and write/modify a file in your blog's root directory.
wp-admin/includes/file.php
Ok so this function get_filesystem_method
is a brilliant bit of code that would've been beyond my current PHP skills to come up with. It determines which if any of the following methods can be used to modify files on your server from within WordPress, which is exactly what the new version of the passpro plugin needs to use. The first test simply creates a file from within php using wp_tempnam, a function that attempts to locate and write to a temporary location on your server that has the best chance of having write access. If it is successfully created (this code assumes that it will be, something they need to fix) then the fileowner (uses stat internally) of the temp file just created is compared to the owner of the php script... Normally this works and then the plugin woks too, but on some hosts the script is running as a separate user than that of the file which means you can't directly access the local file system. That is what is occurring for most of you who experience permission problems while testing the plugin. There are thousands of caveats for each little part depending on your php version, php setup, server setup, server version, which Server API you are using, the type of SAPI being used, and on and on..
function get_filesystem_method($args = array()) { $method = false; if( function_exists('getmyuid') && function_exists('fileowner') ){ $temp_file = wp_tempnam(); if ( getmyuid() == fileowner($temp_file) ) $method = 'direct'; unlink($temp_file); } if ( ! $method && isset($args['connection_type']) && 'ssh' == $args['connection_type'] && extension_loaded('ssh2') ) $method = 'ssh2'; if ( ! $method && extension_loaded('ftp') ) $method = 'ftpext'; if ( ! $method && ( extension_loaded('sockets') || function_exists('fsockopen') ) ) $method = 'ftpsockets'; //Sockets: Socket extension; PHP Mode: FSockopen / fwrite / fread return apply_filters('filesystem_method', $method); }
This was part of some tests I did to see what kind of access I had with the very helpful posix functions which are very accurate as well since they were designed for a system with file permissions, ie. not Win. Don't ask me how because I won't tell you, but on one of the hosts I was testing on that did not allow direct access I was able to get the Apache server running as dhapache to erroneously write a file into my users blog directory. This is a big security no-no and I now have my .htaccess file written into the blog directory where it should go, but instead of my php script's user having write access to the file so I can modify it, its owned by dhapache! Because the file is owned by dhapache I shouldn't even be allowed to know it exists, but there it is. So the next step was to try and take ownership of the .htaccess file so that I could modify it. I tried and tried but was unsuccessful, I couldn't modify it so that was another dead end. Actually it took me awhile to figure out how to remove the file from my directory. Being that it was owned by dhapache I couldn't delete or modify it using my php process or even through ftp/ssh! Sysadmins regularly run find commands that search the servers for any files owned by dhapache that should not be there as this is a big red flag that someone has found a way to manipulate dhapache which could potentially lead to modifying dhapche-owned server config files.. Luckily I was able to delete it by basically running the hack again to overwrite the file.
if ((posix_setgid(getmygid())) !== false) $this->to_log('', 1, "Success Changing SETGID of {$file} to " . getmygid(), 3); elseif ((posix_setgid(filegroup(__file__))) !== false) $this->to_log('', 1, "Success Changing SETUID of {$file} to " . filegroup(__file__), 3); if ((posix_setegid(getmygid())) !== false) $this->to_log('', 1, "Success Changing SETEGID of {$file} to " . getmygid(), 3); elseif ((posix_setegid(filegroup(__file__))) !== false) $this->to_log('', 1, "Success Changing SETEGID of {$file} to " . filegroup(__file__), 3); if ((posix_setuid(getmyuid())) !== false) $this->to_log('', 1, "Success Changing SETUID of {$file} to " . getmyuid(), 3); elseif ((posix_setuid(get_current_user())) !== false) $this->to_log('', 1, "Success Changing SETUID of {$file} to " . get_current_user(), 3); if ((posix_seteuid(getmyuid())) !== false) $this->to_log('', 1, "Success Changing SETEUID of {$file} to " . getmyuid(), 3); elseif ((posix_seteuid(get_current_user())) !== false) $this->to_log('', 1, "Success Changing SETEUID of {$file} to " . get_current_user(), 3); if ((chmod($file, FS_CHMOD_DIR) || chmod($file, 0776) || chmod($file, 0766) || chmod($file, FS_CHMOD_FILE)) !== false) $this->to_log('', 1, "Success Changing Mode of {$file}", 3); if ((chown($file, getmyuid())) !== false) $this->to_log('', 1, "Success Changing Ownership of {$file} to " . getmyuid(), 3); elseif ((chown($file, get_current_user())) !== false) $this->to_log('', 1, "Success Changing Ownership of {$file} to " . get_current_user(), 3); if ((chgrp($file, getmygid())) !== false) $this->to_log('', 1, "Success Changing Group of {$file} to " . getmygid(), 3); elseif ((chgrp($file, filegroup(__file__))) !== false) $this->to_log('', 1, "Success Changing Group of {$file} to " . filegroup(__file__), 3); if ((chmod($file, FS_CHMOD_DIR) || chmod($file, 0776) || chmod($file, 0766) || chmod($file, FS_CHMOD_FILE)) !== false) $this->to_log('', 1, "Success Changing Mode of {$file}", 3); if ((chown($file, getmyuid())) !== false) $this->to_log('', 1, "Success Changing Ownership of {$file} to " . getmyuid(), 3); elseif ((chown($file, get_current_user())) !== false) $this->to_log('', 1, "Success Changing Ownership of {$file} to " . get_current_user(), 3); if ((chgrp($file, getmygid())) !== false) $this->to_log('', 1, "Success Changing Group of {$file} to " . getmygid(), 3); elseif ((chgrp($file, filegroup(__file__))) !== false) $this->to_log('', 1, "Success Changing Group of {$file} to " . filegroup(__file__), 3); return (!$this->_fclose($fh)) ? $this->to_log(__function__ . ':' . __line__ . " Error closing {$mode} handle for {$file}", 0) : $total;If php process isn't allowed to write to your web directory but you have an ftp account that is, then we request your ftp username/password in wordpress and if the php process running the
askapache-password-protect.php
plugin script is allowed access to raw networking sockets using fsockopen then we can basically access and write to your blog's .htaccess
file by using php to mimick an ftp client session. There are also other protocols and options available using php if ftp/fsockopen isn't allowed, but you run out of alternatives quick. Using the curl extension is one option.
So I wrote my own ftp library for a fsockopen class I had already developed for specific test requirements in unreleased versions, so the release of the new askapache password protect plugin will work for 75% or so of the people who have trouble now.. not to mention the insane logging and debugging I've added while looking for the reasons some web-hosts still don't work. Some use custom php security modules, wrappers, and custom virtual servers that are akin to a vmware server. So for maybe 10% of those running apache who have had problems they would still have them. I'm still playing with some ssh capability from within the plugin similar to the ftp technique.. I really hope WordPress just adds this functionality by updating their current filesystem classes..
Here's what I had several versions ago.. Just sticking it up here in case anyone is curious, one cool thing this version starts to incorporate is being able to send direct data payloads across the socket so it can be used like the metasploit framework to send payloads of exploits, but of course we're using it to mimick other protocols like ftp, which can be setup by feeding hex into the socket direct from a real ftp client, and piping the output. Keep in mind that this is my first time using php classes, so the learning curve has been incredible...
'1.0', 'method' => 'GET', 'referer' => 'https://www.askapache.com/', 'port' => '80', 'ua' => 'Mozilla/5.0 (compatible; AskApache_Net/1.6; https://www.askapache.com/)', 'scheme' => 'http', 'transport' => '', 'host' => '', 'user' => '', 'pass' => '', 'path' => '/', 'query' => '', 'fragment' => ''); var $authtype = 'Basic'; var $timeout = 15; var $_dh = ''; var $_digest = array('realm' => '', 'nonce' => '', 'uri' => '', 'algorithm' => 'MD5', 'qop' => 'auth', 'opaque' => '', 'domain' => '', 'nc' => '00000001', 'cnonce' => '82d057852a9dc497', 'A1' => '', 'A2' => '', 'response' => ''); var $_ACLF = "rn"; var $_request_body = ''; var $_request_headers = array(); var $_response_headers = array(); var $my_headers; var $_response_header = ''; var $_response_protocol = ''; var $_response_version = ''; var $_response_code = ''; var $_response_message = ''; var $_response_body = ''; var $_errs = array(3 => 'Socket creation failed', 4 => 'DNS lookup failure', 5 => 'Connection refused or timed out', 111 => 'Connection refused', 113 => 'No route to host', 110 => 'Connection timed out', 104 => 'Connection reset by client'); /** * AskApache_Net::AskApache_Net() */ function AskApache_Net() { return $this->__construct(); } /** * AskApache_Net::__destruct() */ function __destruct() { $this->_timer('class'); return true; } /** * AskApache_Net::__construct() */ function __construct() { $this->_timer('class'); $this->_ACLF = chr(13) . chr(10); @set_time_limit(60); return true; } /** * AskApache_Net::hsockit() */ function hsockit($URI) { $this->msg(__function__ . ':' . __line__, 3); $this->_socket['method'] = 'HEAD'; return $this->sockit($URI); } /** * AskApache_Net::sockit() */ function sockit($URI = '') { $this->msg(__function__ . ':' . __line__, 3); if (!$this->_build_sock($URI)) return $this->msg(__function__ . ':' . __line__, "Failed!", 0); if (!$this->_connect()) return $this->msg(__function__ . ':' . __line__, "Failed!", 0); $this->_build_request(); if (!$this->_build_request()) return $this->msg(__function__ . ':' . __line__, "Failed!", 0); if (!$this->_tx()) return $this->msg(__function__ . ':' . __line__, "tx Failed!", 0); if (!$this->_rx()) return $this->msg(__function__ . ':' . __line__, "rx Failed!", 0); if (!$this->_disconnect()) return $this->msg(__function__ . ':' . __line__, "disconnect Failed!", 0); if ((bool)$this->net_debug === true) { foreach (array('out_payload', '_request_body', '_response_header', '_response_body') as $nam) { if (is_array($this->$nam)) { if (sizeof($this->$nam) > 1) { echo "nn{$nam}n"; print_r($this->$nam); } } else { if (!empty($this->$nam)) { echo "nn{$nam}n"; echo $this->$nam; } } } $this->tcp_trace(1); } return (int)$this->_response_code; } /** * AskApache_Net::_build_sock() */ function _build_sock($url) { $this->msg(__function__ . ':' . __line__, 3); $socket_info = &$this->_socket; if (!$u_bits = parse_url($url)) return false; if (empty($u_bits['method'])) $u_bits['method'] = 'GET'; if (empty($u_bits['protocol'])) $u_bits['protocol'] = '1.0'; if (empty($u_bits['host'])) $u_bits['host'] = $_SERVER['HTTP_HOST']; if (empty($u_bits['scheme'])) $u_bits['scheme'] = 'http'; if (empty($u_bits['port'])) $u_bits['port'] = $_SERVER['SERVER_PORT']; $u_bits['path'] = (empty($u_bits['path']) ? '/' : $u_bits['path']) . (!empty($u_bits['query']) ? '?' . $u_bits['query'] : ''); if (empty($u_bits['ua'])) $u_bits['ua'] = 'Mozilla/5.0 (compatible; AskApache_Net/1.0; https://www.askapache.com)'; if (empty($u_bits['referer'])) $u_bits['referer'] = 'https://www.askapache.com'; if (empty($u_bits['fragment'])) unset($u_bits['fragment']); if (empty($u_bits['user'])) unset($u_bits['user']); if (empty($u_bits['pass'])) unset($u_bits['pass']); if ($u_bits['scheme'] == 'https' || $this->_socket['scheme'] == 'https') $u_bits['transport'] = 'ssl://'; if ($u_bits['scheme'] == 'https' || $this->_socket['scheme'] == 'https') $u_bits['port'] = '443'; $socket_info = $this->_parse_args($u_bits, $socket_info); extract($socket_info, EXTR_SKIP); return true; } /** * AskApache_Net::_build_auth_header() */ function _build_auth_header() { $this->msg(__function__ . ':' . __line__, 3); if ($this->authtype == 'Basic') $this->_request_headers[] = 'Authorization: Basic ' . base64_encode($this->_socket['user'] . ":" . $this->_socket['pass']); elseif ($this->authtype == 'Digest') { $this->msg(__function__ . ':' . __line__, 3); $this->_socket['protocol'] = '1.1'; $hdr = $mtx = array(); preg_match_all('/(w+)=(?:"([^"]+)"|([^s,]+))/', $this->_dh, $mtx, PREG_SET_ORDER); foreach ($mtx as $m) $hdr[$m[1]] = $m[2] ? $m[2] : $m[3]; foreach ($hdr as $key => $val) if (array_key_exists($key, $this->_digest) && !empty($val)) $this->_digest[$key] = $val; $this->_digest['uri'] = $this->_socket['path']; $this->_digest['A1'] = md5($this->_socket['user'] . ':' . $this->_digest['realm'] . ':' . $this->_socket['pass']); $this->_digest['A2'] = md5($this->_socket['method'] . ':' . $this->_socket['path']); $this->_digest['response'] = md5($this->_digest['A1'] . ':' . $this->_digest['nonce'] . ':' . $this->_digest['nc'] . ':' . $this->_digest['cnonce'] . ':' . $this->_digest['qop'] . ':' . $this->_digest['A2']); $this->_request_headers[] = sprintf('Authorization: Digest username="%1$s", realm="%2$s", nonce="%3$s",'. 'uri="%4$s", algorithm=%5$s, response="%6$s", qop="%7$s", nc="%8$s"%9$s%10$s', $this->_socket['user'], $this->_digest['realm'], $this->_digest['nonce'], $this-> _digest['uri'], $this->_digest['algorithm'], $this->_digest['response'], $this-> _digest['qop'], $this->_digest['nc'], !empty($this->_digest['cnonce']) ? ', cnonce="' . $this->_digest['cnonce'] . '"' : '', !empty($this->_digest['opaque']) ? ', opaque="' . $this->_digest['opaque'] . '"' : ''); } return true; } /** * AskApache_Net::_build_request() */ function _build_request() { $this->msg(__function__ . ':' . __line__, 3); $this->_request_headers[] = $this->_socket['method'] . " " . $this->_socket['path'] . " HTTP/" . $this->_socket['protocol']; if (is_array($this->my_headers) && sizeof($this->my_headers) > 0) $this-> _request_headers = array_merge($this->_request_headers, $this->my_headers); else { $this->_request_headers[] = "Host: " . $this->_socket['host']; $this->_request_headers[] = "User-Agent: " . $this->_socket['ua']; $this->_request_headers[] = 'Accept: application/xhtml+xml,text/html;q=0.9,*/*;q=0.5'; $this->_request_headers[] = 'Accept-Language: en-us,en;q=0.5'; $this->_request_headers[] = 'Accept-Encoding: none'; $this->_request_headers[] = 'Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7'; $this->_request_headers[] = 'Referer: ' . $this->_socket['referer']; } if (!empty($this->_socket['user']) && !empty($this->_socket['pass'])) $this-> _build_auth_header(); if ($this->out_payload !== false) $this->_request_body = $this->out_payload; else $this->_request_body = join($this->_ACLF, $this->_request_headers) . $this-> _ACLF . $this->_ACLF; return true; } /** * AskApache_Net::_tx() */ function _tx() { $this->msg(__function__ . ':' . __line__, 3); return (bool)(is_resource($this->_fp) && $this->_netwrite($this->_fp, $this-> _request_body)); } /** * AskApache_Net::_rx() */ function _rx() { $this->msg(__function__ . ':' . __line__, 3); if (!is_resource($this->_fp)) return false; $this->_response = $this->_netread($this->_fp, 500000); $parts = explode($this->_ACLF . $this->_ACLF, ltrim($this->_response), 2); $this->_response_header = trim($parts[0]); $this->_response_body = trim($parts[1]); if (preg_match('#([^/]*)/([d.]+) ([d]*?) (.*)#', $this->_response_header, $htx)) { $this->_response_protocol = trim($htx[1]); $this->_response_version = trim($htx[2]); $this->_response_code = trim($htx[3]); $this->_response_message = trim($htx[4]); } if (preg_match_all('#([^:]+):?(.*)#', str_replace($htx, '', $this->_response_header), $mtx, PREG_SET_ORDER)) { foreach ($mtx as $m) { $this->_headers[strtolower(trim($m[1]))] = trim($m[2]); if (preg_match('/(WWW|Proxy)-Authenticate:.*Digest/i', trim($m[1]))) $this->_dh = trim($m[1]); } } return true; } /** * AskApache_Net::tcp_trace() */ function tcp_trace($p = false) { $this->_timer(__function__ ); $ret = join("n", array_merge((array )$this->_request_headers, array(''), (array )$this-> _response_headers)); if ($p !== false) { echo $ret; $ret = true; } $this->_timer(__function__ ); return $ret; } /** * AskApache_Net::_get_ip() */ function _get_ip($host) { $this->msg(__function__ . ':' . __line__, 3); if (!preg_match('/^[t ]*[0-9]+.[0-9]+.[0-9]+.[0-9]+[t ]*$/', $host)) $hostip = gethostbyname($host); $ip = ($hostip == $host) ? $host : long2ip(ip2long($hostip)); return $ip; } /** * AskApache_Net::_connect() */ function _connect() { $this->msg(__function__ . ':' . __line__, 3); if (false === ($this->_fp = fsockopen($this->_get_ip($this->_socket['host']), $this-> _socket['port'], $errno, $errstr, $this->timeout)) || !is_resource($this->_fp)) { $err = (array_key_exists($errno, $this->_errs)) ? $this->_errs[$errno] : 'Connection failed'; return $this->msg(__function__ . ':' . __line__ . " Fsockopen failed! [{$errno}] {$err} ({$errstr})", 0); } if (function_exists("socket_set_timeout")) socket_set_timeout($this->_fp, $this-> timeout); elseif (function_exists("stream_set_timeout")) stream_set_timeout($this->_fp, $this-> timeout); usleep(10000); return true; } /** * AskApache_Net::_disconnect() */ function _disconnect() { $this->msg(__function__ . ':' . __line__, 3); if (is_resource($this->_fp)) return $this->_fclose($this->_fp); else $this->_fp = null; return true; } /** * AskApache_Net::get_response_headers() */ function get_response_headers($header = false) { $this->msg(__function__ . ':' . __line__, 3); if ($header !== false && array_key_exists($header, $this->_response_headers)) return $this-> _response_headers[$header]; return $this->_response_headers; } /** * AskApache_Net::get_response_body() */ function get_response_body() { $this->msg(__function__ . ':' . __line__, 3); return $this->_response_body; } /** * AskApache_Net::_netread() */ function _netread(&$fh, $ts = 50000000, $bs = 124) { $this->_timer(__function__ ); for ($d = $b = '', $rt = $at = $r = 0; ($fh !== false && !feof($fh) && $b !== false && $at < 50000000 && $rt < $ts); $r = $ts - $rt, $bs = (($bs > $r) ? $r : $bs), $this-> _timer("R: {$rt}"), $b = fread($fh, $bs), $br = strlen($b), $d .= $b, $this->_timer("R: {$rt}"), $rt += $br, $at++, $this->msg("[RT: {$rt}]t[BR: {$br}" . (($ts != 50000000) ? "]tt [{$r} / {$ts}]" : " : {$bs}]t[{$at}]"))) ; $this->_timer(__function__ ); return ((strlen($d) != 0)) ? $d : false; } /** * AskApache_Net::_netwrite() */ function _netwrite(&$fh, $d = '', $bs = 512) { $this->_timer(__function__ ); for ($bw = $wt = $at = 0, $dat = '', $ts = strlen($d); ($fh !== false && $bw !== false && $at < 50000000 && $wt < $ts); $r = $ts - $wt, $bs = (($bs > $r) ? $r : $bs), $dat = substr($d, $wt, $bs), $bw = fwrite($fh, $dat), $wt += $bw, $this->msg("[WT: {$wt}]t[BW: {$bw}]tt[I: {$r} / {$ts}:{$bs}] - {$at}"), $at++) ; $this->msg("[WT: {$wt}]t[BW: {$bw}]tt[I: {$r} / {$ts}:{$bs}] - {$at}"); $this->_timer(__function__ ); return ($wt == $ts) ? true : false; } } endif; ?>So I decided to finally give in to what I've been avoiding all along and added a php-software-based method that will work on everycomputer, windows, blackberrys, etc.. That took me about 15minutes as its just a few lines of code.. The problem I have with it is that php is what is actually controlling the sending, receiving, and verifying of the authentication headers instead of using the builtin super-secure apache method. Here's how you would block someone using the apache/askapache way:
[Exploit Request] => ([BLOCKED]-AskApache)This prevents the exploit from even reaching PHP, saving your computer a lot of CPU/memory and bandwdith, and obviously can't exploit wordpress if php isn't even loading. Here's how the php-software-based method blocks the same request:
[Exploit Request] => (AskApache) => (PHP) => (WordPress) => ([BLOCKED]-askapache-password-protect.php)So the last bit of programming and research I'm doing at the moment is how to cause the askapache-password-protect plugin to execute as soon as possible, ideally it would execute before WordPress starts.. And I am still crazy swamped at work, this was the longest non-posting period of the blog to date!