· 5 years ago · Mar 24, 2020, 09:34 PM
1<?php // -*- coding: utf-8 -*-
2
3define('PHPSHELL_VERSION', '2.5');
4/*
5
6 **************************************************************
7 * PHP Shell *
8 **************************************************************
9
10 PHP Shell is an interactive PHP script that will execute any command
11 entered. See the files README, INSTALL, and SECURITY or
12 http://phpshell.sourceforge.net/ for further information.
13
14 Copyright (C) 2000-2020 the Phpshell-team
15
16 This program is free software; you can redistribute it and/or
17 modify it under the terms of the GNU General Public License
18 as published by the Free Software Foundation; either version 2
19 of the License, or (at your option) any later version.
20
21 This program is distributed in the hope that it will be useful,
22 but WITHOUT ANY WARRANTY; without even the implied warranty of
23 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 GNU General Public License for more details.
25
26 You should have received a copy of the GNU General Public License
27 along with this program; if not, write to the Free Software
28 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
29
30
31*/
32
33/* There are no user-configurable settings in this file anymore, please see
34 * config.php instead. */
35
36
37/* This error handler will turn all notices, warnings, and errors into fatal
38 * errors, unless they have been suppressed with the @-operator. */
39function error_handler($errno, $errstr, $errfile, $errline, $errcontext)
40{
41 /* The @-operator (used with chdir() below) temporarely makes
42 * error_reporting() return zero, and we don't want to die in that case.
43 * That happens mostly in cases where we can just ignore it. */
44 if (error_reporting() != 0) {
45 die('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
46 "http://www.w3.org/TR/html4/strict.dtd">
47<html>
48<head>
49 <title>PHP Shell ' . PHPSHELL_VERSION . '</title>
50 <meta http-equiv="Content-Script-Type" content="text/javascript">
51 <meta http-equiv="Content-Style-Type" content="text/css">
52 <meta name="generator" content="phpshell">
53 <meta name="robots" content="noindex, follow">
54 <link rel="shortcut icon" type="image/x-icon" href="phpshell.ico">
55 <link rel="stylesheet" href="style.css" type="text/css">
56</head>
57<body>
58 <h1>Fatal Error!</h1>
59 <p><b>' . $errstr . '</b></p>
60 <p>in <b>' . $errfile . '</b>, line <b>' . $errline . '</b>.</p>
61
62 <form name="shell" enctype="multipart/form-data" action="" method="post"><p>
63 If you want to try to reset your session:
64 <input type="submit" name="logout" value="Logout" style="display: inline;">
65 </p></form>
66 <hr>
67
68 <p>Please consult the <a href="README" rel="nofollow">README</a>, <a
69 href="INSTALL" rel="nofollow">INSTALL</a>, and <a href="SECURITY" rel="nofollow">SECURITY</a> files for
70 instruction on how to use PHP Shell.</p>
71
72 <hr>
73
74 <address>
75 Copyright © 2000–2020, the Phpshell-team. Get the latest
76 version at <a
77 href="http://phpshell.sourceforge.net/">http://phpshell.sourceforge.net/</a>.
78 </address>
79
80</body>
81</html>');
82 }
83}
84
85/* Installing our error handler makes PHP die on even the slightest problem.
86 * This is what we want in a security critical application like this. */
87set_error_handler('error_handler');
88
89
90/* Clear screen */
91function builtin_clear($arg) {
92 $_SESSION['output'] = '';
93}
94
95function stripslashes_deep($value) {
96 if (is_array($value)) {
97 return array_map('stripslashes_deep', $value);
98 } else {
99 return stripslashes($value);
100 }
101}
102
103
104function htmlescape($value) {
105 return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE);
106}
107
108
109/* even though proc_open has a $cwd argument, we don't use it because php 4
110 * doesn't support it. */
111function add_dir($cmd, $dir){
112 return "cd ".escapeshellarg($dir)."\n".$cmd;
113}
114
115/* executes a command in the given working directory and returns output */
116function exec_cwd($cmd, $directory) {
117 list($status, $stdout, $stderr) = exec_command($cmd, $directory);
118 return $stdout;
119}
120
121
122
123/*
124 * Where the real magic happens
125 *
126 * $mergeoutputs says if the command's stdout and stderr should be separated
127 * or merged into a single string.
128 * $fd9 adds an extra pipe on file descriptor 9 to the process, used for out
129 * of band communication.
130 * The return value is an array containing array(status, stdout[, stderr][, fd9])
131 * with the last two possibly being omitted.
132 */
133function exec_command($cmd, $dir, $mergeoutput=false, $fd9=false) {
134
135 $io = array();
136 $pipes = array(1 => array('pipe', 'w'), 2 => array('pipe', 'w'));
137 if ($fd9) $pipes[9] = array('pipe', 'w');
138 $p = proc_open(add_dir($cmd, $dir), $pipes, $io);
139
140 /*
141 * Read output using stream_select. Reading the pipes sequentially could
142 * potentially cause a deadlock if the subshell would write a large
143 * amount of data to pipe 2 (stderr), while we are reading pipe 1. The
144 * subshell would then block waiting for us to read pipe 2, and we would
145 * block waiting for the subshell to write to pipe 1, resulting in a
146 * deadlock.
147 */
148
149 // set all streams to nonblocking mode, so we can read them all at once
150 // below
151 foreach ($io as $pipe) {
152 stream_set_blocking($pipe, 0);
153 }
154
155 $out = $err = $out9 = '';
156
157 while (true) {
158 // we need to recreate $read each time, because it gets modified in
159 // stream_select. Also, we just want to select on those pipes that are
160 // not closed yet.
161 $read = array();
162 foreach ($io as $pipe) {
163 if (!feof($pipe))
164 $read[] = $pipe;
165 }
166
167 // break out if nothing more to read
168 if (count($read) == 0)
169 break;
170
171 // define these because we must pass something by reference
172 $write = null;
173 $except = null;
174
175 // wait for the subshell to write to any of the pipes
176 stream_select($read, $write, $except, 10000);
177
178 // and read them. We don't bother to see which one is ready, we just
179 // try them all. That's why we put them in nonblocking mode.
180 $out .= fgets($io[1]);
181 if ($mergeoutput) {
182 $out .= fgets($io[2]);
183 } else {
184 $err .= fgets($io[2]);
185 }
186 if ($fd9) {
187 $out9 .= fgets($io[9]);
188 }
189 }
190
191 fclose($io[1]);
192 fclose($io[2]);
193 if ($fd9) fclose($io[9]);
194 $status = proc_close($p);
195 $ret = array($status, $out);
196 if (!$mergeoutput) $ret[] = $err;
197 if ($fd9) $ret[] = $out9;
198 return $ret;
199}
200
201function setdefault(&$var, $options) {
202 foreach ($options as $opt) {
203 if ($opt != '') {
204 $var = $opt;
205 return;
206 }
207 }
208}
209
210function reset_csrf_token() {
211 $_SESSION['csrf_token'] = base64_encode(random_bytes(16));
212}
213
214// Re-generate a session id. Only has effect if session_start is called lateron.
215function reset_session_id() {
216 $newid = bin2hex(random_bytes(16));
217 return session_id($newid);
218}
219
220// Generate a new session id for a running session.
221function reset_session() {
222 session_destroy();
223 reset_session_id();
224 session_start();
225}
226
227
228function runcommand($cmd) {
229 global $rows, $columns, $ini;
230
231 $extra_env =
232 "export ROWS=$rows\n".
233 "export COLUMNS=$columns\n".
234 "export HOME=\"" . realpath($ini['settings']['home-directory']) . "\"\n";
235
236 $aliases = '';
237 foreach ($ini['aliases'] as $al => $expansion) {
238 $aliases .= "alias $al=".escapeshellarg($expansion)."\n";
239 }
240
241 $command =
242 $extra_env.
243 $aliases.
244 $cmd." \n". # extra space in case the command ends in \
245 "pwd >&9\n";
246
247 list($status, $out, $newcwd) = exec_command($command, $_SESSION['cwd'], true, true);
248
249 // trim because 'pwd' adds a newline
250 if (strlen($newcwd) > 0 && $newcwd{0} == '/')
251 $_SESSION['cwd'] = trim($newcwd);
252
253 $_SESSION['output'] .= htmlescape($out);
254}
255
256
257function builtin_download($arg) {
258 /* download specified file */
259
260 if ($arg == '') {
261 $_SESSION['output'] .= "Syntax: download filename\n(you forgot filename)\n";
262 return;
263 }
264
265 if ($arg[0] != '/') {
266 $downloadfn = $_SESSION['cwd'] . '/' . $arg ;
267 } else {
268 $downloadfn = $arg ;
269 }
270
271 /* test if file exists */
272 clearstatcache();
273 if (!file_exists($downloadfn)) {
274 $_SESSION['output'] .= "download: file not found: '$arg'\n";
275 return;
276 }
277
278 if (!is_readable($downloadfn)) {
279 $_SESSION['output'] .= "download: Permission denied for file '$arg'\n";
280 return;
281 }
282
283 /* Passing a filename correctly in a content disposition header is nigh
284 * impossible. If the filename is unsafe, we just pass nothing and let the
285 * user choose himself.
286 * The 'rules' are at http://tools.ietf.org/html/rfc6266#appendix-D
287 * If problematic characters are encountered we use the filename*= form,
288 * user agents that don't support that don't get a filename hint.
289 */
290 $basename = basename($arg);
291 // match non-ascii, non printable, and '%', '\', '"'.
292 if (preg_match('/[\x00-\x1F\x80-\xFF\x7F%\\\\"]/', $basename)) {
293 // Assume UTF-8 on the file system, since there's no way to check
294 $filename_hdr = "filename*=UTF-8''".rawurlencode($basename).';';
295 } else {
296 $filename_hdr = 'filename="'.$basename.'";';
297 }
298
299 header('Content-Description: File Transfer');
300 header('Content-Type: application/octet-stream');
301 header('Content-Disposition: attachment; '.$filename_hdr);
302 header('Content-Transfer-Encoding: binary');
303 header('Expires: 0');
304 header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0');
305 if (filesize($downloadfn)) {
306 header('Content-Length: '.filesize($downloadfn));
307 }
308 ob_clean();
309 flush();
310 readfile($downloadfn);
311 exit();
312}
313
314/* This is a tiny editor which you can start calling 'editor file'*/
315function builtin_editor($arg) {
316 global $editorcontent, $filetoedit, $showeditor, $writeaccesswarning;
317
318 if ($arg == '') {
319 $_SESSION['output'] .= " Syntax: editor filename\n (you forgot the filename)\n";
320 return;
321 }
322
323 $filetoedit = $arg;
324
325 clearstatcache();
326 if (!file_exists($_SESSION['cwd'] . '/' . $arg)) {
327 // file does not exist
328 $editorcontent = '';
329 $showeditor = true;
330
331 // test current directory for write access
332 if (!is_writeable($_SESSION['cwd'])) {
333 $writeaccesswarning = true;
334 }
335
336 } else {
337
338 if (!is_file($_SESSION['cwd'] . '/' . $arg)) {
339 $_SESSION['output'] .= "editor: file '$arg' not found or not a regular file\n";
340 return;
341 }
342
343 if (!is_readable($_SESSION['cwd'] . '/' . $arg)) {
344 $_SESSION['output'] .= "editor: Permission denied for file '$arg'\n";
345 return;
346 }
347
348 if (!is_writeable($_SESSION['cwd'] . '/' . $arg)) {
349 $writeaccesswarning = true;
350 }
351
352 // file_get_contents() would be possible instead of implode('', file(...)),
353 // but that should work with php < 4.3
354 $editorcontent = implode('', file($_SESSION['cwd'] . '/' . $arg));
355 $showeditor = true;
356 }
357
358 return;
359}
360
361function builtin_logout($arg = null) {
362
363 reset_session();
364
365 /* Empty the session data, except for the 'authenticated' entry which the
366 * rest of the code needs to be able to check. */
367 $_SESSION = array('authenticated' => false);
368
369 /* Reset the csrf token, as otherwise the login form won't render */
370 reset_csrf_token();
371}
372
373function builtin_history($arg) {
374 /* history command (without parameter) - output the command history */
375 if (trim($arg) == '') {
376 $i = 1;
377 foreach ($_SESSION['history'] as $histline) {
378 $_SESSION['output'] .= htmlescape(sprintf("%5d %s\n", $i, $histline));
379 $i++;
380 }
381 /* history command (with parameter "-c") - clear the command history */
382 } elseif (preg_match('/^[[:blank:]]*-c[[:blank:]]*$/', $arg)) {
383 $_SESSION['history'] = array() ;
384 }
385}
386
387
388/*
389 * To be as safe as possible against brute-force password guessing attempts and
390 * against DOS attacks that try to exploit the expensive password checking of
391 * blowfish, we read and parse the ratelimit file twice. First to see if we
392 * should attempt to authenticate at all or if there's still a timeout in force,
393 * second to clear or increase the current user's failed login attempts. Keeping
394 * the file opened and locked during the password verification would provide an
395 * attack vector to DOS attacks. When recording the result of that verification
396 * we need to parse the file again in case there have been any updates
397 * inbetween. However, the file is simple to parse so the parsing step is
398 * probably much faster than the password verification.
399 *
400 * PHP Shell assumes file locking will work. It won't work if the file is stored
401 * on a FAT volume, or if php is running in multithreaded (instead of
402 * multiprocess) mode. Both are unlikely as FAT is quite outdated, and many PHP
403 * extensions are not thread-safe so PHP hosting providers usually don't run PHP
404 * in multithreaded mode.
405 */
406class RateLimit {
407
408 var $filename;
409 var $intemp;
410
411 function RateLimit() {
412 global $ini;
413 if (strlen(trim($ini['settings']['rate-limit-file']))) {
414 $this->filename = $ini['settings']['rate-limit-file'];
415 $this->intemp = false;
416 } else {
417 $tempdir = function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : '';
418 if (!@is_dir($tempdir)) {
419 $tempdir = (string) getenv('TMPDIR');
420 }
421 if (!@is_dir($tempdir)) {
422 $tempdir = '/tmp';
423 }
424 // the md5 is not for security, just obfuscation
425 $this->filename = $tempdir.'/floodcontrol_'.md5('PHP Shell '.$_SERVER['SERVER_NAME']);
426 $this->intemp = true;
427 }
428 }
429
430 function parse_file($str) {
431 $parsed = array();
432 foreach (explode("\n", $str) as $line) {
433 $a = explode(' ', rtrim($line));
434 if (count($a) < 3) {
435 continue;
436 }
437 list($ip, $count, $timestamp) = $a;
438 $parsed[$ip] = array('count' => $count, 'timestamp' => $timestamp);
439 }
440 return $parsed;
441 }
442
443 function serialize_table($table) {
444 $a = array();
445 foreach ($table as $ip => $row) {
446 $a[] = "$ip {$row['count']} {$row['timestamp']}\n";
447 }
448 return implode('', $a);
449 }
450
451 function gc_table($table) {
452 // remove entries older than a week
453 $limit = time() - 60 * 60 * 24 * 7;
454 foreach (array_keys($table) as $ip) {
455 if ($table[$ip]['timestamp'] < $limit) {
456 unset($table[$ip]);
457 }
458 }
459 return $table;
460 }
461
462
463 function readfile($fh) {
464 $contents = '';
465 while (!feof($fh)) {
466 $contents .= fread($fh, 8192);
467 }
468 return $contents;
469 }
470
471 function check_linked($fh, $name) {
472 clearstatcache();
473 $fh_stat = fstat($fh);
474 $name_stat = @stat($name);
475 return !is_null($name_stat) &&
476 $fh_stat['dev'] === $name_stat['dev'] &&
477 $fh_stat['ino'] === $name_stat['ino'];
478 }
479
480 // returns an array with the current timeout and the next timeout if the
481 // user needs to try again.
482 function get_timeout() {
483 if (!file_exists($this->filename)) {
484 return 0;
485 }
486 $fh = fopen($this->filename, 'r');
487 flock($fh, LOCK_SH);
488 $linked = $this->check_linked($fh, $this->filename);
489 $contents = $this->readfile($fh);
490 flock($fh, LOCK_UN);
491 fclose($fh);
492 $table = $this->parse_file($contents);
493 if (!$linked) {
494 return $this->get_timeout();
495 }
496
497 if (!isset($table[$_SERVER['REMOTE_ADDR']])) {
498 return 0;
499 } else {
500 $record = $table[$_SERVER['REMOTE_ADDR']];
501 // start counting only on the third failed try
502 $timeout = (int) pow(2, $record['count']-2);
503 $waited = time() - $record['timestamp'];
504 $nexttimeout = (int) pow(2, $record['count']-1);
505 return array(max(0, $timeout - $waited), $nexttimeout);
506 }
507 }
508
509 // register a failed login of the current user
510 function register_user() {
511 $fh = fopen($this->filename, 'a+');
512 if ($this->intemp) {chmod($this->filename, 0640);}
513 flock($fh, LOCK_EX);
514 $linked = $this->check_linked($fh, $this->filename);
515 $table = $this->gc_table($this->parse_file($this->readfile($fh)));
516 $ip = $_SERVER['REMOTE_ADDR'];
517 $table[$ip] = array('count' => @$table[$ip]['count']+1, 'timestamp' => time());
518 ftruncate($fh, 0);
519 rewind($fh);
520 fwrite($fh, $this->serialize_table($table));
521 fflush($fh);
522 flock($fh, LOCK_UN);
523 fclose($fh);
524 if (!$linked) {
525 return $this->register_user();
526 }
527 }
528
529 // a successful login, clear failed login attempts for user
530 function clear_user() {
531 if (!file_exists($this->filename)) {
532 return;
533 }
534 $fh = fopen($this->filename, 'a+');
535 if ($this->intemp) {chmod($this->filename, 0640);}
536 flock($fh, LOCK_EX);
537 $linked = $this->check_linked($fh, $this->filename);
538 $table = $this->gc_table($this->parse_file($this->readfile($fh)));
539 unset($table[$_SERVER['REMOTE_ADDR']]);
540 ftruncate($fh, 0);
541 rewind($fh);
542 fwrite($fh, $this->serialize_table($table));
543 fflush($fh);
544 if ($linked && $this->intemp && count($table) == 0) {
545 @unlink($this->filename);
546 }
547 flock($fh, LOCK_UN);
548 fclose($fh);
549 if (!$linked) {
550 return $this->clear_user();
551 }
552 }
553}
554
555// attempt to authenticate but prevent brute forcing
556function try_authenticate($username, $password) {
557 global $ini, $warning;
558 $nextwait = 0;
559 if ($ini['settings']['enable-rate-limiting']) {
560 $rl = new RateLimit();
561 list ($wait, $nextwait) = $rl->get_timeout();
562 if ($wait) {
563 $minutes = floor($wait / 60);
564 $waittxt = $minutes ? $minutes.' minutes and ' : '';
565 $waittxt .= ($wait % 60).' seconds';
566 $warning .= "<p class='warning'><b>Error:</b> Too many failed login attempts,
567 please wait <span id='waitcount'>$waittxt</span> more before
568 re-trying to log in.</p><script type='text/javascript'>startcountdown($wait, 'waitcount');
569 </script>\n";
570 return false;
571 }
572 $authenticated = authenticate($username, $password);
573 if ($authenticated) {
574 $rl->clear_user();
575 } else {
576 $rl->register_user();
577 }
578 } else {
579 $authenticated = authenticate($username, $password);
580 }
581 if (!$authenticated && $nextwait >= 2) {
582 $minutes = floor($nextwait / 60);
583 $waittxt = $minutes ? $minutes.' minutes and ' : '';
584 $waittxt .= ($nextwait % 60).' seconds';
585 $warning .= "<p class=\"error\">Login failed, please try again in
586 <span id='waitcount'>$waittxt</span>:</p>
587 <script type='text/javascript'>startcountdown($nextwait, 'waitcount')</script>\n";
588 } elseif (!$authenticated) {
589 $warning .= "<p class=\"error\">Login failed, please try again:</p>\n";
590 }
591 return $authenticated;
592}
593
594// returns true if authentication was successful, false if not
595function authenticate($username, $password) {
596 global $ini, $warning;
597
598 if (!isset($ini['users'][$username])) {
599 return false;
600 }
601 $ini_passwordhash = $ini['users'][$username];
602 return password_verify($password, $ini_passwordhash);
603}
604
605
606
607/* the builtins this shell recognizes */
608$builtins = array(
609 'download' => 'builtin_download',
610 'editor' => 'builtin_editor',
611 'exit' => 'builtin_logout',
612 'logout' => 'builtin_logout',
613 'history' => 'builtin_history',
614 'clear' => 'builtin_clear');
615
616
617
618/** initialize everything **/
619
620/** Load the configuration. **/
621$ini = parse_ini_file('config.php', true);
622
623if (empty($ini['settings'])) {
624 $ini['settings'] = array();
625}
626
627/* Default settings --- these settings should always be set to something. */
628$default_settings = array(
629 'home-directory' => '.',
630 'safe-mode-warning' => true,
631 'file-upload' => false,
632 'PS1' => '$ ',
633 'bind-user-IP' => true,
634 'timeout' => 180,
635 'enable-rate-limiting' => true,
636 'rate-limit-file' => '');
637// Controls if we are in editor mode
638$showeditor = false;
639// Show warning if we're editing a file we can't write to
640$writeaccesswarning = false;
641// Did we try to authenticate the users password during this request?
642$passwordchecked = false;
643// Append any html to this string for warning/error messages
644$warning = '';
645
646/* Merge settings. */
647$ini['settings'] = array_merge($default_settings, $ini['settings']);
648
649
650/** initialize session **/
651
652$newsession = !isset($_COOKIE[session_name()]);
653$https = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off');
654$expiredsession = false;
655
656ini_set('session.use_only_cookies', '1');
657
658if (version_compare(PHP_VERSION, '5.2.0', '>=')) {
659 session_set_cookie_params(0, // cookie lifetime until browser closes
660 $_SERVER['REQUEST_URI'], // bind cookie to this specific URI
661 null, // use default domain (www.site.com)
662 $https, // If called over HTTPS, lock cookie to that
663 true // httponly, available since PHP 5.2
664 );
665} else {
666 // same as above, but without 'httponly'
667 session_set_cookie_params(0, $_SERVER['REQUEST_URI'], null, $https);
668}
669
670if ($newsession) {
671 reset_session_id();
672}
673
674session_start();
675if (!$newsession && $_SESSION == array()) {
676 $expiredsession = true;
677 // Either the session expired, or the client invented its own session cookie.
678 // Don't allow the client to choose their own session ID.
679 reset_session();
680}
681
682
683if (!isset($_SESSION['csrf_token'])) {
684 reset_csrf_token();
685}
686
687/* done initialising session */
688
689/** get POST variables **/
690
691if (get_magic_quotes_gpc()) {
692 $_POST = stripslashes_deep($_POST);
693}
694
695/* Initialize some variables we need */
696setdefault($_SESSION['env']['rows'], array(@$_POST['rows'], @$_SESSION['env']['rows'], 24));
697setdefault($_SESSION['env']['columns'], array(@$_POST['columns'], @$_SESSION['env']['columns'], 80));
698
699if (!preg_match('/^[[:digit:]]+$/', $_SESSION['env']['rows'])) {
700 $_SESSION['env']['rows']=24 ;
701}
702if (!preg_match('/^[[:digit:]]+$/', $_SESSION['env']['columns'])) {
703 $_SESSION['env']['columns']=80 ;
704}
705$rows = $_SESSION['env']['rows'];
706$columns = $_SESSION['env']['columns'];
707
708
709/* initialisation completed, start processing */
710
711
712header("Content-Type: text/html; charset=utf-8");
713
714
715/* Delete the session data if the user requested a logout.
716 * Logging out is allowed without the CSRF token or other security checks, so
717 * someone can still logout if there's an error in the rest of the code.
718 * This also means that an attacker using CSRF can force someone to logout, but
719 * that is not an important security problem. */
720if (isset($_POST['logout'])) {
721 builtin_logout('');
722// Check CSRF token
723} elseif ($_SERVER['REQUEST_METHOD'] == 'POST' && @$_POST['csrf_token'] != $_SESSION['csrf_token']) {
724 // Whoops, a possible cross-site request forgery attack!
725 // But possibly it's just that the session expired.
726 if ($expiredsession) {
727 $warning .= "<p class='error'>Session timed out</p>\n";
728 } else {
729 $warning .= "<p class='error'>Error: CSRF token failure</p>\n";
730 }
731 // Clear any POST commands, treat this request like a GET.
732 $_POST = array();
733}
734// Enforce session security settings
735if (!isset($_SESSION['authenticated'])) {
736 $_SESSION['authenticated'] = false;
737}
738if (!$newsession && $_SESSION['authenticated']) {
739 if ($ini['settings']['bind-user-IP'] && $_SESSION['user-IP'] != $_SERVER['REMOTE_ADDR']) {
740 $_SESSION['authenticated'] = false;
741 }
742 if ($ini['settings']['timeout'] != 0 &&
743 (time() - $_SESSION['login-timestamp']) / 60 > $ini['settings']['timeout']) {
744 $_SESSION['authenticated'] = false;
745 }
746}
747
748/* set some variables we need a lot */
749$username = isset($_POST['username']) ? $_POST['username'] : '';
750$password = isset($_POST['password']) ? $_POST['password'] : '';
751$command = isset($_POST['command']) ? $_POST['command'] : '';
752
753/* Attempt authentication. */
754if (isset($_SESSION['nonce']) && isset($_POST['nonce']) &&
755 $_POST['nonce'] == $_SESSION['nonce'] && isset($_POST['login'])) {
756 unset($_SESSION['nonce']);
757 $passwordchecked = true;
758
759 $_SESSION['authenticated'] = try_authenticate($username, $password);
760 if ($passwordchecked && $_SESSION['authenticated']) {
761 // For security purposes, reset the session ID if we just logged in.
762 // Preserve session parameters, re-login may be caused by e.g. a timeout.
763 $session = $_SESSION;
764 reset_session();
765 $_SESSION = $session;
766 unset($session);
767 reset_csrf_token();
768 $_SESSION['login-timestamp'] = time();
769 $_SESSION['user-IP'] = $_SERVER['REMOTE_ADDR'];
770 }
771}
772
773/* process user commands */
774if ($_SESSION['authenticated']) {
775 /* Clear screen if submitted */
776 if (isset($_POST['clear'])) {
777 builtin_clear('');
778 }
779
780 /* Initialize the session variables. */
781 if (empty($_SESSION['cwd'])) {
782 $_SESSION['cwd'] = realpath($ini['settings']['home-directory']);
783 $_SESSION['history'] = array();
784 $_SESSION['output'] = '';
785 }
786
787 /* Clicked on one of the subdirectory links - ignore the command */
788 if (isset($_POST['levelup'])) {
789 $levelup = $_POST['levelup'] ;
790 while ($levelup > 0) {
791 $command = '' ; /* ignore the command */
792 $_SESSION['cwd'] = dirname($_SESSION['cwd']);
793 $levelup -- ;
794 }
795 }
796 /* Selected a new subdirectory as working directory - ignore the command */
797 if (isset($_POST['changedirectory'])) {
798 $changedir= $_POST['changedirectory'];
799 if (strlen($changedir) > 0) {
800 if (@chdir($_SESSION['cwd'] . '/' . $changedir)) {
801 $command = '' ; /* ignore the command */
802 $_SESSION['cwd'] = realpath($_SESSION['cwd'] . '/' . $changedir);
803 }
804 }
805 }
806
807 /* handle file uploads */
808 $uploaderror = 0;
809 if (isset($_FILES['uploadfile']['tmp_name'])) {
810 if (is_uploaded_file($_FILES['uploadfile']['tmp_name'])) {
811 if (!move_uploaded_file($_FILES['uploadfile']['tmp_name'], $_SESSION['cwd'] . '/' . $_FILES['uploadfile']['name'])) {
812 $warning .= "<p class='warning'>Error: cannot move {$_FILES['uploadfile']['name']}</p>\n" ;
813 }
814 } elseif (isset($_FILES['uploadfile']['error']) && $_FILES['uploadfile']['error'] != 0) {
815 $uploaderror = $_FILES['uploadfile']['error'];
816 }
817 }
818
819 /* Save content from 'editor' */
820 if (isset($_POST['savefile']) && isset($_POST["filetoedit"]) && $_POST["filetoedit"] != "") {
821 $io = array();
822 $p = proc_open(add_dir('cat >'.escapeshellarg($_POST['filetoedit']), $_SESSION['cwd']),
823 array(0 => array('pipe', 'r'), 2 => array('pipe', 'w')), $io);
824
825 /*
826 * I'm not entirely sure this approach will not deadlock, but I think
827 * it is ok. If the subshell fails it will exit and our write
828 * fails. There is one assumption though: the subshell will not block
829 * on writing to it's stderr while we are not reading it. As long as
830 * the error message is small enough to fit in the kernel buffer, as
831 * is expected with sh/cat redirect errors, this is no problem, but if
832 * that assumption does not hold we will have to do some uglier tricks
833 * using stream_select and friends.
834 *
835 * IMPORTANT ASSUMPTION: THE ERROR MESSAGE IS SMALL ENOUGH TO FIT IN
836 * THE KERNELS BUFFER OF THE SUBSHELLS STDOUT.
837 */
838
839 /* The docs are not entirely clear whether fwrite can write only part
840 * of the string to a pipe, but testing shows that php internally
841 * splits up large writes into smaller ones, so normally everything
842 * gets written. */
843 $content = str_replace("\r\n", "\n", $_POST["filecontent"]);
844 $status = fwrite($io[0], $content);
845 /* We can't really rely on the number of bytes written if
846 $status<strlen($_POST['filecontent']), because the pipe has a kernel
847 buffer of a few kilobytes. So we don't show the number of actually
848 written bytes in the error message, just that something went wrong. */
849 if ($status === false or $status < strlen($content)) {
850 $_SESSION['output'] .= "editor: Error saving editor content to ".htmlescape($_POST['filetoedit'])."\n";
851 }
852 // close immediately to let the shell know we are done.
853 fclose($io[0]);
854 // also read any error messages
855 $errmsg = '';
856 while (!feof($io[2])) {
857 $errmsg .= fread($io[2], 8192);
858 }
859 if (trim($errmsg) != '') {
860 $_SESSION['output'] .= htmlescape('editor: '.$errmsg);
861 }
862 fclose($io[2]);
863 $status = proc_close($p);
864 if ($status != 0) {
865 $_SESSION['output'] .= "editor: Error: subprocess exited with status $status.\n";
866 }
867 }
868
869 /* execute the command */
870 if (trim($command) != '') {
871 /* Save the command for later use in the JavaScript. If the command is
872 * already in the history, then the old entry is removed before the
873 * new entry is put into the list at the front. */
874 if (($i = array_search($command, $_SESSION['history'])) !== false) {
875 unset($_SESSION['history'][$i]);
876 }
877
878 array_unshift($_SESSION['history'], $command);
879
880 /* Now append the command to the output. */
881 $_SESSION['output'] .= htmlescape($ini['settings']['PS1'] . $command) . "\n";
882
883 // append a space to $command to guarantee the last capture group
884 // matches. It's removed afterward.
885 preg_match('/^[[:blank:]]*([^[:blank:]]+)([[:blank:]].*)$/', $command.' ', $regs);
886 $cmd_name = $regs[1];
887 $arg = trim($regs[2]);
888 if (strlen($arg) > 1 && $arg{0} === substr($arg, -1) && ($arg{0} == '"' || $arg{0} == "'")) {
889 $arg = substr($arg, 1, -1);
890 }
891
892 if (array_key_exists($cmd_name, $builtins)) {
893 $builtins[$cmd_name]($arg);
894 } else {
895 /* The command is not an internal command, so we execute it and
896 * save the output. We use the full input, not the one parsed with
897 * the regex above, and let the shell parse it. */
898 runcommand($command);
899 }
900 }
901
902 /* Build the command history for use in the JavaScript */
903 if (empty($_SESSION['history'])) {
904 $js_command_hist = '""';
905 } else {
906 $escaped = array_map('addslashes', $_SESSION['history']);
907 $js_command_hist = '"", "' . implode('", "', $escaped) . '"';
908 }
909}
910
911
912?>
913<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
914 "http://www.w3.org/TR/html4/strict.dtd">
915<html>
916<head>
917 <title>PHP Shell <?php echo PHPSHELL_VERSION ?></title>
918 <meta http-equiv="Content-Script-Type" content="text/javascript">
919 <meta http-equiv="Content-Style-Type" content="text/css">
920 <meta name="generator" content="phpshell">
921 <meta name="robots" content="noindex, follow">
922 <link rel="shortcut icon" type="image/x-icon" href="phpshell.ico">
923 <link rel="stylesheet" href="style.css" type="text/css">
924
925 <script type="text/javascript">
926 <?php if ($_SESSION['authenticated'] && ! $showeditor) { ?>
927
928 var current_line = 0;
929 var command_hist = new Array(<?php echo $js_command_hist ?>);
930 var last = 0;
931
932 function key(e) {
933 if (!e) var e = window.event;
934
935 if (e.keyCode == 38 && current_line < command_hist.length-1) {
936 command_hist[current_line] = document.shell.command.value;
937 current_line++;
938 document.shell.command.value = command_hist[current_line];
939 }
940
941 if (e.keyCode == 40 && current_line > 0) {
942 command_hist[current_line] = document.shell.command.value;
943 current_line--;
944 document.shell.command.value = command_hist[current_line];
945 }
946
947 }
948
949 function init() {
950 document.shell.setAttribute("autocomplete", "off");
951 document.getElementById('output').scrollTop = document.getElementById('output').scrollHeight;
952 document.shell.command.focus();
953 }
954
955 <?php } elseif ($_SESSION['authenticated'] && $showeditor) { ?>
956
957 function init() {
958 document.shell.filecontent.focus();
959 }
960
961 <?php } else { /* if not authenticated */ ?>
962
963 function init() {
964 document.shell.username.focus();
965 }
966
967 <?php } ?>
968 function levelup(d) {
969 document.shell.levelup.value=d ;
970 document.shell.submit() ;
971 }
972 function changesubdir(d) {
973 document.shell.changedirectory.value=document.shell.dirselected.value ;
974 document.shell.submit() ;
975 }
976
977 function startcountdown(seconds, target) {
978 var targetnode = document.getElementById(target);
979 var timerId = setInterval(function(){
980 var minutes = Math.floor(seconds / 60);
981 var text = (seconds % 60)+' seconds';
982 if (minutes > 0) {
983 text = minutes+' minutes and '+text;
984 }
985 targetnode.innerHTML = text;
986 if(seconds === 0){
987 clearInterval(timerId);
988 }
989 seconds--;
990 }, 1000 );
991 }
992
993 </script>
994</head>
995
996<body onload="init()">
997
998<h1>PHP Shell <?php echo PHPSHELL_VERSION ?></h1>
999
1000<form name="shell" enctype="multipart/form-data" action="" method="post">
1001<div><input name="csrf_token" type="hidden" value="<?php echo $_SESSION['csrf_token'];?>">
1002<input name="levelup" id="levelup" type="hidden">
1003<input name="changedirectory" id="changedirectory" type="hidden"></div>
1004
1005<?php
1006if (!$_SESSION['authenticated']) {
1007 /* Generate a new nonce every time we present the login page. This binds
1008 * each login to a unique hit on the server and prevents the simple replay
1009 * attack where one uses the back button in the browser to replay the POST
1010 * data from a login. */
1011 $_SESSION['nonce'] = base64_encode(random_bytes(16));
1012
1013if ($ini['settings']['safe-mode-warning'] && ini_get('safe_mode')) { ?>
1014
1015<div class="warning">
1016<b>Warning:</b> <a href="https://secure.php.net/features.safe-mode">Safe Mode</a> is enabled. PHP Shell will probably not work correctly.
1017See the <a href="SECURITY">SECURITY</a> file for some background information about Safe Mode and its effects on PHP Shell.
1018</div>
1019
1020<?php } /* Safe mode. */ ?>
1021
1022<fieldset>
1023 <legend>Authentication</legend>
1024
1025 <?php
1026 if (!$https) {
1027 echo "<p class='warning'><b>Security warning:</b>
1028 You are using an unencrypted connection, this is not recommended.
1029 Everything (your username, password and all your commands and results)
1030 will be sent unencrypted in cleartext across the internet. Try using
1031 <a href='https://".htmlescape($_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'])."'>PHP Shell over HTTPS</a>,
1032 or if that does not work, try contacting your system administrator or
1033 hosting provider on how to set up HTTPS support.</p>\n";
1034 }
1035 echo $warning;
1036 if (empty($ini['users'])) {
1037 echo "<p class='warning'><b>Warning:</b>
1038 No user accounts are defined. You must <a href=\"pwhash.php\">configure
1039 some user accounts</a> before using PHP shell.</p>\n";
1040 } else {
1041 if (!$passwordchecked) {
1042 echo " <p>Please login:</p>\n";
1043 }
1044
1045 ?>
1046 <label for="username">Username:</label>
1047 <input name="username" id="username" type="text" value="<?php echo $username ?>"><br>
1048 <label for="password">Password:</label>
1049 <input name="password" id="password" type="password">
1050 <p><input type="submit" name="login" value="Login"></p>
1051 <input name="nonce" type="hidden" value="<?php echo $_SESSION['nonce']; ?>">
1052 <?php } ?>
1053</fieldset>
1054</form>
1055
1056<?php } else { /* Authenticated. */ ?>
1057<fieldset>
1058 <legend style="background-color: transparent"><?php echo "Phpshell running on: " . $_SERVER['SERVER_NAME']; ?></legend>
1059<?php
1060 echo $warning;
1061?>
1062<p>Current Working Directory:
1063<span class="pwd"><?php
1064 if ( $showeditor ) {
1065 echo htmlescape($_SESSION['cwd']) . '</span>';
1066 } else { /* normal mode - offer navigation via hyperlinks */
1067 $parts = explode('/', $_SESSION['cwd']);
1068
1069 for ($i=1; $i<count($parts); $i=$i+1) {
1070 echo '<a class="pwd" title="Change to this directory. Your command will not be executed." href="javascript:levelup(' . (count($parts)-$i) . ')">/</a>' ;
1071 echo htmlescape($parts[$i]);
1072 }
1073 echo '</span>';
1074 if (is_readable($_SESSION['cwd'])) { /* is the current directory readable? */
1075 /* Now we make a list of the directories. */
1076 $dir_handle = opendir($_SESSION['cwd']);
1077 /* We store the output so that we can sort it later: */
1078 $options = array();
1079 /* Run through all the files and directories to find the dirs. */
1080 while ($dir = @readdir($dir_handle)) {
1081 if (($dir != '.') and ($dir != '..') and @is_dir($_SESSION['cwd'] . "/" . $dir)) {
1082 $options[$dir] = "<option value=\"/$dir\">$dir</option>";
1083 }
1084 }
1085 closedir($dir_handle);
1086 if (count($options)>0) {
1087 ksort($options);
1088 echo '<br><a href="javascript:changesubdir()">Change to subdirectory</a>: <select name="dirselected">';
1089 echo implode("\n", $options);
1090 echo '</select>';
1091 }
1092 } else {
1093 echo "<br/><b>Notice:</b> current directory not readable.";
1094 }
1095 }
1096?>
1097<br>
1098
1099 <?php if (! $showeditor) { /* Outputs the 'terminal' without the editor */ ?>
1100
1101<div class="terminal">
1102<pre id="output" style="height: <?php echo $rows*2 ?>ex; overflow-y: scroll;">
1103<?php
1104 $lines = substr_count($_SESSION['output'], "\n");
1105 $padding = str_repeat("\n", max(0, $rows - $lines));
1106 echo rtrim($padding . wordwrap($_SESSION['output'], $columns, "\n", true));
1107?>
1108</pre>
1109<p class="prompt">
1110<span id="ps1"><?php echo htmlescape($ini['settings']['PS1']); ?></span>
1111<input name="command" type="text" onkeyup="key(event)"
1112 size="<?php echo $columns-strlen($ini['settings']['PS1']); ?>" tabindex="1">
1113</p>
1114</div>
1115
1116<?php } else { /* Output the 'editor' */
1117print "You are editing this file: <code>" . htmlescape($filetoedit) . "</code>\n";
1118if ($writeaccesswarning) { ?>
1119
1120<div class="warning">
1121 <p><b>Warning:</b> You may not have write access to <code><?php echo htmlescape($filetoedit); ?></code></p>
1122</div>
1123
1124<?php
1125} /*write access warning*/
1126echo $warning;
1127?>
1128
1129<div id="terminal">
1130<textarea name="filecontent" id="filecontent" cols="<?php echo $columns ?>" rows="<?php echo $rows ?>">
1131<?php
1132 print(htmlescape($editorcontent));
1133?>
1134</textarea>
1135</div>
1136
1137<?php } /* End of terminal */ ?>
1138
1139<p>
1140<?php if (! $showeditor) { /* You can not resize the textarea while
1141 * the editor is 'running', because if you would
1142 * do so you would lose the changes you have
1143 * already made in the textarea since last saving */
1144?>
1145 <span style="float: right">Size: <input type="text" name="rows" size="2"
1146 maxlength="3" value="<?php echo $rows ?>"> × <input type="text"
1147 name="columns" size="2" maxlength="3" value="<?php echo $columns
1148 ?>"></span><br>
1149<input type="submit" value="Execute command">
1150<input type="submit" name="clear" value="Clear screen">
1151<?php } else { /* for 'editor-mode' */ ?>
1152<input type="hidden" name="filetoedit" id="filetoedit" value="<?php print($filetoedit) ?>">
1153<input type="submit" name="savefile" value="Save and Exit">
1154<input type="reset" value="Undo all Changes">
1155<input type="submit" value="Exit without saving" onclick="javascript:document.getElementById('filetoedit').value='';document.getElementById('filecontent').value='';return true;">
1156<?php } ?>
1157
1158 <input type="submit" name="logout" value="Logout">
1159</p>
1160</fieldset>
1161</form>
1162
1163<?php if ($ini['settings']['file-upload']) { ?>
1164<form name="upload" enctype="multipart/form-data" action="" method="post">
1165<div><br><br>
1166<input name="csrf_token" type="hidden" value="<?php echo $_SESSION['csrf_token'];?>">
1167<fieldset>
1168 <legend>File upload</legend>
1169<?php
1170 if (isset($_FILES['uploadfile']['tmp_name']) && $uploaderror >0) {
1171 $uploaderrors = array(
1172 0=>"There is no error, the file uploaded with success",
1173 1=>"The uploaded file exceeds the upload_max_filesize directive in php.ini",
1174 2=>"The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form",
1175 3=>"The uploaded file was only partially uploaded",
1176 4=>"No file was uploaded",
1177 6=>"Missing a temporary folder"
1178 );
1179 echo "<p class='warning'>File Upload error: $uploaderrors[$uploaderror]</p>";
1180 }
1181?>
1182 Select file for upload:
1183 <input type="file" name="uploadfile" size="40"><br>
1184<input type="submit" value="Upload file">
1185</fieldset>
1186</div>
1187</form>
1188 <?php } ?>
1189
1190<?php } ?>
1191
1192<hr>
1193
1194<p>Please consult the <a href="README" rel="nofollow">README</a>, <a
1195href="INSTALL" rel="nofollow">INSTALL</a>, and <a href="SECURITY" rel="nofollow">SECURITY</a> files for
1196instruction on how to use PHP Shell.</p>
1197<p>If you have not created accounts for phpshell, please use
1198<a href="pwhash.php">pwhash.php</a> to create secure (hashed and salted) passwords.</p>
1199
1200<hr>
1201<address>
1202Copyright © 2000–2020, the Phpshell-team. Get the
1203latest version at <a
1204href="http://phpshell.sourceforge.net/">http://phpshell.sourceforge.net/</a>.
1205</address>
1206</body>
1207</html>