This commit is contained in:
George Hughey 2020-02-07 06:42:31 -08:00
parent 337f916920
commit 586f6c8174
7 changed files with 879 additions and 30 deletions

602
actions/http.py Normal file
View File

@ -0,0 +1,602 @@
# This file gutted from scapy as the module there doesn't quite work there.
# This file is part of Scapy
# See http://www.secdev.org/projects/scapy for more information
# Copyright (C) 2019 Gabriel Potter <gabriel@potter.fr>
# Copyright (C) 2012 Luca Invernizzi <invernizzi.l@gmail.com>
# Copyright (C) 2012 Steeve Barbeau <http://www.sbarbeau.fr>
# This file is a modified version of the former scapy_http plugin.
# It was reimplemented for scapy 2.4.3+ using sessions, stream handling.
# Original Authors : Steeve Barbeau, Luca Invernizzi
# Originally published under a GPLv2 license
import os
import re
import struct
import subprocess
from scapy.compat import plain_str, bytes_encode, \
gzip_compress, gzip_decompress
from scapy.config import conf
from scapy.consts import WINDOWS
from scapy.error import warning
from scapy.fields import StrField
from scapy.packet import Packet, bind_layers, bind_bottom_up, Raw
from scapy.utils import get_temp_file, ContextManagerSubprocess
from scapy.layers.inet import TCP, TCP_client
from scapy.modules import six
if "http" not in conf.contribs:
conf.contribs["http"] = {}
conf.contribs["http"]["auto_compression"] = True
# https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
GENERAL_HEADERS = [
"Cache-Control",
"Connection",
"Permanent",
"Content-Length",
"Content-MD5",
"Content-Type",
"Date",
"Keep-Alive",
"Pragma",
"Upgrade",
"Via",
"Warning"
]
COMMON_UNSTANDARD_GENERAL_HEADERS = [
"X-Request-ID",
"X-Correlation-ID"
]
REQUEST_HEADERS = [
"A-IM",
"Accept",
"Accept-Charset",
"Accept-Encoding",
"Accept-Language",
"Accept-Datetime",
"Access-Control-Request-Method",
"Access-Control-Request-Headers",
"Authorization",
"Cookie",
"Expect",
"Forwarded",
"From",
"Host",
"HTTP2-Settings",
"If-Match",
"If-Modified-Since",
"If-None-Match",
"If-Range",
"If-Unmodified-Since",
"Max-Forwards",
"Origin",
"Proxy-Authorization",
"Range",
"Referer",
"TE",
"User-Agent"
]
COMMON_UNSTANDARD_REQUEST_HEADERS = [
"Upgrade-Insecure-Requests",
"Upgrade-Insecure-Requests",
"X-Requested-With",
"DNT",
"X-Forwarded-For",
"X-Forwarded-Host",
"X-Forwarded-Proto",
"Front-End-Https",
"X-Http-Method-Override",
"X-ATT-DeviceId",
"X-Wap-Profile",
"Proxy-Connection",
"X-UIDH",
"X-Csrf-Token",
"Save-Data",
]
RESPONSE_HEADERS = [
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"Access-Control-Expose-Headers",
"Access-Control-Max-Age",
"Access-Control-Allow-Methods",
"Access-Control-Allow-Headers",
"Accept-Patch",
"Accept-Ranges",
"Age",
"Allow",
"Alt-Svc",
"Content-Disposition",
"Content-Encoding",
"Content-Language",
"Content-Location",
"Content-Range",
"Delta-Base",
"ETag",
"Expires",
"IM",
"Last-Modified",
"Link",
"Location",
"Permanent",
"P3P",
"Proxy-Authenticate",
"Public-Key-Pins",
"Retry-After",
"Server",
"Set-Cookie",
"Strict-Transport-Security",
"Trailer",
"Transfer-Encoding",
"Tk",
"Vary",
"WWW-Authenticate",
"X-Frame-Options",
]
COMMON_UNSTANDARD_RESPONSE_HEADERS = [
"Content-Security-Policy",
"X-Content-Security-Policy",
"X-WebKit-CSP",
"Refresh",
"Status",
"Timing-Allow-Origin",
"X-Content-Duration",
"X-Content-Type-Options",
"X-Powered-By",
"X-UA-Compatible",
"X-XSS-Protection",
]
def _strip_header_name(name):
"""Takes a header key (i.e., "Host" in "Host: www.google.com",
and returns a stripped representation of it
"""
return plain_str(name.strip()).replace("-", "_")
def _header_line(name, val):
"""Creates a HTTP header line"""
# Python 3.4 doesn't support % on bytes
return bytes_encode(name) + b": " + bytes_encode(val)
def _parse_headers(s):
headers = s.split(b"\r\n")
headers_found = {}
for header_line in headers:
try:
key, value = header_line.split(b':', 1)
except ValueError:
continue
header_key = _strip_header_name(key).lower()
# headers_found[header_key] = (key, value.strip()) # The first big change occurs here, using the header_line instead of value.strip()
headers_found[header_key] = (key, header_line .strip())
return headers_found
def _parse_headers_and_body(s):
''' Takes a HTTP packet, and returns a tuple containing:
_ the first line (e.g., "GET ...")
_ the headers in a dictionary
_ the body
'''
crlfcrlf = b"\r\n\r\n"
crlfcrlfIndex = s.find(crlfcrlf)
if crlfcrlfIndex != -1:
headers = s[:crlfcrlfIndex + len(crlfcrlf)]
body = s[crlfcrlfIndex + len(crlfcrlf):]
else:
headers = s
body = b''
first_line, headers = headers.split(b"\r\n", 1)
return first_line.strip(), _parse_headers(headers), body
def _dissect_headers(obj, s):
"""Takes a HTTP packet as the string s, and populates the scapy layer obj
(either HTTPResponse or HTTPRequest). Returns the first line of the
HTTP packet, and the body
"""
first_line, headers, body = _parse_headers_and_body(s)
for f in obj.fields_desc:
# We want to still parse wrongly capitalized fields
stripped_name = _strip_header_name(f.name).lower()
try:
_, value = headers.pop(stripped_name)
except KeyError:
continue
obj.setfieldval(f.name, value)
if headers:
headers = {key: value for key, value in six.itervalues(headers)}
obj.setfieldval('Unknown_Headers', headers)
return first_line, body
class _HTTPContent(Packet):
# https://developer.mozilla.org/fr/docs/Web/HTTP/Headers/Transfer-Encoding
def _get_encodings(self):
encodings = []
if isinstance(self, HTTPResponse):
if self.Transfer_Encoding:
encodings += [plain_str(x).strip().lower() for x in
plain_str(self.Transfer_Encoding).split(",")]
if self.Content_Encoding:
encodings += [plain_str(x).strip().lower() for x in
plain_str(self.Content_Encoding).split(",")]
return encodings
def hashret(self):
# The only field both Answers and Responses have in common
return self.Http_Version
def post_dissect(self, s):
if not conf.contribs["http"]["auto_compression"]:
return s
encodings = self._get_encodings()
# Un-chunkify
if "chunked" in encodings:
data = b""
while s:
length, _, body = s.partition(b"\r\n")
try:
length = int(length, 16)
except ValueError:
# Not a valid chunk. Ignore
break
else:
load = body[:length]
if body[length:length + 2] != b"\r\n":
# Invalid chunk. Ignore
break
s = body[length + 2:]
data += load
if not s:
s = data
# Decompress
try:
if "deflate" in encodings:
import zlib
s = zlib.decompress(s)
elif "gzip" in encodings:
s = gzip_decompress(s)
elif "compress" in encodings:
import lzw
s = lzw.decompress(s)
except Exception:
# Cannot decompress - probably incomplete data
pass
return s
def post_build(self, pkt, pay):
if not conf.contribs["http"]["auto_compression"]:
return pkt + pay
encodings = self._get_encodings()
# Compress
if "deflate" in encodings:
import zlib
pay = zlib.compress(pay)
elif "gzip" in encodings:
pay = gzip_compress(pay)
elif "compress" in encodings:
import lzw
pay = lzw.compress(pay)
return pkt + pay
def self_build(self, field_pos_list=None):
''' Takes an HTTPRequest or HTTPResponse object, and creates its
string representation.'''
if not isinstance(self.underlayer, HTTP):
warning(
"An HTTPResponse/HTTPRequest should always be below an HTTP"
)
p = b""
# Walk all the fields, in order
for f in self.fields_desc:
if f.name == "Unknown_Headers":
continue
# Get the field value
val = self.getfieldval(f.name)
if not val:
# Not specified. Skip
continue
# Fields used in the first line have a space as a separator,
# whereas headers are terminated by a new line
if isinstance(self, HTTPRequest):
if f.name in ['Method', 'Path']:
separator = b' '
else:
separator = b'\r\n'
elif isinstance(self, HTTPResponse):
if f.name in ['Http_Version', 'Status_Code']:
separator = b' '
else:
separator = b'\r\n'
# Add the field into the packet
p = f.addfield(self, p, val + separator)
# Handle Unknown_Headers
if self.Unknown_Headers:
headers_text = b""
for name, value in six.iteritems(self.Unknown_Headers):
headers_text += _header_line(name, value) + b"\r\n"
p = self.get_field("Unknown_Headers").addfield(
self, p, headers_text
)
# The packet might be empty, and in that case it should stay empty.
if p:
# Add an additional line after the last header
p = f.addfield(self, p, b'\r\n')
return p
def guess_payload_class(self, payload):
"""Detect potential payloads
"""
if self.Connection and b"Upgrade" in self.Connection:
from scapy.contrib.http2 import H2Frame
return H2Frame
return super(_HTTPContent, self).guess_payload_class(payload)
class _HTTPHeaderField(StrField):
"""Modified StrField to handle HTTP Header names"""
__slots__ = ["real_name"]
def __init__(self, name, default):
self.real_name = name
name = _strip_header_name(name)
StrField.__init__(self, name, default, fmt="H")
def _generate_headers(*args):
"""Generate the header fields based on their name"""
# Order headers
all_headers = []
for headers in args:
all_headers += headers
# Generate header fields
results = []
for h in sorted(all_headers):
results.append(_HTTPHeaderField(h, None))
return results
# Create Request and Response packets
class HTTPRequest(_HTTPContent):
name = "HTTPRequest"
fields_desc = [
# First line
_HTTPHeaderField("Method", "GET"),
_HTTPHeaderField("Path", "/"),
_HTTPHeaderField("Http-Version", "HTTP/1.1"),
# Headers
] + (
_generate_headers(
GENERAL_HEADERS,
REQUEST_HEADERS,
COMMON_UNSTANDARD_GENERAL_HEADERS,
COMMON_UNSTANDARD_REQUEST_HEADERS
)
) + [
_HTTPHeaderField("Unknown-Headers", None),
]
def do_dissect(self, s):
"""From the HTTP packet string, populate the scapy object"""
first_line, body = _dissect_headers(self, s)
try:
Method, Path, HTTPVersion = re.split(br"\s+", first_line, 2)
self.setfieldval('Method', Method)
self.setfieldval('Path', Path)
self.setfieldval('Http_Version', HTTPVersion)
except ValueError:
pass
if body:
self.raw_packet_cache = s[:-len(body)]
else:
self.raw_packet_cache = s
return body
def mysummary(self):
return self.sprintf(
"%HTTPRequest.Method% %HTTPRequest.Path% "
"%HTTPRequest.Http_Version%"
)
# TODO: decide to keep or not
class HTTPResponse(_HTTPContent):
name = "HTTP Response"
fields_desc = [
# First line
_HTTPHeaderField("Http-Version", "HTTP/1.1"),
_HTTPHeaderField("Status-Code", "200"),
_HTTPHeaderField("Reason-Phrase", "OK"),
# Headers
] + (
_generate_headers(
GENERAL_HEADERS,
RESPONSE_HEADERS,
COMMON_UNSTANDARD_GENERAL_HEADERS,
COMMON_UNSTANDARD_RESPONSE_HEADERS
)
) + [
_HTTPHeaderField("Unknown-Headers", None),
]
def answers(self, other):
return HTTPRequest in other
def do_dissect(self, s):
''' From the HTTP packet string, populate the scapy object '''
first_line, body = _dissect_headers(self, s)
try:
HTTPVersion, Status, Reason = re.split(br"\s+", first_line, 2)
self.setfieldval('Http_Version', HTTPVersion)
self.setfieldval('Status_Code', Status)
self.setfieldval('Reason_Phrase', Reason)
except ValueError:
pass
if body:
self.raw_packet_cache = s[:-len(body)]
else:
self.raw_packet_cache = s
return body
def mysummary(self):
return self.sprintf(
"%HTTPResponse.Http_Version% %HTTPResponse.Status_Code% "
"%HTTPResponse.Reason_Phrase%"
)
class HTTP(Packet):
name = "HTTP 1"
fields_desc = []
show_indent = 0
@classmethod
def dispatch_hook(cls, _pkt=None, *args, **kargs):
if _pkt and len(_pkt) >= 9:
from scapy.contrib.http2 import _HTTP2_types, H2Frame
# To detect a valid HTTP2, we check that the type is correct
# that the Reserved bit is set and length makes sense.
while _pkt:
if len(_pkt) < 9:
# Invalid total length
return cls
if ord(_pkt[3:4]) not in _HTTP2_types:
# Invalid type
return cls
length = struct.unpack("!I", b"\0" + _pkt[:3])[0] + 9
if length > len(_pkt):
# Invalid length
return cls
sid = struct.unpack("!I", _pkt[5:9])[0]
if sid >> 31 != 0:
# Invalid Reserved bit
return cls
_pkt = _pkt[length:]
return H2Frame
return cls
# tcp_reassemble is used by TCPSession in session.py
@classmethod
def tcp_reassemble(cls, data, metadata):
detect_end = metadata.get("detect_end", None)
is_unknown = metadata.get("detect_unknown", True)
if not detect_end or is_unknown:
metadata["detect_unknown"] = False
http_packet = HTTP(data)
# Detect packing method
if not isinstance(http_packet.payload, _HTTPContent):
return http_packet
length = http_packet.Content_Length
if length is not None:
# The packet provides a Content-Length attribute: let's
# use it. When the total size of the frags is high enough,
# we have the packet
length = int(length)
# Subtract the length of the "HTTP*" layer
if http_packet.payload.payload or length == 0:
http_length = len(data) - len(http_packet.payload.payload)
detect_end = lambda dat: len(dat) - http_length >= length
else:
# The HTTP layer isn't fully received.
detect_end = lambda dat: False
metadata["detect_unknown"] = True
else:
# It's not Content-Length based. It could be chunked
encodings = http_packet[HTTP].payload._get_encodings()
chunked = ("chunked" in encodings)
if chunked:
detect_end = lambda dat: dat.endswith(b"\r\n\r\n")
else:
# If neither Content-Length nor chunked is specified,
# it means it's the TCP packet that contains the data,
# or that the information hasn't been given yet.
detect_end = lambda dat: metadata.get("tcp_end", False)
metadata["detect_unknown"] = True
metadata["detect_end"] = detect_end
if detect_end(data):
return http_packet
else:
if detect_end(data):
http_packet = HTTP(data)
return http_packet
def guess_payload_class(self, payload):
"""Decides if the payload is an HTTP Request or Response, or
something else.
"""
try:
prog = re.compile(
br"^(?:OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) "
br"(?:.+?) "
br"HTTP/\d\.\d$"
)
crlfIndex = payload.index(b"\r\n")
req = payload[:crlfIndex]
result = prog.match(req)
if result:
return HTTPRequest
else:
prog = re.compile(br"^HTTP/\d\.\d \d\d\d .*$")
result = prog.match(req)
if result:
return HTTPResponse
except ValueError:
# Anything that isn't HTTP but on port 80
pass
return Raw
class GenevaHTTPRequest():
"""
Defines a Geneva HTTP request, where we can replace, set, delete, or insert to existing fields.
"""
def __init__(self, content):
"""
content: Raw string of the request (Bytes)
Creates a Geneva HTTP request
"""
self.original_content = content # Save off the original just in case
self.parsed_content = HTTPRequest(content)
def replace(self, header, index, content):
# replace contents at index
if header not in self.parsed_content["HTTPRequest"].fields:
# TODO: throw some sort of error, this header doesn't exist
return None
if index+len(content) > len(self.parsed_content["HTTPRequest"].fields[header]):
# TODO: throw some sort of error, this index is too large
return None
self.parsed_content["HTTPRequest"].fields[header] = self.parsed_content["HTTPRequest"].fields[header][0:index] \
+ content + \
self.parsed_content["HTTPRequest"].fields[header][index+len(content):]
return str(self)
def delete(self, header, index, num):
# delete num characters from header beginning at index
if header not in self.parsed_content["HTTPRequest"].fields:
# TODO: throw some sort of error, this header doesn't exist
return None
if index+num > len(self.parsed_content["HTTPRequest"].fields[header]):
# TODO: throw some sort of error, this index is too large
return None
self.parsed_content["HTTPRequest"].fields[header] = self.parsed_content["HTTPRequest"].fields[header][0:index] \
+ self.parsed_content["HTTPRequest"].fields[header][index+num:]
return str(self)
def set_header(self, header, content):
# Completely replaces a header with content. We don't care if the
# field already exists.
self.parsed_content["HTTPRequest"].fields[header] = content
return str(self)
def __str__(self):
return str(self.parsed_content)

View File

@ -6,6 +6,8 @@ import urllib.parse
from scapy.all import IP, RandIP, UDP, DNS, DNSQR, Raw, TCP, fuzz
from actions.http import HTTPRequest
class Layer():
"""
@ -601,6 +603,81 @@ class UDPLayer(Layer):
}
class HTTPRequestLayer(Layer):
"""
Defines an interface to access parsed HTTP fields
"""
name = "HTTPRequest"
protocol = HTTPRequest
_fields = [
"Method",
"Path",
"Http_Version",
"Host"
]
fields = _fields
def __init__(self, layer):
"""
Initializes the HTTP layer.
"""
Layer.__init__(self, layer)
self.request = HTTPRequest(bytes(layer)) # TODO: I dont like this
self.getters = {
'load' : self.get_load,
}
self.setters = {
'load' : self.set_load,
}
self.generators = {
'load' : self.gen_load,
}
def get(self, field):
"""
Override get, since the HTTPRequest doesn't immediately make its fields known
"""
assert field in self.fields
if field in self.getters:
return self.getters[field](field)
return getattr(self.request, field)
def set(self, packet, field, value):
"""
Override get, since the HTTPRequest doesn't immediately make its fields known
"""
print('entering set, but I dont like this')
print(packet)
print(field)
print(value)
assert field in self.fields
base = field.split("-")[0]
if field in self.setters:
self.setters[field](packet, field, value)
# Dual field accessors are fields that require two pieces of information
# to retrieve them (for example, "options-eol"). These are delimited by
# a dash "-".
elif "-" in field and base in self.setters:
self.setters[base](packet, field, value)
else:
setattr(self.layer, field, value)
# Request the packet be reparsed to confirm the value is stable
# XXX Temporarily disabling the reconstitution check due to scapy bug (#2034)
#assert bytes(self.protocol(bytes(self.layer))) == bytes(self.layer)
@classmethod
def name_matches(cls, name):
"""
Override the name parsing to check for HTTP REQUEST here.
"""
return name.upper() in ["HTTPREQUEST"]
class DNSLayer(Layer):
"""
Defines an interface to access DNS header fields.

View File

@ -3,13 +3,18 @@ import random
import actions.layer
from scapy.layers.http import HTTP as ScapyHTTP, HTTPRequest as ScapyHTTPRequest
from scapy.all import IP, TCP
from actions.http import HTTPRequest as GenevaHTTPRequest
_SUPPORTED_LAYERS = [
actions.layer.IPLayer,
actions.layer.TCPLayer,
actions.layer.UDPLayer,
actions.layer.DNSLayer,
actions.layer.DNSQRLayer
actions.layer.DNSQRLayer,
#actions.layer.HTTPLayer,
actions.layer.HTTPRequestLayer
]
SUPPORTED_LAYERS = _SUPPORTED_LAYERS
@ -23,7 +28,13 @@ class Packet():
"""
Initializes the packet object.
"""
self.packet = packet
if(ScapyHTTPRequest in packet.layers()):
# We have a ScapyHTTPRequest. Instead, let's convert to our HttpRequest.
# Overwrite the current ScapyHTTPRequest with a GenevaHTTPRequest
self.packet = packet
self.packet[ScapyHTTPRequest] = GenevaHTTPRequest(bytes(packet[ScapyHTTPRequest]))
else:
self.packet = packet
self.layers = self.setup_layers()
self.sleep = 0
@ -131,7 +142,7 @@ class Packet():
"""
layers = {}
for layer in self.read_layers():
layers[layer.name.upper()] = layer
layers[layer.name] = layer
return layers
def copy(self):
@ -179,7 +190,11 @@ class Packet():
if self.haslayer("TCP"):
del self.packet["TCP"].chksum
return self.layers[str_protocol].set(self.packet, field, value)
lay = self.layers[str_protocol].set(self.packet, field, value)
print(lay)
return lay
def get(self, str_protocol, field):
"""

View File

@ -85,4 +85,5 @@ class Strategy(object):
# If no action tree was applicable, send the packet unimpeded
if not ran:
packets_to_send = [packet]
return packets_to_send

View File

@ -15,44 +15,82 @@ import actions.utils
from actions.layer import DNSLayer
import random
from actions.http import HTTPRequest as HTTPRequest
import urllib.parse
import string
# All supported tamper primitives
SUPPORTED_PRIMITIVES = ["corrupt", "replace", "add", "compress"]
SUPPORTED_PRIMITIVES = ["corrupt", "replace", "add", "compress", "insert", "delete"]
class TamperAction(Action):
"""
Defines the TamperAction for Geneva.
"""
def __init__(self, environment_id=None, field=None, tamper_type=None, tamper_value=None, tamper_proto="TCP"):
def __init__(self, environment_id=None, field=None, tamper_type=None, tamper_value=None, tamper_proto="TCP", start_index=None, end_index=None, encoded_payload=None):
Action.__init__(self, "tamper", "both")
self.field = field
self.tamper_value = tamper_value
self.tamper_proto = actions.utils.string_to_protocol(tamper_proto)
self.tamper_proto_str = tamper_proto
self.tamper_type = tamper_type
self.start_index = start_index
self.end_index = end_index
self.encoded_payload = encoded_payload
if encoded_payload:
self.decoded_payload = bytes(urllib.parse.unquote(encoded_payload), "UTF-8")
def tamper(self, packet, logger):
"""
Edits a given packet according to the action settings.
"""
# Return packet untouched if not applicable
if not packet.haslayer(self.tamper_proto_str):
return packet
# Retrieve the old value of the field for logging purposes
old_value = packet.get(self.tamper_proto_str, self.field)
new_value = self.tamper_value
# If corrupting the packet field, generate a value for it
try:
if self.tamper_type == "corrupt":
new_value = packet.gen(self.tamper_proto_str, self.field)
if self.tamper_proto == HTTPRequest:
packet = corrupt(packet, self.field, self.start_index, self.end_index)
del packet["IP"].chksum
del packet["IP"].len
del packet["TCP"].chksum
del packet["TCP"].dataofs
return packet
else:
new_value = packet.gen(self.tamper_proto_str, self.field)
elif self.tamper_type == "add":
new_value = int(self.tamper_value) + int(old_value)
elif self.tamper_type == "compress":
return packet.dns_decompress(logger)
elif self.tamper_type == "insert":
packet = insert(packet, self.field, self.start_index, self.decoded_payload)
del packet["IP"].chksum
del packet["IP"].len
del packet["TCP"].chksum
del packet["TCP"].dataofs
return packet
elif self.tamper_type == "replace":
packet = replace(packet, self.field, self.start_index, self.decoded_payload)
del packet["IP"].chksum
del packet["IP"].len
del packet["TCP"].chksum
del packet["TCP"].dataofs
return packet
elif self.tamper_type == "delete":
packet = delete(packet, self.field, self.start_index, self.end_index)
return packet
except NotImplementedError:
# If a primitive does not support the type of packet given
return packet
@ -62,7 +100,7 @@ class TamperAction(Action):
logger.debug(" - Tampering %s field `%s` (%s) by %s (to %s)" %
(self.tamper_proto_str, self.field, str(old_value), self.tamper_type, str(new_value)))
print("about to call set")
packet.set(self.tamper_proto_str, self.field, new_value)
return packet
@ -82,11 +120,19 @@ class TamperAction(Action):
"""
s = Action.__str__(self)
if self.tamper_type == "corrupt":
s += "{%s:%s:%s}" % (self.tamper_proto_str, self.field, self.tamper_type)
elif self.tamper_type in ["replace", "add"]:
s += "{%s:%s:%s:%s}" % (self.tamper_proto_str, self.field, self.tamper_type, self.tamper_value)
if self.tamper_proto == HTTPRequest:
s += "{%s:%s:%s:%s}" % (self.tamper_proto_str, self.field, self.tamper_type, str(self.start_index) + "-" + str(self.end_index))
else:
s += "{%s:%s:%s}" % (self.tamper_proto_str, self.field, self.tamper_type)
elif self.tamper_type in ["replace", "add", "insert"]:
if self.tamper_proto == HTTPRequest:
s += "{%s:%s:%s:%s:%s}" % (self.tamper_proto_str, self.field, self.tamper_type, str(self.start_index), self.encoded_payload)
else:
s += "{%s:%s:%s:%s}" % (self.tamper_proto_str, self.field, self.tamper_type, self.tamper_value)
elif self.tamper_type == "compress":
s += "{%s:%s:compress}" % ("DNS", "qd", )
elif self.tamper_type == "delete":
s += "{%s:%s:%s:%s}" % (self.tamper_proto_str, self.field, self.tamper_type, str(self.start_index) + "-" + str(self.end_index))
return s
@ -105,28 +151,129 @@ class TamperAction(Action):
# Count the number of params in this given string
num_parameters = string.count(":")
# If num_parameters is greater than 3, it's not a valid tamper action
if num_parameters > 3 or num_parameters < 2:
# If num_parameters is greater than 4, it's not a valid tamper action
if num_parameters > 4 or num_parameters < 2:
msg = "Cannot parse tamper action %s" % string
logger.error(msg)
raise Exception(msg)
params = string.split(":")
if num_parameters == 3:
self.tamper_proto_str, self.field, self.tamper_type, self.tamper_value = params
if num_parameters == 4:
# HTTP replace or insert
self.tamper_proto_str, self.field, self.tamper_type, self.start_index, self.encoded_payload = params
self.start_index = int(self.start_index)
self.tamper_proto = actions.utils.string_to_protocol(self.tamper_proto_str)
if "options" in self.field:
if not self.tamper_value:
self.tamper_value = '' # An empty string instead of an empty byte literal
self.decoded_payload = bytes(urllib.parse.unquote(self.encoded_payload), "UTF-8")
# tamper_value might be parsed as a string despite being an integer in most cases.
# Try to parse it out here
try:
if "load" not in self.field:
self.tamper_value = int(self.tamper_value)
except:
pass
else:
elif num_parameters == 3:
# HTTP corrupt or delete could be here, check for those first
self.tamper_proto_str = params[0]
self.tamper_proto = actions.utils.string_to_protocol(self.tamper_proto_str)
if self.tamper_proto_str == "HTTPRequest":
self.field = params[1]
self.tamper_type = params[2]
indices = params[3].split('-')
self.start_index = int(indices[0])
self.end_index = int(indices[1])
else:
self.tamper_proto_str, self.field, self.tamper_type, self.tamper_value = params
if "options" in self.field:
if not self.tamper_value:
self.tamper_value = '' # An empty string instead of an empty byte literal
# tamper_value might be parsed as a string despite being an integer in most cases.
# Try to parse it out here
try:
if "load" not in self.field:
self.tamper_value = int(self.tamper_value)
except:
pass
elif num_parameters == 2:
self.tamper_proto_str, self.field, self.tamper_type = params
self.tamper_proto = actions.utils.string_to_protocol(self.tamper_proto_str)
return True
def insert(packet, header, index, content):
"""
Helper method to insert content into packet[header][index]
"""
if header not in packet["HTTPRequest"].fields:
# TODO: throw some sort of error, this header doesn't exist
return None
if index > len(packet["HTTPRequest"].fields[header]):
# TODO: throw some sort of error, this index is too large
return None
packet["HTTPRequest"].fields[header] = packet["HTTPRequest"].fields[header][0:index] \
+ content + \
packet["HTTPRequest"].fields[header][index:]
return packet
def replace(packet, header, index, content):
"""
Helper method to replace packet[header][index] with content
"""
if header not in packet["HTTPRequest"].fields:
# TODO: throw some sort of error, this header doesn't exist
return None
if index+len(content) > len(packet["HTTPRequest"].fields[header]):
# TODO: throw some sort of error, this index is too large
return None
packet["HTTPRequest"].fields[header] = packet["HTTPRequest"].fields[header][0:index] \
+ content + \
packet["HTTPRequest"].fields[header][index+len(content):]
return packet
def delete(packet, header, start_index, end_index):
"""
Helper method to remove the characters at header[start_index] to header[end_index]
"""
if header not in packet["HTTPRequest"].fields:
# TODO: throw some sort of error, this header doesn't exist
return None
if end_index+1 > len(packet["HTTPRequest"].fields[header]):
# TODO: throw some sort of error, this index is too large
return None
packet["HTTPRequest"].fields[header] = packet["HTTPRequest"].fields[header][0:start_index] \
+ packet["HTTPRequest"].fields[header][end_index+1:]
return packet
def corrupt(packet, header, start_index, end_index):
"""
Helper method to remove the characters at header[start_index] to header[end_index]
"""
print("in cor")
if header not in packet["HTTPRequest"].fields:
# TODO: throw some sort of error, this header doesn't exist
return None
if end_index+1 > len(packet["HTTPRequest"].fields[header]):
# TODO: throw some sort of error, this index is too large
return None
print("in cor")
try:
old_field = packet["HTTPRequest"].fields[header]
tampered_field = packet["HTTPRequest"].fields[header][0:start_index]
for i in range(0, end_index - start_index + 1):
# Ensure we're getting a new character
new_character = bytes(random.choice(string.printable), "UTF-8")
while(new_character == old_field[i]):
new_character = bytes(random.choice(string.printable), "UTF-8")
tampered_field = tampered_field + new_character
tampered_field = tampered_field + packet["HTTPRequest"].fields[header][end_index+1:]
print("settings tampered field to:")
print(tampered_field)
packet["HTTPRequest"].fields[header] = bytes(tampered_field, "UTF-8")
except Exception as e:
print(e)
return packet

View File

@ -14,6 +14,7 @@ import actions.trigger
import actions.packet
from scapy.all import TCP, IP, UDP, rdpcap
from actions.http import HTTPRequest
import netifaces
@ -153,6 +154,8 @@ def string_to_protocol(protocol):
return IP
elif protocol.upper() == "UDP":
return UDP
elif protocol.upper() == "HTTPREQUEST":
return HTTPRequest
def get_id():

View File

@ -177,10 +177,14 @@ class WindowsEngine(GenericEngine):
try:
self.logger.debug("Sending packet %s", str(packet))
# Convert the packet to a bytearray so memoryview can edit the underlying memory
print("About to cnvert")
pack = bytearray(bytes(packet.packet))
print("about to send")
print(pack)
# Don't recalculate checksum since sometimes we will have already changed it
self.divert.send(pydivert.Packet(memoryview(pack), self.interface, dir), recalculate_checksum=False)
except Exception:
except Exception as e:
print(e)
self.logger.exception("Error in engine mysend.")
def handle_outbound_packet(self, divert_packet):
@ -197,7 +201,7 @@ class WindowsEngine(GenericEngine):
# Send all of the packets we've collected to send
for out_packet in packets_to_send:
self.mysend(out_packet, Direction.OUTBOUND)
self.mysend(out_packet, Direction.OUTBOUND)
def handle_inbound_packet(self, divert_packet):
"""