tests/test_parameters.py
Rama master · réplica de lectura del repositorio original.
#!/usr/bin/env python
# encoding: UTF-8
"""
This file is part of Commix Project (https://commixproject.com).
Copyright (c) 2014-2026 Anastasios Stasinopoulos (@ancst).
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
For more see the file 'readme/COPYING' for copying permission.
"""
"""
How a parameter is read out of a request and put back with the marker in it. Everything here is
string work, done before anything is sent - and getting it wrong moves the injection point rather
than failing outright, which is the kind of mistake a scan reports as 'not injectable'.
"""
import unittest
from src.core.requests import parameters
from src.utils import settings
from src.core.parse import cmdline as menu
class UrlParts(unittest.TestCase):
def test_the_query_is_cut_from_the_path(self):
self.assertEqual("http://h/a/b.php", parameters.get_url_part("http://h/a/b.php?x=1&y=2"))
def test_a_url_without_a_query_is_left_whole(self):
self.assertEqual("http://h/a/b.php", parameters.get_url_part("http://h/a/b.php"))
class KeyValues(unittest.TestCase):
def test_the_value_is_what_follows_the_first_equals(self):
self.assertEqual("1", parameters.get_kv_value("a=1"))
def test_a_pair_without_a_value_answers_empty(self):
self.assertEqual("", parameters.get_kv_value("a"))
def test_an_equals_inside_the_value_is_kept(self):
self.assertEqual("1=2", parameters.get_kv_value("a=1=2"))
class CdataWrapping(unittest.TestCase):
def test_the_wrapper_comes_off(self):
self.assertEqual("v", parameters.unwrap_cdata("<![CDATA[v]]>"))
def test_a_bare_value_is_left_alone(self):
self.assertEqual("v", parameters.unwrap_cdata("v"))
class MarkerPlacement(unittest.TestCase):
def test_the_marker_lands_after_the_value_it_names(self):
tagged = parameters.apply_tag_to_value("a=1&b=2", "1")
self.assertEqual("a=1" + settings.INJECT_TAG + "&b=2", tagged)
def test_the_other_values_are_untouched(self):
self.assertIn("b=2", parameters.apply_tag_to_value("a=1&b=2", "1"))
class XmlShields(unittest.TestCase):
"""
The leaves are lifted out so the payload cannot be read as markup, then put back. What matters is
that putting them back returns exactly what was lifted - a drift here corrupts the request body.
"""
def test_a_shielded_document_comes_back_whole(self):
for document in ("<a><b>v</b></a>",
"<a><b>1</b><c>2</c></a>",
"<root><x>a&b</x></root>",
"<a/>"):
shielded, lifted = parameters.shield_xml_leaves(document)
self.assertEqual(document, parameters.restore_xml_shields(shielded, lifted), document)
def test_the_shielded_form_holds_none_of_the_leaf_text(self):
shielded, lifted = parameters.shield_xml_leaves("<a><b>SECRET</b></a>")
self.assertNotIn("SECRET", shielded)
self.assertTrue(any("SECRET" in _ for _ in lifted))
class JsonShapes(unittest.TestCase):
def test_an_empty_object_is_recognised(self):
self.assertTrue(parameters.is_empty_json_object("{}"))
self.assertTrue(parameters.is_empty_json_object(' { } '))
def test_an_object_with_a_member_is_not(self):
self.assertFalse(parameters.is_empty_json_object('{"a":1}'))
def test_json_is_parsed_in_the_order_it_was_written(self):
parsed = parameters.json_format('{"b":1,"a":2}')
self.assertEqual(["b", "a"], list(parsed.keys()))
def test_something_that_is_not_json_is_refused(self):
# The callers decide what to do about it; answering a half-parse would be worse.
self.assertRaises(Exception, parameters.json_format, "a=1&b=2")
class PostSplitting(unittest.TestCase):
def test_a_form_body_splits_on_the_ampersand(self):
split, original = parameters.split_post_parameters("a=1&b=2")
self.assertEqual(["a=1", "b=2"], split)
self.assertEqual("a=1&b=2", original)
def test_a_single_pair_stays_one(self):
split, _ = parameters.split_post_parameters("a=1")
self.assertEqual(["a=1"], split)
class NamedBoundary(unittest.TestCase):
"""
What '--prefix' and '--suffix' do to the boundary the sweep was going to send.
"""
def setUp(self):
self.prefix, self.suffix = menu.options.prefix, menu.options.suffix
self.load_session = settings.LOAD_SESSION
settings.LOAD_SESSION = False
def tearDown(self):
menu.options.prefix, menu.options.suffix = self.prefix, self.suffix
settings.LOAD_SESSION = self.load_session
def test_a_named_boundary_stands_in_place_of_the_one_being_tried(self):
"""
Added to it instead, what the user named arrives behind whatever each boundary already carried,
so the sweep sends combinations nobody asked for and never once sends the one that was.
"""
menu.options.prefix, menu.options.suffix = "'", "'"
_, prefix = parameters.prefixes("payload", ";")
_, suffix = parameters.suffixes("payload", "&")
self.assertEqual("'", prefix)
self.assertEqual("'", suffix)
def test_an_unnamed_boundary_is_left_as_it_was(self):
menu.options.prefix = menu.options.suffix = None
_, prefix = parameters.prefixes("payload", ";")
_, suffix = parameters.suffixes("payload", "&")
self.assertEqual(";", prefix)
self.assertEqual("&", suffix)
class TestableValue(unittest.TestCase):
"""
Whether the parameter's own value is handed to the target, or a placeholder put in its place.
"""
def setUp(self):
from src.core.controller import controller
self.controller = controller
self.testable_value = settings.TESTABLE_VALUE
self.load_session = settings.LOAD_SESSION
settings.TESTABLE_VALUE = "127.0.0.1"
settings.LOAD_SESSION = False
def tearDown(self):
settings.TESTABLE_VALUE = self.testable_value
settings.LOAD_SESSION = self.load_session
def test_the_real_value_is_what_the_target_is_asked_with(self):
"""
The command before the payload is given this value, so a placeholder the target was not
answering on makes that command fail - and a separator that chains on it having succeeded then
never fires, whatever the target would have carried.
"""
url = "http://127.0.0.1/x.php?addr=127.0.0.1" + settings.INJECT_TAG
self.assertEqual(url, self.controller.attempt_skip_testable_value(url, "GET", "addr"))
self.assertEqual("127.0.0.1", settings.TESTABLE_VALUE)
self.assertFalse(settings.TESTABLE_VALUE_OPTIMIZED)
def test_a_placeholder_an_earlier_run_confirmed_is_taken_up_again(self):
"""
The one exchange still made: a resumed run is handed what the run that stored it had already
asked the payload's own question through, so it is taken as it stands rather than measured for
again. Without this the stored answer is read and nothing is done with it.
"""
stored = self.controller.session_handler.check_stored_testable_value
try:
self.controller.session_handler.check_stored_testable_value = lambda *_: "ABC"
settings.LOAD_SESSION = True
url = "http://127.0.0.1/x.php?addr=127.0.0.1" + settings.INJECT_TAG
rewritten = self.controller.attempt_skip_testable_value(url, "GET", "addr")
self.assertEqual("http://127.0.0.1/x.php?addr=ABC" + settings.INJECT_TAG, rewritten)
self.assertEqual("ABC", settings.TESTABLE_VALUE)
self.assertTrue(settings.TESTABLE_VALUE_OPTIMIZED)
finally:
self.controller.session_handler.check_stored_testable_value = stored
settings.TESTABLE_VALUE_OPTIMIZED = False
if __name__ == "__main__":
unittest.main()
class ReplacedValue(unittest.TestCase):
"""
Where the payload goes relative to the value the parameter arrived with.
Behind it is what nearly every sink wants. A sink reading the value's own first byte is the
exception, and there the value has to go rather than sit in front of the payload - an
interpreter's two-argument open() handed a leading '|' runs what follows, while the same pipe with
a value before it is read as a filename and nothing runs at all.
"""
def setUp(self):
self.testable_value = settings.TESTABLE_VALUE
self.replace_value = settings.REPLACE_VALUE
self.prefix = menu.options.prefix
menu.options.prefix = None
settings.TESTABLE_VALUE = "127.0.0.1"
def tearDown(self):
settings.TESTABLE_VALUE = self.testable_value
settings.REPLACE_VALUE = self.replace_value
menu.options.prefix = self.prefix
def test_the_value_is_carried_in_front_of_the_payload_by_default(self):
settings.REPLACE_VALUE = False
payload, prefix = parameters.prefixes(";echo Hello", "")
self.assertTrue(payload.startswith("127.0.0.1"), repr(payload) + " drops the value")
def test_the_payload_stands_alone_where_it_replaces_the_value(self):
settings.REPLACE_VALUE = True
payload, prefix = parameters.prefixes("|echo Hello", "")
self.assertEqual("|echo Hello", payload)
self.assertTrue(payload.startswith("|"), "the separator is not the value's first byte")
def test_a_boundary_prefix_is_still_carried_when_the_value_is_replaced(self):
settings.REPLACE_VALUE = True
payload, prefix = parameters.prefixes(";echo Hello", "'")
self.assertEqual("';echo Hello", payload)