firewall/src/wordpress.php
<?php
namespace BitFireWP;
use BitFire\Request;
use ThreadFin\MaybeA;
use ThreadFinDB\Credentials;
use ThreadFinDB\DB;
use function ThreadFin\dbg;
use function ThreadFin\trace;
use BitFire\Config as CFG;
use const BitFire\DS;
use const BitFire\WAF_SRC;
require_once WAF_SRC . "db.php";
class Parts {
private $_x;
private $_names = array();
public static function of(string $separator, string $data) {
$p = new Parts();
$p->_x = explode($separator, $data);
return $p;
}
public function name(...$names) : Parts {
for ($i=0;$i<count($names);$i++) {
$this->_names[$names[$i]] = $i;
}
return $this;
}
public function at(string $name) : ?string {
if (!isset($this->_names[$name])) { return NULL; }
$idx = $this->_names[$name];
if ($idx > count($this->_x)) { return NULL; }
return $this->_x[$idx];
}
}
// concatenate all data with a concat glue
function concat_fn(string $bind_char) : callable {
return function(...$concat) use ($bind_char) : string {
$result = "";
for ($i=0,$m=count($concat);$i<$m;$i++) {
$result .= $concat[$i] . $bind_char;
}
return trim($result, $bind_char);
};
}
// take a single line and return the define value, suitable for array_reduce function
function define_to_array(array $input, string $define_line) : array {
if (preg_match("/define\s*\(\s*['\"]([a-zA-Z_]+)['\"]\s*,\s*['\"]([^'\"]+)['\"]/", $define_line, $matches)) {
$input[$matches[1]] = $matches[2];
}
if (preg_match("/\\$([\w_]+)\s*=\s*['\"]?([a-z0-9A-Z_\.-]+)/", $define_line, $matches)) {
$input[$matches[1]] = $matches[2];
}
return $input;
}
// turn define array into credentials
function array_to_credentials(?array $defines) :?Credentials {
$credentials = NULL;
if ($defines && count($defines) > 5) {
$credentials = new Credentials($defines['DB_USER']??'', $defines['DB_PASSWORD']??'', $defines['DB_HOST']??'', $defines['DB_NAME']??'', $defines['table_prefix']??'wp_');
}
return $credentials;
}
// parse wp-config into db credentials
function wp_parse_credentials(string $root) : ?Credentials {
$credentials = NULL;
$defines = wp_parse_define("$root/wp-config.php");
if (isset($defines["SECURE_AUTH_KEY"])) {
$credentials = array_to_credentials($defines);
}
return $credentials;
}
// parse out all defines from the wp-config
function wp_parse_define(string $file) : array {
$defines = [];
if (file_exists($file)) {
$data = file($file);
if (!empty($data)) {
$defines = array_reduce($data, '\BitFireWP\define_to_array', []);
}
}
return $defines;
}
// fetch an auth "salt" for a particular "scheme"
function wp_fetch_salt(string $root, string $scheme) : string {
$scheme = strtoupper($scheme);
$defines = wp_parse_define("$root/wp-config.php");
if (!isset($defines["{$scheme}_KEY"])) { debug("auth define [%s] missing", $scheme); return ""; }
return $defines["{$scheme}_KEY"] . $defines["{$scheme}_SALT"];
}
// validate an auth cookie
function wp_validate_cookie(string $cookie, string $root) : bool {
$data = Parts::of("|", $cookie)->name("username", "exp", "token", "hmac");
$credentials = wp_parse_credentials($root);
$db = DB::cred_connect($credentials);
$sql = $db->fetch("SELECT SUBSTRING(user_pass, 9, 4) AS pass FROM " . $credentials->prefix . "users WHERE user_login = {login} LIMIT 1", array("login" => $data->at("username")));
if ($sql->empty()) { debug("wp-auth failed to load db user data"); return false; }
$key_src = concat_fn("|")($data->at("username"), $sql->col("pass"), $data->at("exp"), $data->at("token"));
// first try to auth with data from the config file
$key_list = array("auth", "secure_auth", "logged_in");
foreach ($key_list as $name) {
$key = hash_hmac('md5', $key_src, wp_fetch_salt($root, $name));
$hash = hash_hmac(function_exists('hash')?'sha256':'sha1', concat_fn("|")($data->at("username"), $data->at("exp"), $data->at("token")), $key);
if (hash_equals($hash, $data->at("hmac"))) { debug("config key wp match [%s]", $name); return true; }
}
// that failed, lets try the db salt and key (may need to try logged_in_key/salt also)
$db_salt = $db->fetch("SELECT option_value FROM " . $credentials->prefix . "options where option_name = 'auth_salt'");
if (!$db_salt->empty()) {
$db_key = $db->fetch("SELECT option_value FROM " . $credentials->prefix . "options where option_name = 'auth_key'");
if (!$db_salt->empty()) {
$salt = $db_salt->col('option_value')();
$key = $db_key->col('option_value')();
$full_key = $key . $salt;
$key = hash_hmac('md5', $key_src, $full_key);
$hash = hash_hmac(function_exists('hash')?'sha256':'sha1', concat_fn("|")($data->at("username"), $data->at("exp"), $data->at("token")), $key);
if (hash_equals($hash, $data->at("hmac"))) { debug("db key wp match [%s]", $name); return true; }
}
}
debug("wp auth failed");
return false;
}
// return the wp cookie value
function wp_get_login_cookie(array $cookies) : string {
$wp = array_filter($cookies, function ($x) {
if (strpos($x, "wordpress_") !== false) {
if ((strpos($x, "wordpress_logged_in") === false) && (strpos($x, "wordpress_test") === false)) {
return true;
}
}
return false;
}, ARRAY_FILTER_USE_KEY);
if (count($wp) < 1) { return ""; }
return array_values($wp)[0];
}
function machine_date($time) : string {
return date("Y-m-d", (int)$time);
}
function bytes_to_kb($bytes) : string {
return round((int)$bytes / 1024, 1) . "Kb";
}
function wp_enrich_wordpress_hash_diffs(string $ver, string $doc_root, array $hash): array
{
if (!isset($hash['path'])) { return $hash; }
$paths = explode('/', $hash['path']);
$out = '/' . trim($doc_root, '/') . ($hash['path'][0] != DS) ? DS : '' . $hash['path'];
$path = "https://core.svn.wordpress.org/tags/{$ver}{$hash['path']}";
if (strpos($doc_root, '/plugins') !== false) {
$path = "https://plugins.svn.wordpress.org/{$hash['name']}/tags/{$ver}/{$hash['path']}";
$hash['out'] = '/wp-content/plugins/' . $hash['name'] . $hash['path'];
} else if (strpos($doc_root, '/themes') !== false) {
$path = "https://themes.svn.wordpress.org/{$hash['name']}/{$ver}/{$hash['path']}";
$hash['out'] = '/wp-content/themes/' . $hash['name'] . $hash['path'];
} else {
$hash['out'] = $hash['path'];
}
$hash['mtime'] = filemtime($out);
$hash['url'] = $path;
$hash['ver'] = $ver;
$hash['doc_root'] = $doc_root;
$hash['machine_date'] = machine_date($hash['mtime']);
$hash['type'] = ($hash['size2']??$hash['size'] > 0) ? "WordPress file" : "Unknown file";
$hash['kb1'] = bytes_to_kb($hash['size']);
$hash['kb2'] = bytes_to_kb($hash['size2']??$hash['size']);
$hash['bgclass'] = ($hash['size2']??$hash['size'] > 0) ? "bg-success-soft" : "bg-danger-soft";
$hash['icon'] = ($hash['size2']??$hash['size'] > 0) ? "fe-check" : "fe-x";
return $hash;
}
/**
* unlock core / themes or plugins and re-lock after this request
* @param string $content_path relative path under wp-root
*/
function temp_lock_dir(string $content_path) {
debug("update wp");
if (function_exists("\BitFire\lock_site_dir")) {
\BitFire\lock_site_dir($content_path, false);
register_shutdown_function(function() use($content_path) {
debug("shutdown called re-lock: [%s]",$content_path);
\BitFire\lock_site_dir($content_path, true);
});
}
else { debug("no lock site dir"); }
}
/**
*
* @param Request $request
* @param MaybeA $cookie
* @return void
*/
function wp_handle_admin(\BitFire\Request $request, MaybeA $cookie) {
debug("wp_handle_admin");
$root = \BitFire\Config::str("cms_root");
if (empty($root)) { debug("no cms_root"); return; }
if (strpos($request->path, "/wp-admin/") === false) { debug("no wp-admin"); return; }
if ($request->post['action']??'' === "heartbeat") { return; }
debug("wp admin request %s", $request->path);
}
function get_credentials() : ?Credentials {
if (defined("WPINC") && defined("DB_USER")) {
$credentials = new Credentials(DB_USER, DB_PASSWORD, DB_HOST, DB_NAME);
if (isset($GLOBALS['wpdb'])) {
trace("WPDB");
$credentials->prefix = $GLOBALS['wpdb']->prefix;
return $credentials;
}
} else {
trace("BITDB");
$credentials = wp_parse_credentials(CFG::str("cms_root"));
$defs = wp_parse_define(CFG::str("cms_root")."/wp-config.php");
if (!empty($credentials)) {
$credentials->prefix = $defs["table_prefix"]??"wp_";
}
return $credentials;
}
return NULL;
}
function get_db_connection() : ?DB {
$credentials = get_credentials();
if ($credentials) {
return DB::cred_connect($credentials);
}
return NULL;
}