def test_encode_two_files(self): _files = [('file1', NamedStringIO('hello world', name='test1.txt')), ('file2', NamedStringIO('bye bye', name='test2.txt'))] _, encoded = multipart_encode((), _files, boundary='fakeboundary') EXPECTED = '--fakeboundary\r\nContent-Disposition: form-data;' \ ' name="file1"; filename="test1.txt"\r\n' \ 'Content-Type: text/plain\r\n\r\nhello world\r\n' \ '--fakeboundary\r\nContent-Disposition: form-data;' \ ' name="file2"; filename="test2.txt"\r\n' \ 'Content-Type: text/plain\r\n\r\nbye bye\r\n' \ '--fakeboundary--\r\n\r\n' self.assertEqual(EXPECTED, encoded)
def _upload_shell_and_confirm_exec(self, vuln_obj, file_content, file_name): """ :return: True if we were able to upload and the remote server actually executes the remote file. """ # The vuln was saved to the kb as a vuln object method = vuln_obj.get_method() exploit_dc = vuln_obj.get_dc() # Create a file that will be uploaded file_handler = NamedStringIO(file_content, file_name) for file_var_name in vuln_obj['file_vars']: # the [0] was added here to support repeated parameter names exploit_dc[file_var_name][0] = file_handler # Upload the file http_method = getattr(self._uri_opener, method) http_method(vuln_obj.get_url(), exploit_dc) # Call the uploaded script with an empty value in cmd parameter # this will return the shell_handler.SHELL_IDENTIFIER if success dst = vuln_obj['file_dest'] self._exploit = dst.get_domain_path().url_join(file_name) self._exploit.querystring = u'cmd=' response = self._uri_opener.GET(self._exploit) if shell_handler.SHELL_IDENTIFIER in response.get_body(): return True return False
def audit(self, freq, orig_response, debugging_id): """ Searches for file upload vulns. :param freq: A FuzzableRequest :param orig_response: The HTTP response associated with the fuzzable request :param debugging_id: A unique identifier for this call to audit() """ if freq.get_method().upper() != 'POST' or not freq.get_file_vars(): return # Unique payload for the files we upload payload = rand_alnum(239) for file_parameter in freq.get_file_vars(): for extension in self._extensions: _, file_content, file_name = get_template_with_payload( extension, payload) # Only file handlers are passed to the create_mutants functions named_stringio = NamedStringIO(file_content, file_name) mutants = create_mutants(freq, [named_stringio], fuzzable_param_list=[file_parameter]) for mutant in mutants: mutant.uploaded_file_name = file_name mutant.extension = extension mutant.file_content = file_content mutant.file_payload = payload self._send_mutants_in_threads(self._uri_opener.send_mutant, mutants, self._analyze_result, debugging_id=debugging_id)
def test_file_stringio_upload(self): data = { "MAX_FILE_SIZE": "10000", "uploadedfile": NamedStringIO('file content', name='test.txt') } resp = self.opener.open(self.MOTH_FILE_UP_URL, data) self.assertTrue('was successfully uploaded' in resp.read())
def create_mutants(freq, mutant_str_list, fuzzable_param_list, append, fuzzer_config): """ This is a very important method which is called in order to create mutants. Usually called from fuzzer.py module. """ if not 'fuzz_form_files' in fuzzer_config: return [] if not isinstance(freq, HTTPPostDataRequest): return [] file_vars = freq.get_file_vars() if not file_vars: return [] fake_file_objs = [] ext = fuzzer_config['fuzzed_files_extension'] for mutant_str in mutant_str_list: if isinstance(mutant_str, basestring): # I have to create the NamedStringIO with a "name". # This is needed for MultipartPostHandler fname = "%s.%s" % (rand_alpha(7), ext) str_file = NamedStringIO(mutant_str, name=fname) fake_file_objs.append(str_file) res = Mutant._create_mutants_worker(freq, FileContentMutant, fake_file_objs, file_vars, append, fuzzer_config) return res
def _upload_shell_and_confirm_exec(self, vuln_obj, file_content, file_name): """ :return: True if we were able to upload and the remote server actually executes the remote file. """ mutant = vuln_obj.get_mutant() mutant = mutant.copy() # Create a file that will be uploaded file_handler = NamedStringIO(file_content, file_name) mutant.set_token_value(file_handler) # For the files which are not in the target, set something smart. mutant.get_dc().smart_fill() # Upload the file self._uri_opener.send_mutant(mutant) # Call the uploaded script with an empty value in cmd parameter # this will return the shell_handler.SHELL_IDENTIFIER if success dst = vuln_obj['file_dest'] exploit_url = dst.get_domain_path().url_join(file_name) exploit_url.querystring = u'cmd=' response = self._uri_opener.GET(exploit_url) if shell_handler.SHELL_IDENTIFIER in response.get_body(): return exploit_url return None
def _verify_vuln(self, vuln_obj): """ This command verifies a vuln. This is really hard work! :P :param vuln_obj: The vuln to exploit. :return : True if vuln can be exploited. """ # The vuln was saved to the kb as a vuln object url = vuln_obj.get_url() method = vuln_obj.get_method() exploit_dc = vuln_obj.get_dc() # Create a file that will be uploaded extension = url.get_extension() path, file_name = self._create_file(extension) file_content = open(os.path.join(path, file_name), "r").read() file_handler = NamedStringIO(file_content, file_name) # If there are files, if 'file_vars' in vuln_obj: # # Upload the file # for file_var_name in vuln_obj['file_vars']: # the [0] was added here to support repeated parameter names exploit_dc[file_var_name][0] = file_handler http_method = getattr(self._uri_opener, method) response = http_method(vuln_obj.get_url(), exploit_dc) # Call the uploaded script with an empty value in cmd parameter # this will return the shell_handler.SHELL_IDENTIFIER if success dst = vuln_obj['file_dest'] self._exploit = dst.get_domain_path().url_join(file_name) self._exploit.querystring = u'cmd=' response = self._uri_opener.GET(self._exploit) # Clean-up file_handler.close() os.remove(os.path.join(path, file_name)) if shell_handler.SHELL_IDENTIFIER in response.get_body(): return True # If we got here, there is nothing positive to report return False
def test_encode_file_null(self): _files = [('file', NamedStringIO('\0hello world', name='test.txt'))] _, encoded = multipart_encode((), _files, boundary='fakeboundary') EXPECTED = '--fakeboundary\r\nContent-Disposition: form-data; name="file";'\ ' filename="test.txt"\r\nContent-Type: text/plain\r\n\r\n\x00'\ 'hello world\r\n--fakeboundary--\r\n\r\n' self.assertEqual(EXPECTED, encoded)
def test_encode_vars_files(self): _vars = [('a', 'b')] _files = [('file', NamedStringIO('file content', name='test.txt'))] _, encoded = multipart_encode(_vars, _files, boundary='fakeboundary') EXPECTED = '--fakeboundary\r\nContent-Disposition: form-data; name="a"'\ '\r\n\r\nb\r\n--fakeboundary\r\nContent-Disposition: form-data;'\ ' name="file"; filename="test.txt"\r\nContent-Type: text/plain'\ '\r\n\r\nfile content\r\n--fakeboundary--\r\n\r\n' self.assertEqual(EXPECTED, encoded)
def build_file(self, value): if isinstance(value, basestring): _, file_content, fname = get_template_with_payload( self._extension, value) # I have to create the NamedStringIO with a "name", # required for MultipartContainer to properly encode this as # multipart/post return NamedStringIO(file_content, name=fname) return value
def smart_fill_file(var_name, file_name): """ This function will return a NamedStringIO, ready to use in multipart forms. The contents of the file and its extension are carefully chosen to try to go through any form filters the web application might be implementing. """ extension = guess_extension(var_name, file_name) _, file_content, file_name = get_file_from_template(extension) # I have to create the NamedStringIO with a "name", # required for MultipartContainer to properly encode this as multipart/post return NamedStringIO(file_content, name=file_name)
def mutant_smart_fill(freq, dc_copy, ignore_pname, ignore_index, fuzzer_config): """ :param freq: The fuzzable request (original request instance) we're fuzzing :param ignore_pname: A parameter name to ignore :param ignore_index: The index we want to ignore :return: A data container that has been filled using smart_fill, ignoring the parameters that I'm fuzzing and filling the file inputs with valid image file. """ for var_name_dc in dc_copy: for element_index_dc, element_value_dc in enumerate( dc_copy[var_name_dc]): if (var_name_dc, element_index_dc) == (ignore_pname, ignore_index): continue if dc_copy.get_type(var_name_dc) in AVOID_FILLING_FORM_TYPES: continue # Fill only if the parameter does NOT have a value set. # # The reason of having this already set would be that the form # has something like this: # # <input type="text" name="p" value="foobar"> # if dc_copy[var_name_dc][element_index_dc] == '': # # Fill it smartly # dc_copy[var_name_dc][element_index_dc] = smart_fill( var_name_dc) # Please see the comment above (search for __HERE__) for an explanation # of what we are doing here: for var_name in freq.get_file_vars(): # Try to upload a valid file extension = fuzzer_config.get('fuzz_form_files') or 'gif' success, file_content, file_name = get_file_from_template(extension) # I have to create the NamedStringIO with a "name", # required for MultipartPostHandler str_file = NamedStringIO(file_content, name=file_name) # TODO: Is this hard-coded [0] enough? dc_copy[var_name][0] = str_file return dc_copy
def test_upload_file_using_fuzzable_request(self): form_params = FormParameters() form_params.add_file_input([('name', 'uploadedfile')]) form_params['uploadedfile'][0] = NamedStringIO('file content', name='test.txt') form_params.add_input([('name', 'MAX_FILE_SIZE'), ('type', 'hidden'), ('value', '10000')]) mpc = MultipartContainer(form_params) freq = FuzzableRequest(self.MOTH_FILE_UP_URL, post_data=mpc, method='POST') resp = self.opener.send_mutant(freq) self.assertIn('was successfully uploaded', resp.get_body())
def test_multipart_post_with_filename(self): fake_file = NamedStringIO('def', name='hello.txt') vars = [('a', 'bcd'), ] files = [('b', fake_file)] boundary, post_data = multipart_encode(vars, files) multipart_boundary = MultipartContainer.MULTIPART_HEADER headers = Headers([('content-length', str(len(post_data))), ('content-type', multipart_boundary % boundary)]) mpc = MultipartContainer.from_postdata(headers, post_data) self.assertIsInstance(mpc, MultipartContainer) self.assertIn('a', mpc) self.assertEqual(mpc['a'], ['bcd']) self.assertEqual(mpc.get_file_vars(), ['b']) self.assertEqual(mpc.get_parameter_type('a'), 'text') self.assertEqual(mpc.get_parameter_type('b'), 'file') self.assertEqual(mpc.get_file_name('b'), 'hello.txt')
def build_file(self, value): # # We don't want to create a new file if value is already a NamedStringIO # but if it is a string, we should create a new NamedStringIO instance # and return it # # The last "not isinstance" is important due to the fact that # NamedStringIO is a basestring subclass # if isinstance(value, basestring) and not isinstance(value, NamedStringIO): _, file_content, fname = get_template_with_payload(self._extension, value) # I have to create the NamedStringIO with a "name", # required for MultipartContainer to properly encode this as # multipart/post return NamedStringIO(file_content, name=fname) return value
def audit(self, freq, orig_response): """ Searches for file upload vulns. :param freq: A FuzzableRequest """ if freq.get_method().upper() != 'POST' or not freq.get_file_vars(): return for file_parameter in freq.get_file_vars(): for extension in self._extensions: _, file_content, file_name = get_file_from_template(extension) # Only file handlers are passed to the create_mutants functions named_stringio = NamedStringIO(file_content, file_name) mutants = create_mutants(freq, [named_stringio], fuzzable_param_list=[file_parameter]) for mutant in mutants: mutant.uploaded_file_name = file_name self._send_mutants_in_threads(self._uri_opener.send_mutant, mutants, self._analyze_result)
def test_mutant_creation_post_data(self): form_params = FormParameters() form_params.add_field_by_attr_items([("name", "username"), ("value", "")]) form_params.add_field_by_attr_items([("name", "address"), ("value", "")]) form_params.add_field_by_attr_items([("name", "image"), ("type", "file")]) form = MultipartContainer(form_params) freq = FuzzableRequest(self.url, post_data=form) ph = 'w3af.core.data.constants.file_templates.file_templates.rand_alpha' with patch(ph) as mock_rand_alpha: mock_rand_alpha.return_value = 'upload' generated_mutants = PostDataMutant.create_mutants(freq, self.payloads, [], False, self.fuzzer_config) self.assertEqual(len(generated_mutants), 6, generated_mutants) _, gif_file_content, _ = get_file_from_template('gif') gif_named_stringio = NamedStringIO(gif_file_content, 'upload.gif') expected_forms = [] form = MultipartContainer(copy.deepcopy(form_params)) form['image'] = [gif_named_stringio] form['username'] = ['def'] form['address'] = ['Bonsai Street 123'] expected_forms.append(form) form = MultipartContainer(copy.deepcopy(form_params)) form['image'] = [gif_named_stringio] form['username'] = ['abc'] form['address'] = ['Bonsai Street 123'] expected_forms.append(form) # TODO: Please note that these two multipart forms are a bug, since # they should never be created by PostDataMutant.create_mutants # (they are not setting the image as a file, just as a string) form = MultipartContainer(copy.deepcopy(form_params)) form['image'] = ['def'] form['username'] = ['John8212'] form['address'] = ['Bonsai Street 123'] expected_forms.append(form) form = MultipartContainer(copy.deepcopy(form_params)) form['image'] = ['abc'] form['username'] = ['John8212'] form['address'] = ['Bonsai Street 123'] expected_forms.append(form) # # TODO: /end # form = MultipartContainer(copy.deepcopy(form_params)) form['image'] = [gif_named_stringio] form['username'] = ['John8212'] form['address'] = ['abc'] expected_forms.append(form) form = MultipartContainer(copy.deepcopy(form_params)) form['image'] = [gif_named_stringio] form['username'] = ['John8212'] form['address'] = ['def'] expected_forms.append(form) boundary = get_boundary() noop = '1' * len(boundary) expected_data = [encode_as_multipart(f, boundary) for f in expected_forms] expected_data = set([s.replace(boundary, noop) for s in expected_data]) generated_forms = [m.get_dc() for m in generated_mutants] generated_data = [str(f).replace(f.boundary, noop) for f in generated_forms] self.assertEqual(expected_data, set(generated_data)) str_file = generated_forms[0]['image'][0] self.assertIsInstance(str_file, NamedStringIO) self.assertEqual(str_file.name[-4:], '.gif') self.assertEqual(gif_file_content, str_file) str_file = generated_forms[1]['image'][0] self.assertIsInstance(str_file, NamedStringIO) self.assertEqual(str_file.name[-4:], '.gif') self.assertEqual(gif_file_content, str_file) self.assertIn('name="image"; filename="upload.gif"', generated_data[0])
def test_stringio_upload(self): _file = NamedStringIO('file content', name='test.txt') self.upload_file(_file)
def test_named_string_io(self): content = 'content' name = 'name' ns_io = NamedStringIO(content, name) self.assertEqual(str(ns_io), content)
def test_generate_all(self): fuzzer_config = { 'fuzz_form_files': True, 'fuzzed_files_extension': 'gif' } form_params = FormParameters() form_params.set_method('POST') form_params.set_action(self.url) form_params.add_field_by_attr_items([("name", "username"), ("value", "")]) form_params.add_field_by_attr_items([("name", "address"), ("value", "")]) form_params.add_field_by_attr_items([("name", "image"), ("type", "file")]) form = MultipartContainer(form_params) freq = FuzzableRequest.from_form(form) ph = 'w3af.core.data.constants.file_templates.file_templates.rand_alpha' with patch(ph) as mock_rand_alpha: mock_rand_alpha.return_value = 'upload' generated_mutants = FileContentMutant.create_mutants( freq, self.payloads, [], False, fuzzer_config) self.assertEqual(len(generated_mutants), 2, generated_mutants) _, file_payload_abc, _ = get_template_with_payload('gif', 'abc') _, file_payload_def, _ = get_template_with_payload('gif', 'def') file_abc = NamedStringIO(file_payload_abc, 'upload.gif') file_def = NamedStringIO(file_payload_def, 'upload.gif') form_1 = MultipartContainer(copy.deepcopy(form_params)) form_2 = MultipartContainer(copy.deepcopy(form_params)) form_1['image'] = [file_abc] form_1['username'] = ['John8212'] form_1['address'] = ['Bonsai Street 123'] form_2['image'] = [file_def] form_2['username'] = ['John8212'] form_2['address'] = ['Bonsai Street 123'] expected_forms = [form_1, form_2] boundary = get_boundary() noop = '1' * len(boundary) expected_data = [ encode_as_multipart(f, boundary) for f in expected_forms ] expected_data = set([s.replace(boundary, noop) for s in expected_data]) generated_forms = [m.get_dc() for m in generated_mutants] generated_data = [ str(f).replace(f.boundary, noop) for f in generated_forms ] self.assertEqual(expected_data, set(generated_data)) str_file = generated_forms[0]['image'][0].get_value() self.assertIsInstance(str_file, NamedStringIO) self.assertEqual(str_file.name[-4:], '.gif') self.assertEqual(file_payload_abc, str_file) str_file = generated_forms[1]['image'][0].get_value() self.assertIsInstance(str_file, NamedStringIO) self.assertEqual(str_file.name[-4:], '.gif') self.assertEqual(file_payload_def, str_file) self.assertIn('name="image"; filename="upload.gif"', generated_data[0])