diff --git a/README.md b/README.md index 8d66875..40b8be4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# :star2: FastSitePHP Playground +# 🌟 FastSitePHP Playground **Thanks for visiting!** 🌠👍 @@ -27,7 +27,7 @@ -## :gear: How it works +## ⚙️ How it works

Playground - How it works @@ -35,13 +35,13 @@ **Update - December 30th, 2021** -Originally this site was hosted on a separate server for years but now the playground is hosted on the same server as the main site along with several other open source sites. This site and the other sites do not get enough traffic to justify the need for separate servers so now only 1 server is used. See detailed setup docs for more. Logic from the above graphic still applies except the separate server. +Originally this site was hosted on a separate server for several years but now the playground is hosted on the same server as the main site along with several other open source sites. This site and the other sites do not get enough traffic to justify the need for separate servers so now only 1 server is used. See detailed setup docs for more. Logic from the above graphic still applies except the separate server. -## :gear: Detailed Server Setup +## ⚙️ Detailed Server Setup -https://github.com/fastsitephp/playground/blob/master/docs/Playground%20Server%20Setup.txt +https://github.com/fastsitephp/playground/blob/master/docs/playground-server-setup.sh -## :desktop_computer: Running Locally +## 🖥️ Running Locally Download this repository then run the install script. This will also generate a new `app_data/.env` file which is used for authentication. @@ -58,13 +58,22 @@ https://github.com/fastsitephp/fastsitephp/blob/master/website/public/js/playgro When you run locally on a standard build of PHP user sites will be insecure however this is acceptable for local development. -## :handshake: Contributing +## 🤝 Contributing * If you find a typo or grammar error please fix and submit. * Additional language template translations are needed. Refer to the main project if you can help with translations. * Any changes to the core code will likely not be accepted unless you first open an issue. A lot of security is needed in order to make this site work so every line of code must be carefully considered. -* If you think you’ve found a minor issue with security or have additional security ideas please open an issue. No financial transactions other than the cost of the server are dependent on this site so opening a public issue is ok. However if you are able to write files, obtain root or sudo access to the server then please [get in touch privately](https://www.fastsitephp.com/en/security-issue). -## :memo: License +## 🔒 Security + +* The actual site uses the following disclaimer `Please do not attack this site or use it for malicious purposes`, however if you are a security researcher its understandable that you may want to test the security of this site. +* Reasonable testing is acceptable however if the site ends up being compromised maliciously or attacks slow down the main sites then it may be taken down so please keep that in mind. +* For manual testing and details on what would be a good starting point to attack the site see files: + * https://github.com/fastsitephp/playground/blob/master/scripts/app-error-testing.php + * https://github.com/fastsitephp/playground/blob/master/scripts/app-error-testing-2.php +* If you think you’ve found an issue with security or have additional security ideas please open an issue. This site has a niche audience and no financial transactions other than the cost of the server ($5 USD a month) are dependent on this site so opening a public issue is ok even if you have an exploit. You can also get in touch privately from: https://www.fastsitephp.com/en/security-issue +* If you accidentally cause serious problems to the server or take it down the please get in contact with the author immediately so a new server can be setup. If someone take the server down from the playground I would be more interested in how and if it can be prevented rather than worried about the server itself. + +## 📝 License This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details. diff --git a/docs/PHP Custom Build Instructions.txt b/docs/PHP Custom Build Instructions.txt index 4192606..10730e7 100644 --- a/docs/PHP Custom Build Instructions.txt +++ b/docs/PHP Custom Build Instructions.txt @@ -2,10 +2,16 @@ This file provides info on how which files to edit for the custom build of PHP. Basically a single INI setting is added [file_access_is_limited] that allows for some requests to have access to writing and modifying files while other requests -will only have read access - using the feature properly requires the function -[ini_set()] to be disabled from [php.ini]. +will only have read access. -See the file [Playground Server Setup.txt] for full build and setup instructions. +Using the feature properly requires the function [ini_set()] to be disabled from +[php.ini] and more importantly [ini_set()] is also disabled from the custom build +of PHP. Originally when FastSitePHP was published [php.ini] setting [disable_functions] +was used however based on https://github.com/mm0r1/exploits it can be bypassed by +user code so the custom build of PHP is needed and now a separate script is used +during server setup to update many files. + +See the file [playground-server-setup.sh] for full build and setup instructions. -------------------------------------------------------------------------------------- | Helpful Links for building and working with PHP Source @@ -29,7 +35,7 @@ https://eddmann.com/posts/introduction-to-creating-a-basic-php-extension/ https://php.tutorials24x7.com/blog/how-to-install-php-8-from-source-on-ubuntu-20-04-lts -------------------------------------------------------------------------------------- -| ext\standard\file.h +| ext/standard/file.h -------------------------------------------------------------------------------------- *) Under: typedef struct { @@ -42,7 +48,7 @@ https://php.tutorials24x7.com/blog/how-to-install-php-8-from-source-on-ubuntu-20 int pclose_ret; -------------------------------------------------------------------------------------- -| ext\standard\file.c +| ext/standard/file.c -------------------------------------------------------------------------------------- *) Before: PHP_INI_END() @@ -106,17 +112,21 @@ https://php.tutorials24x7.com/blog/how-to-install-php-8-from-source-on-ubuntu-20 } -------------------------------------------------------------------------------------- -| ext\standard\exec.c +| main/php.h -------------------------------------------------------------------------------------- -# 4 functions end up being disabled in code. Originally when FastSitePHP -# was published [php.ini] setting [disable_functions] was used however -# based on https://github.com/mm0r1/exploits it can be bypassed by user code -# so the custom build of PHP is needed. + *) Under: + #define PHP_METHOD ZEND_METHOD + *) Add: + #define DISABLED_FOR_PLAYGROUND \ + php_error_docref(NULL, E_ERROR, "This function is disabled by using a custom PHP build for the FastSitePHP Playground."); \ + RETURN_FALSE - PHP_FUNCTION(shell_exec) - PHP_FUNCTION(exec) +-------------------------------------------------------------------------------------- +| Many files are also updated from the script [update-php-c-source-files.php] +| All of the updates are related to the custom C Macro `DISABLED_FOR_PLAYGROUND()` +| How it works it the first line of each function ends up with the Macro. +| Example below for [ext\standard\exec.c]. See the PHP script for details. +-------------------------------------------------------------------------------------- PHP_FUNCTION(system) - PHP_FUNCTION(passthru) { - php_error_docref(NULL, E_ERROR, "This function is disabled by using a custom PHP build for the FastSitePHP Playground."); - RETURN_FALSE; + DISABLED_FOR_PLAYGROUND; diff --git a/docs/PHP INI Settings.txt b/docs/PHP INI Settings.txt index 207e732..698d731 100644 --- a/docs/PHP INI Settings.txt +++ b/docs/PHP INI Settings.txt @@ -24,7 +24,10 @@ zend_extension=opcache.so # or for PHP 8 uncomment line: [zend_extension=opcache] -# Edit and verify the following: +# Edit and verify the following +# Many other functions with file i/o exist (example: gzwrite), however +# the custom build of PHP excludes many functions by default so they +# do not have to be added to the disabled list. allow_url_fopen = Off allow_url_include = Off file_uploads = Off @@ -35,5 +38,5 @@ post_max_size = 1M default_socket_timeout = 1 enable_dl = Off opcache.revalidate_freq=0 -disable_functions = ini_set, ini_restore, exec, passthru, shell_exec, system, proc_open, proc_nice, popen, curl_exec, curl_multi_exec, dl, sleep, usleep, gc_disable, set_include_path, set_time_limit, tempnam, tmpfile, fopen, fwrite, ftruncate, fputcsv, link, umask, touch, chown, chmod, chgrp, glob, symlink, stream_socket_client, stream_socket_server, stream_context_create, stream_socket_pair, dns_get_record, dns_check_record, dns_get_mx, fsockopen, pfsockopen, setcookie, setrawcookie, syslog, openlog, stream_wrapper_restore, finfo_set_flags, gzwrite, gzwrite, mail, session_start, session_create_id, error_log +disable_functions = ini_set, ini_restore, exec, passthru, shell_exec, system, proc_open, proc_nice, popen, curl_exec, curl_multi_exec, dl, sleep, usleep, set_include_path, set_time_limit, tempnam, tmpfile, fopen, fwrite, ftruncate, fputcsv, link, umask, touch, chown, chmod, chgrp, glob, symlink, stream_socket_client, stream_socket_server, stream_context_create, stream_socket_pair, dns_get_record, dns_check_record, dns_get_mx, fsockopen, pfsockopen, setcookie, setrawcookie, syslog, openlog, stream_wrapper_restore, finfo_set_flags, mail, session_start, session_create_id, error_log, lchown, lchgrp, move_uploaded_file disable_classes = SplFileObject, SplTempFileObject, FilesystemIterator, DirectoryIterator, GlobIterator diff --git a/docs/playground-server-setup.sh b/docs/playground-server-setup.sh index c5d9301..bf8cd3a 100644 --- a/docs/playground-server-setup.sh +++ b/docs/playground-server-setup.sh @@ -64,17 +64,18 @@ mkdir /var/www/fastsitephp-playground/public/sites # Setup custom build of PHP with Apache for FastSitePHP Playground which php -# This is the active version of PHP prior to these commands +# This is the active version of PHP prior to these commands (and needed if re-running) # Output: /usr/bin/php sudo apt install -y apache2 apache2-dev libxml2-dev wget https://www.php.net/distributions/php-7.4.27.tar.bz2 tar xjf php-7.4.27.tar.bz2 wget https://fastsitephp.s3-us-west-1.amazonaws.com/playground/php-7.4.27/file.h wget https://fastsitephp.s3-us-west-1.amazonaws.com/playground/php-7.4.27/file.c -wget https://fastsitephp.s3-us-west-1.amazonaws.com/playground/php-7.4.27/exec.c +wget https://fastsitephp.s3.us-west-1.amazonaws.com/playground/php-7.4.27/php.h mv file.h ~/php-7.4.27/ext/standard/file.h mv file.c ~/php-7.4.27/ext/standard/file.c -mv exec.c ~/php-7.4.27/ext/standard/exec.c +mv php.h ~/php-7.4.27/main/php.h +/usr/bin/php /var/www/fastsitephp-playground/scripts/update-php-c-source-files.php cd php-7.4.27 ./configure --with-apxs2=/usr/bin/apxs --disable-all --enable-json --enable-filter --enable-ctype --enable-opcache # run `make` - this is expected to take several minutes once `wait` is called on most servers @@ -137,7 +138,7 @@ sudo service apache2 restart # Runs once per minute, if not using [sudo] then sites will end up not being deleted. sudo crontab -e # Enter [1] for nano, and add the following after header comments: -* * * * * /usr/bin/php /var/www/scripts/delete-expired-sites.php > /var/www/app_data/last-cron-job.txt 2>&1 +* * * * * /usr/bin/php /var/www/fastsitephp-playground/scripts/delete-expired-sites.php > /var/www/fastsitephp-playground/app_data/last-cron-job.txt 2>&1 # Point ngnix to the Apache site running at 127.0.0.1:8080 nano nginx-config.txt @@ -182,6 +183,6 @@ sudo reboot # - Run local build of FastSitePHP and you can test the new playground server. # See the readme and docs on how to run FastSitePHP. The setup is quick and simple. # - Try the site and verify it works and shows the installed version of PHP. -# - Copy and paste contents from the following PHP server side scripts to verify errors: +# - Copy and paste contents from the following PHP server side scripts to verify error tests: # scripts/app-error-testing.php # scripts/app-error-testing-2.php diff --git a/scripts/app-error-testing.php b/scripts/app-error-testing.php index 0c73190..7e65a64 100644 --- a/scripts/app-error-testing.php +++ b/scripts/app-error-testing.php @@ -6,6 +6,17 @@ // PHP Build on a live server most routes will fail with an expected error. // // To use copy the contents of this file to [app.php] after creating a custom site. +// +// If you are a security research and interested in testing attacks the first +// thing to do would be to overwrite the [.htaccess] file root of the temporary +// site. If that file can be overwritten then you will have full write access +// to the server (as allowed under Apache permissions) and that would be a good +// starting point for more serious exploits. If you do use this site to test +// security and accidentally take down the server or cause serious issues with it +// then please let the author of FastSitePHP know so that the issue can be fixed +// and so a new server can be setup (if needed) in a timely manner. +// +// For more Advanced Exploit Testing see the file [app-error-testing-2.php]. $app->get('/', function() use ($app) { $html = <<<'HTML' @@ -30,6 +41,7 @@

  • Timeout Test
  • Check Error Log
  • Memory Limit
  • +
  • Disabled Object
  • Error Page
  • PHP Classes and Functions
  • PHP Info
  • @@ -117,7 +129,9 @@ }); // Error when function is disabled: -// ini_set() has been disabled for security reasons +// ini_set() has been disabled for security reasons +// Error when disabled with custom PHP build: +// ini_set(): This function is disabled by using a custom PHP build for the FastSitePHP Playground. $app->get('/ini-set', function() { ini_set('display_errors', 'off'); return ini_get('display_errors'); @@ -178,7 +192,7 @@ // This should show an empty array var_dump($_FILES); - // If an actual file made it to the server then the folowing could be used: + // If an actual file made it to the server then the following could be used: // move_uploaded_file($filename, $destination) }); @@ -194,10 +208,13 @@ }); // Make sure `error_log()` can't be used to overwrite `.htaccess`. +// This would overwrite the file so it works on the server and does not +// include settings [open_basedir, file_access_is_limited]; with those +// two settings excluded a end-user will have full-write access to any +// folder visible to PHP and Apache. $app->get('/error-log', function() use ($app) { - $s = "php_value open_basedir /\n"; - $s .= "php_flag file_access_is_limited off\n"; - error_log($s, 3, __DIR__ . '/../.htaccess'); + $contents = "FallbackResource index.php"; + error_log($contents, 3, __DIR__ . '/../.htaccess'); return 'Success [.htaccess] is overwritten'; }); @@ -210,31 +227,80 @@ } }); +// Error when disabled from [php.ini]: +// SplFileObject() has been disabled for security reasons +// If using the custom build of PHP but standard [php.ini] settings: +// SplFileObject::__construct(): This function is disabled by using a custom PHP build for the FastSitePHP Playground. +$app->get('/disabled-object', function() use ($app) { + $app->header('Content-Type', 'text/plain'); + $text = ''; + $file = new SplFileObject(__FILE__); + foreach ($file as $line_num => $line) { + $text .= "$line_num: $line"; + } + return $text; +}); + // This should return the standard error template $app->get('/error-page', function() { throw new \Exception('Test'); }); // Use this to see what is enabled on the server +// IMPORTANT - [php.ini] settings [disable_functions] and other settings are not secure +// from advanced exploits. Because of this fact a custom build of PHP is used to fully +// block the disabled functions and classes. See [app-error-testing-2.php] for more. +// This code is making assumptions that disabled function and classes listed here +// are actually disabled in the build. $app->get('/php-func', function() use ($app) { $classes = array_values(get_declared_classes()); $disabled_classes = ini_get('disable_classes'); $disabled_classes = explode(',', str_replace(' ', '', $disabled_classes)); $classes = array_diff($classes, $disabled_classes); - $text = str_repeat('-', 80) . "\n"; - $text .= count($classes) . " Classes\n"; - $text .= str_repeat('-', 80) . "\n"; - foreach ($classes as $name) { - $text .= $name . "\n"; - } - $functions = get_defined_functions(true); + $functions = get_defined_functions(false); $functions = array_values($functions['internal']); - $text .= "\n\n" . str_repeat('-', 80) . "\n"; - $text .= count($functions) . ' Functions' . "\n"; - $text .= str_repeat('-', 80) . "\n"; - foreach ($functions as $name) { - $text .= $name . "\n"; + if (filter_var(ini_get('file_access_is_limited'), FILTER_VALIDATE_BOOLEAN) === true) { + // This info is hard-coded on the assumption that PHP is built with modified functions. + // This script confirms each modified function. + $modified_functions = ['file_put_contents', 'mkdir', 'rmdir', 'rename', 'unlink', 'copy']; + } else { + $modified_functions = []; + } + $disabled_functions = ini_get('disable_functions'); + $disabled_functions = explode(',', str_replace(' ', '', $disabled_functions)); + $unused_disabled_fn = []; + foreach ($disabled_functions as $fn) { + if (!in_array($fn, $functions)) { + $unused_disabled_fn[] = $fn; + } + } + $functions = array_diff($functions, $disabled_functions); + $functions = array_diff($functions, $modified_functions); + + $obj_groups = [ + [$modified_functions, 'Modified Functions'], + [$disabled_classes, 'Disabled Classes'], + [$disabled_functions, 'Disabled Functions'], + [$unused_disabled_fn, 'Disabled Functions (Not included with this PHP Build)'], + [$classes, 'Classes'], + [$functions, 'Functions'], + ]; + + $text = ''; + foreach ($obj_groups as $group) { + $fn = $group[0]; + if (count($fn) === 0) { + continue; + } + $label = $group[1]; + $text .= "\n" . str_repeat('-', 80) . "\n"; + $text .= count($fn) . ' ' . $label . "\n"; + $text .= str_repeat('-', 80) . "\n"; + foreach ($fn as $name) { + $text .= $name . "\n"; + } + $text .= "\n"; } $app->header('Content-Type', 'text/plain'); diff --git a/scripts/update-php-c-source-files.php b/scripts/update-php-c-source-files.php new file mode 100755 index 0000000..89dbe9d --- /dev/null +++ b/scripts/update-php-c-source-files.php @@ -0,0 +1,125 @@ + 'ini_set,ini_restore,sleep,usleep,set_include_path,error_log,move_uploaded_file', + 'ext/standard/exec.c' => 'exec,system,passthru,shell_exec,proc_nice', + 'ext/curl/interface.c' => 'curl_exec', + 'ext/curl/multi.c' => 'curl_multi_exec', + 'ext/standard/dl.c' => 'dl', + 'main/main.c' => 'set_time_limit', + // Functions with `PHP_NAMED_FUNCTION` are prefixed with 'php_if_' for the name + 'ext/standard/file.c' => 'tempnam,PHP_NAMED_FUNCTION(php_if_tmpfile),PHP_NAMED_FUNCTION(php_if_fopen),' + . 'fwrite,PHP_NAMED_FUNCTION(php_if_ftruncate),fputcsv,umask,popen', + 'ext/standard/link.c' => 'link,symlink', + 'ext/standard/filestat.c' => 'touch,chown,lchown,chmod,chgrp,lchgrp', + 'ext/standard/dir.c' => 'glob', + 'ext/standard/streamsfuncs.c' => 'stream_socket_client,stream_socket_server,stream_context_create,stream_socket_pair', + 'ext/standard/dns.c' => 'dns_get_record,dns_check_record,dns_get_mx', + 'ext/standard/fsock.c' => 'fsockopen,pfsockopen', + 'ext/standard/head.c' => 'setcookie,setrawcookie', + 'ext/standard/syslog.c' => 'syslog,openlog', + 'main/streams/userspace.c' => 'stream_wrapper_restore', + 'ext/fileinfo/fileinfo.c' => 'finfo_set_flags', + 'ext/standard/mail.c' => 'mail', + 'ext/session/session.c' => 'session_start,session_create_id', + // Classes + 'ext/spl/spl_directory.c' => 'SPL_METHOD(SplFileObject, __construct)|' + . 'SPL_METHOD(SplTempFileObject, __construct)|SPL_METHOD(FilesystemIterator, __construct)|' + . 'SPL_METHOD(DirectoryIterator, __construct)|SPL_METHOD(GlobIterator, __construct)', +]; +$update_text = 'DISABLED_FOR_PLAYGROUND;'; +$file_updates = 0; +$fn_updates = 0; +$fn_skipped = 0; +$errors = 0; + +echo 'Updating C Files from [php-src] to disable functions:'; +if (!is_dir($root_dir)) { + echo "\nERROR - PHP Source code was not found in the folder: " . $root_dir; + exit(); +} + +foreach ($update_files as $file => $functions) { + $path = $root_dir . '/' . $file; + if (!is_file($path)) { + echo "\nERROR - Missing file: " . $path; + $errors++; + continue; + } + $contents = file_get_contents($path); + $orig_content = $contents; + $functions = (strpos($functions, '|') === false ? explode(',', $functions) : explode('|', $functions)); + $updated = false; + foreach ($functions as $fn_name) { + // Build Search text - either `PHP_FUNCTION(name)` or hard-coded the correct version above + // such as functions using `PHP_NAMED_FUNCTION()` or `SPL_METHOD()`. + $search = (strpos($fn_name, '(') === false ? 'PHP_FUNCTION(' . $fn_name . ')' : $fn_name); + $pos = strpos($contents, $search); + if ($pos === false) { + echo "\nERROR - " . $search . ' was not found in file: ' . $path; + $errors++; + continue; + } + $start = $pos + strlen($search); + $next_50_char = substr($contents, $start, 50); + if (strpos($next_50_char, $update_text) === false) { + $len = strlen($contents); + $found = false; + while ($start < $len) { + if ($contents[$start] === '{') { + $contents = substr($contents, 0, $start+1) . "\n\tDISABLED_FOR_PLAYGROUND;" . substr($contents, $start+1); + $found = true; + break; + } + $start++; + } + if ($found) { + $fn_updates++; + $updated = true; + } else { + echo "\nERROR - Missing file: " . $path; + $errors++; + } + } else { + $fn_skipped++; + } + } + if ($updated) { + file_put_contents($path, $contents); + $file_updates++; + } +} + +echo "\nFiles Checked: " . count(array_keys($update_files)); +echo "\nFiles Updated: " . $file_updates; +echo "\nFunctions Updated: " . $fn_updates; +echo "\nFunctions Skipped: " . $fn_skipped; +echo "\nErrors: " . $errors; +echo "\n";