feat: h5ai 0.30.0 with PHP 8.4.26 security patch for PHP2Shell

This commit is contained in:
Patrick Asmus
2026-10-21 21:45:49 +02:00
parent 3568d43858
commit e6536444ad
133 changed files with 4363 additions and 29 deletions
+188
View File
@@ -0,0 +1,188 @@
<?php
class Archive {
const NULL_BYTE = "\0";
private static $SEGMENT_SIZE = 16777216; // 1024 * 1024 * 16 = 16MiB
private static $TAR_PASSTHRU_CMD = 'cd [ROOTDIR] && tar --no-recursion -c -- [DIRS] [FILES]';
private static $ZIP_PASSTHRU_CMD = 'cd [ROOTDIR] && zip - -- [FILES]';
private $context;
private $base_path;
private $dirs;
private $files;
public function __construct($context) {
$this->context = $context;
}
public function output($type, $base_href, $hrefs) {
$this->base_path = $this->context->to_path($base_href);
if (!$this->context->is_managed_path($this->base_path)) {
return false;
}
$this->dirs = [];
$this->files = [];
$this->add_hrefs($hrefs);
if (count($this->dirs) === 0 && count($this->files) === 0) {
if ($type === 'php-tar') {
$this->add_dir($this->base_path, '/');
} else {
$this->add_dir($this->base_path, '.');
}
}
if ($type === 'php-tar') {
return $this->php_tar($this->dirs, $this->files);
} elseif ($type === 'shell-tar') {
return $this->shell_cmd(Archive::$TAR_PASSTHRU_CMD);
} elseif ($type === 'shell-zip') {
return $this->shell_cmd(Archive::$ZIP_PASSTHRU_CMD);
}
return false;
}
private function shell_cmd($cmd) {
$cmd = str_replace('[ROOTDIR]', escapeshellarg($this->base_path), $cmd);
$cmd = str_replace('[DIRS]', count($this->dirs) ? implode(' ', array_map('escapeshellarg', $this->dirs)) : '', $cmd);
$cmd = str_replace('[FILES]', count($this->files) ? implode(' ', array_map('escapeshellarg', $this->files)) : '', $cmd);
try {
Util::passthru_cmd($cmd);
} catch (Exeption $err) {
return false;
}
return true;
}
private function php_tar($dirs, $files) {
$filesizes = [];
$total_size = 512 * count($dirs);
foreach (array_keys($files) as $real_file) {
$size = filesize($real_file);
$filesizes[$real_file] = $size;
$total_size += 512 + $size;
if ($size % 512 != 0) {
$total_size += 512 - ($size % 512);
}
}
header('Content-Length: ' . $total_size);
foreach ($dirs as $real_dir => $archived_dir) {
echo $this->php_tar_header($archived_dir, 0, @filemtime($real_dir . DIRECTORY_SEPARATOR . '.'), 5);
}
foreach ($files as $real_file => $archived_file) {
$size = $filesizes[$real_file];
echo $this->php_tar_header($archived_file, $size, @filemtime($real_file), 0);
$this->print_file($real_file);
if ($size % 512 != 0) {
echo str_repeat(Archive::NULL_BYTE, 512 - ($size % 512));
}
}
return true;
}
private function php_tar_header($filename, $size, $mtime, $type) {
$name = substr(basename($filename), -99);
$prefix = substr(Util::normalize_path(dirname($filename)), -154);
if ($prefix === '.') {
$prefix = '';
}
$header =
str_pad($name, 100, Archive::NULL_BYTE) // filename [100]
. '0000755' . Archive::NULL_BYTE // file mode [8]
. '0000000' . Archive::NULL_BYTE // uid [8]
. '0000000' . Archive::NULL_BYTE // gid [8]
. str_pad(decoct($size), 11, '0', STR_PAD_LEFT) . Archive::NULL_BYTE // file size [12]
. str_pad(decoct($mtime), 11, '0', STR_PAD_LEFT) . Archive::NULL_BYTE // file modification time [12]
. ' ' // checksum [8]
. str_pad($type, 1) // file type [1]
. str_repeat(Archive::NULL_BYTE, 100) // linkname [100]
. 'ustar' . Archive::NULL_BYTE // magic [6]
. '00' // version [2]
. str_repeat(Archive::NULL_BYTE, 80) // uname, gname, defmajor, devminor [32 + 32 + 8 + 8]
. str_pad($prefix, 155, Archive::NULL_BYTE) // filename [155]
. str_repeat(Archive::NULL_BYTE, 12); // fill [12]
assert(strlen($header) === 512);
// checksum
$checksum = array_sum(array_map('ord', str_split($header)));
$checksum = str_pad(decoct($checksum), 6, '0', STR_PAD_LEFT) . Archive::NULL_BYTE . ' ';
$header = substr_replace($header, $checksum, 148, 8);
return $header;
}
private function print_file($file) {
// Send file content in segments to not hit PHP's memory limit (default: 128M)
if ($fd = fopen($file, 'rb')) {
while (!feof($fd)) {
print fread($fd, Archive::$SEGMENT_SIZE);
@ob_flush();
@flush();
}
fclose($fd);
}
}
private function add_hrefs($hrefs) {
if (!is_array($hrefs)) {
$hrefs = array($hrefs);
}
foreach ($hrefs as $href) {
if (trim($href) === '') {
continue;
}
$href = Util::normalize_path($href, false);
$d = dirname($href);
$n = basename($href);
if ($this->context->is_managed_href($d) && !$this->context->is_hidden($n)) {
$real_file = $this->context->to_path($href);
$archived_file = preg_replace('!^' . preg_quote(Util::normalize_path($this->base_path, true)) . '!', '', $real_file);
if (is_dir($real_file)) {
$this->add_dir($real_file, $archived_file);
} else {
$this->add_file($real_file, $archived_file);
}
}
}
}
private function add_file($real_file, $archived_file) {
if (is_readable($real_file)) {
$this->files[$real_file] = $archived_file;
}
}
private function add_dir($real_dir, $archived_dir) {
if ($this->context->is_managed_path($real_dir)) {
$this->dirs[$real_dir] = $archived_dir;
$files = $this->context->read_dir($real_dir);
foreach ($files as $file) {
$real_file = $real_dir . '/' . $file;
$archived_file = $archived_dir . '/' . $file;
if (is_dir($real_file)) {
$this->add_dir($real_file, $archived_file);
} else {
$this->add_file($real_file, $archived_file);
}
}
}
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
class Custom {
private static $EXTENSIONS = ['html', 'md'];
private $context;
public function __construct($context) {
$this->context = $context;
}
private function read_custom_file($path, $name, &$content, &$type) {
$file_prefix = $this->context->get_setup()->get('FILE_PREFIX');
foreach (Custom::$EXTENSIONS as $ext) {
$file = $path . '/' . $file_prefix . '.' . $name . '.' . $ext;
if (is_readable($file)) {
$content = file_get_contents($file);
$type = $ext;
return;
}
}
}
public function get_customizations($href) {
if (!$this->context->query_option('custom.enabled', false)) {
return [
'header' => ['content' => null, 'type' => null],
'footer' => ['content' => null, 'type' => null]
];
}
$root_path = $this->context->get_setup()->get('FILE_PREFIX');
$path = $this->context->to_path($href);
$header = null;
$header_type = null;
$footer = null;
$footer_type = null;
$this->read_custom_file($path, 'header', $header, $header_type);
$this->read_custom_file($path, 'footer', $footer, $footer_type);
while ($header === null || $footer === null) {
if ($header === null) {
$this->read_custom_file($path, 'headers', $header, $header_type);
}
if ($footer === null) {
$this->read_custom_file($path, 'footers', $footer, $footer_type);
}
if ($path === $root_path) {
break;
}
$parent_path = Util::normalize_path(dirname($path));
if ($parent_path === $path) {
break;
}
// Stop once we reach the root
if (
$this->context->query_option('custom.stopSearchingAtRoot', true) &&
$path === $this->context->get_setup()->get('ROOT_PATH')
) {
break;
}
$path = $parent_path;
}
return [
'header' => ['content' => $header, 'type' => $header_type],
'footer' => ['content' => $footer, 'type' => $footer_type]
];
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
class Search {
private $context;
public function __construct($context) {
$this->context = $context;
}
public function get_paths($root, $pattern = null, $ignorecase = false) {
$paths = [];
if ($pattern && $this->context->is_managed_path($root)) {
$re = Util::wrap_pattern($pattern);
if ($ignorecase) {
$re .= 'i';
}
$names = $this->context->read_dir($root);
foreach ($names as $name) {
$path = $root . '/' . $name;
if (preg_match($re, @basename($path))) {
$paths[] = $path;
}
if (@is_dir($path)) {
$paths = array_merge($paths, $this->get_paths($path, $pattern, $ignorecase));
}
}
}
return $paths;
}
public function get_items($href, $pattern = null, $ignorecase = false) {
$cache = [];
$root = $this->context->to_path($href);
$paths = $this->get_paths($root, $pattern, $ignorecase);
$items = array_map(function ($path) {
return Item::get($this->context, $path, $cache)->to_json_object();
}, $paths);
return $items;
}
}
+253
View File
@@ -0,0 +1,253 @@
<?php
class Thumb {
private static $FFMPEG_CMDV = ['ffmpeg', '-ss', '0:00:10', '-i', '[SRC]', '-an', '-vframes', '1', '[DEST]'];
private static $AVCONV_CMDV = ['avconv', '-ss', '0:00:10', '-i', '[SRC]', '-an', '-vframes', '1', '[DEST]'];
private static $CONVERT_CMDV = ['convert', '-density', '200', '-quality', '100', '-strip', '[SRC][0]', '[DEST]'];
private static $GM_CONVERT_CMDV = ['gm', 'convert', '-density', '200', '-quality', '100', '[SRC][0]', '[DEST]'];
private static $THUMB_CACHE = 'thumbs';
private $context;
private $setup;
private $thumbs_path;
private $thumbs_href;
public function __construct($context) {
$this->context = $context;
$this->setup = $context->get_setup();
$this->thumbs_path = $this->setup->get('CACHE_PUB_PATH') . '/' . Thumb::$THUMB_CACHE;
$this->thumbs_href = $this->setup->get('CACHE_PUB_HREF') . Thumb::$THUMB_CACHE;
if (!is_dir($this->thumbs_path)) {
@mkdir($this->thumbs_path, 0755, true);
}
}
public function thumb($type, $source_href, $width, $height) {
$source_path = $this->context->to_path($source_href);
if (!file_exists($source_path) || Util::starts_with($source_path, $this->setup->get('CACHE_PUB_PATH'))) {
return null;
}
$capture_path = $source_path;
if ($type === 'img') {
$capture_path = $source_path;
} elseif ($type === 'mov') {
if ($this->setup->get('HAS_CMD_AVCONV')) {
$capture_path = $this->capture(Thumb::$AVCONV_CMDV, $source_path);
} elseif ($this->setup->get('HAS_CMD_FFMPEG')) {
$capture_path = $this->capture(Thumb::$FFMPEG_CMDV, $source_path);
}
} elseif ($type === 'doc') {
if ($this->setup->get('HAS_CMD_CONVERT')) {
$capture_path = $this->capture(Thumb::$CONVERT_CMDV, $source_path);
} elseif ($this->setup->get('HAS_CMD_GM')) {
$capture_path = $this->capture(Thumb::$GM_CONVERT_CMDV, $source_path);
}
}
return $this->thumb_href($capture_path, $width, $height);
}
private function thumb_href($source_path, $width, $height) {
if (!file_exists($source_path)) {
return null;
}
$name = 'thumb-' . sha1($source_path) . '-' . $width . 'x' . $height . '.jpg';
$thumb_path = $this->thumbs_path . '/' . $name;
$thumb_href = $this->thumbs_href . '/' . $name;
if (!file_exists($thumb_path) || filemtime($source_path) >= filemtime($thumb_path)) {
$image = new Image();
$et = false;
if ($this->setup->get('HAS_PHP_EXIF') && $this->context->query_option('thumbnails.exif', false) === true && $height != 0) {
$et = @exif_thumbnail($source_path);
}
if($et !== false) {
file_put_contents($thumb_path, $et);
$image->set_source($thumb_path);
$image->normalize_exif_orientation($source_path);
} else {
$image->set_source($source_path);
}
$image->thumb($width, $height);
$image->save_dest_jpeg($thumb_path, 80);
}
return file_exists($thumb_path) ? $thumb_href : null;
}
private function capture($cmdv, $source_path) {
if (!file_exists($source_path)) {
return null;
}
$capture_path = $this->thumbs_path . '/capture-' . sha1($source_path) . '.jpg';
if (!file_exists($capture_path) || filemtime($source_path) >= filemtime($capture_path)) {
foreach ($cmdv as &$arg) {
$arg = str_replace('[SRC]', $source_path, $arg);
$arg = str_replace('[DEST]', $capture_path, $arg);
}
Util::exec_cmdv($cmdv);
}
return file_exists($capture_path) ? $capture_path : null;
}
}
class Image {
private $source_file;
private $source;
private $width;
private $height;
private $type;
private $dest;
public function __construct($filename = null) {
$this->source_file = null;
$this->source = null;
$this->width = null;
$this->height = null;
$this->type = null;
$this->dest = null;
$this->set_source($filename);
}
public function __destruct() {
$this->release_source();
$this->release_dest();
}
public function set_source($filename) {
$this->release_source();
$this->release_dest();
if (is_null($filename)) {
return;
}
$this->source_file = $filename;
list($this->width, $this->height, $this->type) = @getimagesize($this->source_file);
if (!$this->width || !$this->height) {
$this->source_file = null;
$this->width = null;
$this->height = null;
$this->type = null;
return;
}
$this->source = imagecreatefromstring(file_get_contents($this->source_file));
}
public function save_dest_jpeg($filename, $quality = 80) {
if (!is_null($this->dest)) {
@imagejpeg($this->dest, $filename, $quality);
@chmod($filename, 0775);
}
}
public function release_dest() {
if (!is_null($this->dest)) {
@imagedestroy($this->dest);
$this->dest = null;
}
}
public function release_source() {
if (!is_null($this->source)) {
@imagedestroy($this->source);
$this->source_file = null;
$this->source = null;
$this->width = null;
$this->height = null;
$this->type = null;
}
}
public function thumb($width, $height) {
if (is_null($this->source)) {
return;
}
$src_r = 1.0 * $this->width / $this->height;
if ($height == 0) {
if ($src_r >= 1) {
$height = 1.0 * $width / $src_r;
} else {
$height = $width;
$width = 1.0 * $height * $src_r;
}
if ($width > $this->width) {
$width = $this->width;
$height = $this->height;
}
}
$ratio = 1.0 * $width / $height;
if ($src_r <= $ratio) {
$src_w = $this->width;
$src_h = $src_w / $ratio;
$src_x = 0;
} else {
$src_h = $this->height;
$src_w = $src_h * $ratio;
$src_x = 0.5 * ($this->width - $src_w);
}
$width = intval($width);
$height = intval($height);
$src_x = intval($src_x);
$src_w = intval($src_w);
$src_h = intval($src_h);
$this->dest = imagecreatetruecolor($width, $height);
$icol = imagecolorallocate($this->dest, 255, 255, 255);
imagefill($this->dest, 0, 0, $icol);
imagecopyresampled($this->dest, $this->source, 0, 0, $src_x, 0, $width, $height, $src_w, $src_h);
}
public function rotate($angle) {
if (is_null($this->source) || ($angle !== 90 && $angle !== 180 && $angle !== 270)) {
return;
}
$this->source = imagerotate($this->source, $angle, 0);
if ( $angle === 90 || $angle === 270 ) {
list($this->width, $this->height) = [$this->height, $this->width];
}
}
public function normalize_exif_orientation($exif_source_file = null) {
if (is_null($this->source) || !function_exists('exif_read_data')) {
return;
}
if ($exif_source_file === null) {
$exif_source_file = $this->source_file;
}
$exif = exif_read_data($exif_source_file);
switch (@$exif['Orientation']) {
case 3:
$this->rotate(180);
break;
case 6:
$this->rotate(270);
break;
case 8:
$this->rotate(90);
break;
}
}
}