Invision Power Services, Inc. * @copyright (c) Invision Power Services, Inc. * @license https://www.invisioncommunity.com/legal/standards/ * @package Invision Community * @since 31 Jul 2017 */ // This file deliberately exists outside of the framework // and MUST NOT call init.php @ini_set('display_errors', 'off'); @require( "../../system/3rd_party/pclzip/pclzip.lib.php" ); if ( file_exists( "../../constants.php" ) ) { require "../../constants.php"; } foreach ( [ 'FOLDER_PERMISSION_NO_WRITE' => 0755, 'FILE_PERMISSION_NO_WRITE' => 0644, 'IPS_FILE_PERMISSION' => 0666, 'TEMP_DIRECTORY' => sys_get_temp_dir(), 'TEXT_ENCRYPTION_KEY' => NULL ] as $k => $v ) { if ( !defined( $k ) ) { define( $k, $v ); } } /** * Extractor Class */ class Extractor { const NUMBER_PER_GO = 20; /** * @brief File */ private $file; /** * @brief Root Directory */ private $container; /** * @brief PclZip Object */ private $zip; /** * @brief Zip Contents */ private $contents; /** * @brief FTP Connection */ private $ftp; /** * @brief SSH Connection */ private $ssh; /** * @brief SFTP Connection */ private $sftp; /** * @brief SFTP Directory */ private $sftpDir; /** * @brief Database connection */ public $db; /** * @brief Database prefix */ public $dbPrefix; /** * Constructor * * @param string $file Zip File * @param string $container Root directory * @return void */ public function __construct( $file, $container ) { if ( !$file or !$container ) { throw new Exception( "Zip file or directory to extract to was not supplied" ); } $this->file = $file; $this->container = $container . '/'; } /** * Security Check * * @param string $key Security key * @return void */ public function securityCheck( $key ) { require "../../conf_global.php"; $this->db = new mysqli( $INFO['sql_host'], $INFO['sql_user'], $INFO['sql_pass'], $INFO['sql_database'], !empty( $INFO['sql_port'] ) ? $INFO['sql_port'] : null, !empty( $INFO['sql_socket'] ) ? $INFO['sql_socket'] : null ); $this->dbPrefix = $INFO['sql_tbl_prefix'] ?? ''; /* If the connection failed, do not continue */ if( $error = mysqli_connect_error() ) { return FALSE; } /* Check that there is an upgrade in progress */ $query = $this->db->query( "SELECT conf_value FROM {$this->dbPrefix}core_sys_conf_settings WHERE conf_key='setup_in_progress'" ); if( !$query ) { return FALSE; } if( (bool) $query->fetch_assoc()['conf_value'] === FALSE ) { return FALSE; } return $this->_compareHashes( md5( $INFO['board_start'] . $_GET['file'] . $INFO['sql_pass'] ), $key ); } /** * Establish FTP/SSH connection * * @param array $value FTP/SSH Credentials * @return void */ public function connectToFtp( $value ) { if ( $value['protocol'] == 'sftp' ) { $this->ssh = ssh2_connect( $value['server'], $value['port'] ); if ( $this->ssh === FALSE ) { throw new Exception( "Could not connect to SCP" ); } if ( !@ssh2_auth_password( $this->ssh, $value['un'], $value['pw'] ) ) { throw new Exception( "Could not login to SCP" ); } $this->sftp = @ssh2_sftp( $this->ssh ); if ( $this->sftp === FALSE ) { throw new Exception( "Could not initiate SFTP connection" ); } if ( $value['path'] and !@ssh2_sftp_stat( $this->sftp, $value['path'] ) ) { throw new Exception( "Could not locate path via SFTP" ); } $this->sftpDir = ssh2_sftp_realpath( $this->sftp, $value['path'] ) . '/'; } else { if ( $value['protocol'] == 'ssl_ftp' ) { $this->ftp = @ftp_ssl_connect( $value['server'], $value['port'], 3 ); } else { $this->ftp = @ftp_connect( $value['server'], $value['port'], 3 ); } if ( $this->ftp === FALSE ) { throw new Exception( "Could not connect to FTP" ); } if ( !@ftp_login( $this->ftp, $value['un'], $value['pw'] ) ) { throw new Exception( "Could not login to FTP" ); } if( ftp_nlist( $this->ftp, '.' ) === FALSE ) { @ftp_pasv( $this->ftp, true ); } if ( !@ftp_chdir( $this->ftp, $value['path'] ) ) { throw new Exception( "Could not change directory to {$value['path']}" ); } } } /** * Number of files * * @return int */ public function numberOfFiles() { $properties = $this->zip->properties(); return $properties['nb']; } /** * Extract the files * * @param int $offset Offset * @return int */ public function extract( $offset ) { $this->zip = new PclZip( $this->file ); $this->contents = $this->zip->listContent(); if ( !$this->contents ) { throw new Exception( "Could not list the files in the zip" ); } $done = 0; for ( $i = 0; $i < self::NUMBER_PER_GO; $i++ ) { $index = $offset + $i; if ( isset( $this->contents[ $index ] ) ) { $this->_extractFile( $index ); $done++; } else { return $done; } } return $done; } /** * Extract a particular file * * @param int $index File index in zip * @return void */ private function _extractFile( $index ) { /* Get the file */ $path = mb_substr( $this->contents[ $index ]['filename'], 0, mb_strlen( $this->container ) ) === $this->container ? mb_substr( $this->contents[ $index ]['filename'], mb_strlen( $this->container ) ) : $this->contents[ $index ]['filename']; if ( !$path or mb_substr( $path, -1 ) === '/' or mb_substr( $path, 0, 1 ) === '.' or mb_substr( $path, 0, 9 ) === '__MACOSX/' ) { return; } /* Create a directory if needed */ $dir = dirname( $path ); $directories = array( $dir ); while ( $dir != '.' ) { $dir = dirname( $dir ); if ( $dir != '.' ) { $directories[] = $dir; } } foreach ( array_reverse( $directories ) as $dir ) { if ( !is_dir( "../../{$dir}" ) ) { if ( $this->sftp ) { @ssh2_sftp_mkdir( $this->sftp, $this->sftpDir . $dir ); } if ( $this->ftp ) { @ftp_mkdir( $this->ftp, $dir ); } else { @mkdir( "../../{$dir}", FOLDER_PERMISSION_NO_WRITE ); } } } /* Write contents */ $contents = @$this->zip->extractByIndex( $index, PCLZIP_OPT_EXTRACT_AS_STRING); $contents = $contents[0]['content']; if ( $this->sftp or $this->ftp ) { $tmpFile = tempnam( TEMP_DIRECTORY, 'IPS' ); file_put_contents( $tmpFile, $contents ); if ( $this->sftp ) { if ( @ssh2_scp_send( $this->ssh, $tmpFile, $this->sftpDir . $path, FILE_PERMISSION_NO_WRITE ) === FALSE ) { throw new Exception( "Could not transfer file to server via SFTP" ); } } else { if ( @ftp_put( $this->ftp, $path, $tmpFile, FTP_BINARY ) === FALSE ) { throw new Exception( "Could not transfer file to server via FTP" ); } } @unlink( $tmpFile ); } else { /* Determine if the file exists (we'll want to know this later) */ $fileExists = file_exists( "../../{$path}" ); $fh = @fopen( "../../{$path}", 'w+' ); if ( $fh === FALSE ) { $lastError = error_get_last(); throw new Exception( $lastError['message'] ); } if ( @fwrite( $fh, $contents ) === FALSE ) { $lastError = error_get_last(); throw new Exception( $lastError['message'] ); } else { /* If the file existed before we started, we should clear it from opcache if opcache is enabled */ if( $fileExists ) { if ( function_exists( 'opcache_invalidate' ) ) { @opcache_invalidate( "../../{$path}" ); } } /* Otherwise, we should set file permissions on the file if it's brand new in case server defaults to something odd */ else { @chmod( "../../{$path}", FILE_PERMISSION_NO_WRITE ); } } @fclose( $fh ); } } /* !Misc Utility Methods */ /** * Compare hashes in fixed length, time constant manner. * This is replicated from \IPS\Login::compareHashes(), however we don't want to include framework code here. * * @param string $expected The expected hash * @param string $provided The provided input * @return boolean */ private function _compareHashes( $expected, $provided ) { if ( !is_string( $expected ) || !is_string( $provided ) || $expected === '*0' || $expected === '*1' || $provided === '*0' || $provided === '*1' ) // *0 and *1 are failures from crypt() - if we have ended up with an invalid hash anywhere, we will reject it to prevent a possible vulnerability from deliberately generating invalid hashes { return FALSE; } $len = strlen( $expected ); if ( $len !== strlen( $provided ) ) { return FALSE; } $status = 0; for ( $i = 0; $i < $len; $i++ ) { $status |= ord( $expected[ $i ] ) ^ ord( $provided[ $i ] ); } return $status === 0; } } /** * Encrypted - code from \IPS\Text\Encrypt */ class Encrypt { /** * Get Key * * @return void */ public static function key() { require "../../conf_global.php"; return TEXT_ENCRYPTION_KEY ?: md5( $INFO['sql_pass'] . $INFO['sql_database'] ); } /** * @brief Cipher */ public $cipher; /** * @brief IV */ protected $iv = NULL; /** * @brief Tag */ protected $tag = NULL; /** * @brief Hash of cipher */ protected $hmac = NULL; /** * From plaintext * * @param string $plaintext Plaintext * @return static */ public static function fromPlaintext( $plaintext ) { $obj = new static; /* Try to use OpenSSL if it's available... */ if ( function_exists( 'openssl_get_cipher_methods' ) ) { /* If GCM is available (PHP 7.1+), use that as if provides authenticated encryption natively */ if ( in_array( 'aes-128-gcm', openssl_get_cipher_methods() ) ) { $obj->iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( 'aes-128-gcm' ) ); $obj->cipher = openssl_encrypt( $plaintext, 'aes-128-gcm', static::key(), 0, $obj->iv, $obj->tag ); return $obj; } /* Otherwise, use CBC and store the hash so we can do our own authentication when decrypting */ elseif ( in_array( 'aes-128-cbc', openssl_get_cipher_methods() ) ) { $obj->iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( 'aes-128-cbc' ) ); $obj->cipher = openssl_encrypt( $plaintext, 'aes-128-cbc', static::key(), OPENSSL_RAW_DATA, $obj->iv ); $obj->hmac = hash_hmac( 'sha256', $obj->cipher, static::key(), TRUE ); return $obj; } } /* If we're still here, fallback to the PHP library */ require_once '../../system/3rd_party/AES/AES.php'; $obj->cipher = AesCtr::encrypt( $plaintext, static::key(), 256 ); return $obj; } /** * From plaintext * * @param string $cipher Cipher * @param string|null $iv The IV, or if null, will use the PHP library rather than built-in openssl_*() methods * @param string|null $tag The tag if using AES-128-GCM * @param string|null $hash The hash if using AES-128-CBC * @return static */ public static function fromCipher( $cipher, $iv = NULL, $tag = NULL, $hash = NULL ) { $obj = new static; $obj->cipher = $cipher; $obj->iv = $iv; $obj->tag = $tag; $obj->hmac = $hash; return $obj; } /** * From tag * * @param string $tag Tag * @return static */ public static function fromTag( $tag ) { if ( preg_match( '/^\[\!AES128GCM\[(.+?)\]\[(.+?)\]\[(.+?)\]\]/', $tag, $matches ) ) { return static::fromCipher( $matches[1], hex2bin( $matches[2] ), hex2bin( $matches[3] ) ); } elseif ( preg_match( '/^\[\!AES128CBC\[(.+?)\]\]/', $tag, $matches ) ) { $cipher = base64_decode( $matches[1] ); $ivLength = openssl_cipher_iv_length('aes-128-cbc'); return static::fromCipher( substr( $cipher, $ivLength + 32 ), substr( $cipher, 0, $ivLength ), NULL, substr( $cipher, $ivLength, 32 ) ); } elseif ( preg_match( '/^\[\!AES\[(.+?)\]\]/', $tag, $matches ) ) { return static::fromCipher( $matches[1] ); } else { return static::fromPlaintext( $tag ); } } /** * Wrap in a tag to use later with fromTag * * @return string */ public function tag() { if ( $this->tag ) { return '[!AES128GCM[' . $this->cipher . '][' . bin2hex( $this->iv ) . '][' . bin2hex( $this->tag ) . ']]'; } elseif ( $this->hmac ) { return '[!AES128CBC[' . base64_encode( $this->iv . $this->hmac . $this->cipher ) . ']]'; } else { return '[!AES[' . $this->cipher . ']]'; } } /** * Decript * * @return string */ public function decrypt() { if ( $this->tag ) { return openssl_decrypt( $this->cipher, 'aes-128-gcm', static::key(), 0, $this->iv, $this->tag ); } elseif ( $this->hmac ) { $decrypted = openssl_decrypt( $this->cipher, 'aes-128-cbc', static::key(), OPENSSL_RAW_DATA, $this->iv ); if ( hash_equals( $this->hmac, hash_hmac( 'sha256', $this->cipher, static::key(), TRUE ) ) ) { return $decrypted; } return ''; } else { require_once '../../system/3rd_party/AES/AES.php'; return AesCtr::decrypt( $this->cipher, static::key(), 256 ); } } } /** * Function to write a log file to disk * * @param mixed $message Exception or message to log * @return void */ function writeLogFile( $message ) { /* What are we writing? */ $date = date('r'); if ( $message instanceof Exception) { $messageToLog = $date . "\n" . get_class( $message ) . '::' . $message->getCode() . "\n" . $message->getMessage() . "\n" . $message->getTraceAsString(); } else { if ( is_array( $message ) ) { $message = var_export( $message, TRUE ); } $messageToLog = $date . "\n" . $message . "\n" . ( new Exception)->getTraceAsString(); } /* Where are we writing it? */ $dir = rtrim( __DIR__, '/' ) . '/../../uploads/logs'; /* Write it */ $header = "\n\n"; $file = $dir . '/' . date( 'Y' ) . '_' . date( 'm' ) . '_' . date('d') . '_' . ( 'extractfailure' ) . '.php'; if ( file_exists( $file ) ) { @file_put_contents( $file, "\n\n-------------\n\n" . $messageToLog, FILE_APPEND ); } else { @file_put_contents( $file, $header . $messageToLog ); } @chmod( $file, IPS_FILE_PERMISSION ); } /* ! Controller */ try { $extractor = new Extractor( isset( $_GET['file'] ) ? $_GET['file'] : NULL, isset( $_GET['container'] ) ? $_GET['container'] : '' ); /* Check this request came from the ACP */ if ( !$extractor->securityCheck( $_GET['key'] ) ) { throw new Exception( "Security check failed" ); } /* Establish an FTP connection if necessary */ if ( $_GET['ftp'] ) { /* Get encrypted FTP credentials */ $query = $extractor->db->query( "SELECT conf_value FROM " . $extractor->dbPrefix . "core_sys_conf_settings WHERE conf_key='upgrade_ftp_details'" ); if( !$query ) { throw new RuntimeException('Cannot find (S)FTP credentials'); } $ftpCredentials = $query->fetch_assoc()['conf_value']; if( empty( $ftpCredentials ) ) { throw new RuntimeException('Cannot load (S)FTP credentials'); } if ( substr( $ftpCredentials, 0, 5 ) === '[!AES' ) { $decodedFtpDetails = Encrypt::fromTag( $ftpCredentials )->decrypt(); } else { $decodedFtpDetails = Encrypt::fromCipher( $ftpCredentials )->decrypt(); } $extractor->connectToFtp( json_decode( $decodedFtpDetails, TRUE ) ); } /* Extract a batch of files */ $i = isset( $_GET['i'] ) ? $_GET['i'] : 0; $done = $extractor->extract( $i ); /* If we didn't extract anything, are we done? */ if ( !$done and $i >= $extractor->numberOfFiles() ) { @unlink( $_GET['file'] ); if ( function_exists( 'opcache_reset' ) ) { @opcache_reset(); } echo <<
HTML; } /* Nope, redirect to the next batch */ else { $newI = $i + $extractor::NUMBER_PER_GO; $percentComplete = round( 100 / $extractor->numberOfFiles() * $newI, 2 ); $percentComplete = ( $percentComplete > 100 ) ? 100 : $percentComplete; $properties = array( 'file' => $_GET['file'], 'container' => $_GET['container'], 'key' => $_GET['key'], 'ftp' => $_GET['ftp'], 'i' => $newI, ); $url = "extract.php?" . http_build_query( $properties, '', '&' ); echo <<
HTML; } } catch ( Throwable $e ) { writeLogFile( $e ); echo ""; exit; } ?>