├── .gitignore ├── malleable ├── __init__.py ├── utility.py ├── profile.py ├── implementation.py ├── transformation.py └── transaction.py ├── test.py ├── LICENSE ├── amazon.profile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /malleable/__init__.py: -------------------------------------------------------------------------------- 1 | from utility import MalleableError, MalleableUtil, MalleableObject 2 | from transformation import Transform, Terminator, Container 3 | from transaction import MalleableRequest, MalleableResponse, Transaction 4 | from implementation import Get, Post, Stager 5 | from profile import Profile 6 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import malleable 2 | 3 | try: 4 | p = malleable.Profile() 5 | p.ingest("amazon.profile") 6 | if p.validate(): 7 | request = p.get.construct_client("mydomain.sample", "mydata") 8 | print request.url, request.headers, request.body 9 | except MalleableError as e: 10 | print str(e) 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 johneiser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /amazon.profile: -------------------------------------------------------------------------------- 1 | # 2 | # Amazon browsing traffic profile 3 | # 4 | # Author: @harmj0y 5 | # 6 | 7 | set sleeptime "5000"; 8 | set jitter "0"; 9 | set maxdns "255"; 10 | set useragent "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"; 11 | 12 | http-get { 13 | 14 | set uri "/s/ref=nb_sb_noss_1/167-3294888-0262949/field-keywords=books"; 15 | 16 | client { 17 | 18 | header "Accept" "*/*"; 19 | header "Host" "www.amazon.com"; 20 | 21 | metadata { 22 | base64; 23 | prepend "session-token="; 24 | prepend "skin=noskin;"; 25 | append "csm-hit=s-24KU11BB82RZSYGJ3BDK|1419899012996"; 26 | header "Cookie"; 27 | } 28 | } 29 | 30 | server { 31 | 32 | header "Server" "Server"; 33 | header "x-amz-id-1" "THKUYEZKCKPGY5T42PZT"; 34 | header "x-amz-id-2" "a21yZ2xrNDNtdGRsa212bGV3YW85amZuZW9ydG5rZmRuZ2tmZGl4aHRvNDVpbgo="; 35 | header "X-Frame-Options" "SAMEORIGIN"; 36 | header "Content-Encoding" "gzip"; 37 | 38 | output { 39 | print; 40 | } 41 | } 42 | } 43 | 44 | http-post { 45 | 46 | set uri "/N4215/adj/amzn.us.sr.aps"; 47 | 48 | client { 49 | 50 | header "Accept" "*/*"; 51 | header "Content-Type" "text/xml"; 52 | header "X-Requested-With" "XMLHttpRequest"; 53 | header "Host" "www.amazon.com"; 54 | 55 | parameter "sz" "160x600"; 56 | parameter "oe" "oe=ISO-8859-1;"; 57 | 58 | id { 59 | parameter "sn"; 60 | } 61 | 62 | parameter "s" "3717"; 63 | parameter "dc_ref" "http%3A%2F%2Fwww.amazon.com"; 64 | 65 | output { 66 | base64; 67 | print; 68 | } 69 | } 70 | 71 | server { 72 | 73 | header "Server" "Server"; 74 | header "x-amz-id-1" "THK9YEZJCKPGY5T42OZT"; 75 | header "x-amz-id-2" "a21JZ1xrNDNtdGRsa219bGV3YW85amZuZW9zdG5rZmRuZ2tmZGl4aHRvNDVpbgo="; 76 | header "X-Frame-Options" "SAMEORIGIN"; 77 | header "x-ua-compatible" "IE=edge"; 78 | 79 | output { 80 | print; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /malleable/utility.py: -------------------------------------------------------------------------------- 1 | from pyparsing import * 2 | 3 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 4 | # UTILITY 5 | # 6 | # Defining helper functionality to optimize code quality. 7 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 8 | 9 | class MalleableError(Exception): 10 | """Custom exception class used to identify local exceptions.""" 11 | 12 | @classmethod 13 | def throw(cls, clss, func, message): 14 | """Throw a MalleableError. 15 | 16 | Args: 17 | clss (class): The class containing the exception. 18 | func (str): The function in which the exception occurred. 19 | message (str): A description of the exception. 20 | 21 | Raises: 22 | MalleableError: When called. 23 | """ 24 | raise(cls("%s::%s - %s" % (clss.__name__, func, message))) 25 | 26 | class MalleableUtil(object): 27 | """Custom utility class used to provide helper functionality.""" 28 | 29 | @staticmethod 30 | def to_hex(byte): 31 | """Convert a byte into a hex character. 32 | 33 | Args: 34 | byte (char) 35 | 36 | Returns: 37 | str: Byte as a hex character. 38 | """ 39 | return hex(ord(byte)) if byte else None 40 | 41 | @staticmethod 42 | def from_hex(hex): 43 | """Convert a hex character into a byte. 44 | 45 | Args: 46 | hex (str): A single hex character. 47 | 48 | Returns: 49 | char: byte. 50 | """ 51 | return hex.split("0x")[-1].zfill(2).decode("hex") if hex else None 52 | 53 | class MalleableObject(object): 54 | """Custom object class used to implement consistent functionality.""" 55 | 56 | SEMICOLON = Suppress(";") 57 | FIELD = Word(alphanums + "_-") 58 | VALUE = (QuotedString("\"", escChar="\\") | QuotedString("'", escChar="\\")) 59 | COMMENT = Suppress("#") + Suppress(restOfLine) 60 | 61 | def __init__(self): 62 | """Constructor for a Malleable object.""" 63 | self._defaults() 64 | 65 | def _defaults(self): 66 | """Default initialization for a Malleable object.""" 67 | pass 68 | 69 | def _clone(self): 70 | """Deep copy of a Malleable object. 71 | 72 | Returns: 73 | MalleableObject 74 | """ 75 | return self.__class__() 76 | 77 | def _serialize(self): 78 | """Serialize a Malleable object (json). 79 | 80 | Returns: 81 | dict (str, obj): Serialized data (json) 82 | """ 83 | return {} 84 | 85 | @classmethod 86 | def _deserialize(cls, data): 87 | """Deserialize data (json) into a Malleable object. 88 | 89 | Args: 90 | data (dict (str, obj)): Serialized data (json) 91 | 92 | Returns: 93 | Malleable object 94 | """ 95 | return cls() 96 | 97 | @classmethod 98 | def _pattern(cls): 99 | """Define the pattern to recognize this object while parsing a file. 100 | 101 | Returns: 102 | pyparsing object 103 | """ 104 | return None 105 | 106 | def _parse(self, data): 107 | """Store the information from a parsed pyparsing result. 108 | 109 | Args: 110 | data: pyparsing data 111 | """ 112 | pass 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MalleableC2Parser 2 | 3 | A [**Malleable Command and Control Profile**](https://www.cobaltstrike.com/help-malleable-c2) is a "simple program that specifies how to transform data and store it in a transaction", and is a key feature of [**Cobal Strike**](https://www.cobaltstrike.com/)'s Beacon payload. This library is an attempt to abstract that functionality out so that other toolsets may use the same files to define their own communication profiles. 4 | 5 | ## Usage 6 | 7 | ``` 8 | import malleable 9 | try: 10 | p = malleable.Profile() 11 | p.ingest("amazon.profile") 12 | if p.validate(): 13 | request = p.get.construct_client("mydomain.sample", "mydata") 14 | print request.url, request.headers, request.body 15 | except malleable.MalleableError as e: 16 | print str(e) 17 | ``` 18 | 19 | ## Architecture 20 | 21 | ### Profile 22 | 23 | The `Profile` houses all the functionality of the Malleable C2 profile and is capable of ingesting and validating profiles. A standard Malleable C2 profile contains a `Get Implementation`, a `Post Implementation`, and possibly a `Stager Implementation`, as well as several global variables like `sleeptime`, `jitter`, and `useragent`. 24 | 25 | ### Implementation 26 | 27 | An `Implementation` is the specific instantiation of an HTTP client-server `Transaction`, and there are three defined: `Get`, `Post`, and `Stager`. Each `Implementation` has its own storage paradigm and purpose within the communication profile. 28 | 29 | - Get: Fetch tasking from the C2 30 | - Client: metadata (Session metadata) 31 | - Server: output (Beacon's tasks) 32 | - Post: Return results to the C2 33 | - Client: id (Session ID), output (Beacon's responses) 34 | - Server: output (Empty) 35 | - Stager: Download a payload stage 36 | - Client: metadata (Empty) 37 | - Server: output (Encoded payload stage) 38 | 39 | ### Transaction 40 | 41 | A `Transaction` defines the core components of an interaction between a web client request and a web server response. As such, a `Transaction` houses a `Client` and `Server` object, each holding the appropriate components included in their part of the transaction. 42 | 43 | - Client: url, verb scheme, host, port, path, parameters, headers, body 44 | 45 | - Server: code, headers, body 46 | 47 | Each `Client` and `Server` object of a `Transaction` also includes the ability to *store* and *extract* encoded data within its structure, houseing the true value of a Malleable C2 profile. 48 | 49 | ### Transformation 50 | 51 | This group of classes defines the model through which arbitrary data can undergo a sequence of reversable transformations. A `Transform` houses the arbitrary functionality of a reversable transformation, and the following are defined: 52 | 53 | - Append 54 | - Base64 55 | - Base64Url 56 | - Mask 57 | - Netbios 58 | - Netbiosu 59 | - Prepend 60 | 61 | A `Terminator` houses the arbitrary functionality of a reversable storage mechanism, and the following are defined: 62 | 63 | - Print 64 | - Header 65 | - Parameter 66 | - UriAppend 67 | 68 | And finally, a `Container` houses a sequence of `Transforms` and their defined `Terminator`. For example, a `Get Implementation` might include the `metadata Container`, which houses the `Base64Url Transform` and the `UriAppend Terminator`. This means that the metadata to be sent in a GET request to the C2 server will first be Base64 encoded and url encoded, then stored at the end of the url. The server will then retrieve the encoded data from the end of the url and proceed to url decode and base64 decode it. 69 | -------------------------------------------------------------------------------- /malleable/profile.py: -------------------------------------------------------------------------------- 1 | import os, string 2 | from pyparsing import * 3 | from utility import MalleableError, MalleableUtil, MalleableObject 4 | from transformation import Transform, Terminator, Container 5 | from transaction import MalleableRequest, MalleableResponse, Transaction 6 | from implementation import Get, Post, Stager 7 | 8 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 9 | # PROFILE 10 | # 11 | # Defining the top-layer object to be interacted with. 12 | # 13 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 14 | 15 | class Profile(MalleableObject): 16 | """A class housing all the functionality of a Malleable C2 profile. 17 | 18 | Attributes: 19 | get (Get (Transaction)) 20 | post (Post (Transaction)) 21 | stager (Stager (Transaction)) 22 | 23 | useragent (str, property) 24 | sleeptime (int) [milliseconds] 25 | jitter (int) [percent] 26 | """ 27 | 28 | def _defaults(self): 29 | """Default initialization for the Profile object.""" 30 | super(Profile, self)._defaults() 31 | self.get = Get() 32 | self.post = Post() 33 | self.stager = Stager() 34 | self.sleeptime = 60000 35 | self.jitter = 0 36 | 37 | def _clone(self): 38 | """Deep copy of the Profile object. 39 | 40 | Returns: 41 | Profile 42 | """ 43 | new = super(Profile, self)._clone() 44 | new.get = self.get._clone() 45 | new.post = self.post._clone() 46 | new.stager = self.stager._clone() 47 | new.sleeptime = self.sleeptime 48 | new.jitter = self.jitter 49 | return new 50 | 51 | def _serialize(self): 52 | """Serialize the Profile object. 53 | 54 | Returns: 55 | dict (str, obj): Serialized data (json) 56 | """ 57 | return dict(super(Profile, self)._serialize().items() + { 58 | "get" : self.get._serialize(), 59 | "post" : self.post._serialize(), 60 | "stager" : self.stager._serialize(), 61 | "sleeptime" : self.sleeptime, 62 | "jitter" : self.jitter 63 | }.items()) 64 | 65 | @classmethod 66 | def _deserialize(cls, data): 67 | """Deserialize data into a Profile object. 68 | 69 | Args: 70 | data (dict (str, obj)): Serialized data (json) 71 | 72 | Returns: 73 | Profile object 74 | """ 75 | profile = super(Profile, cls)._deserialize(data) 76 | if data: 77 | try: 78 | profile.get = Get._deserialize(data["get"]) if "get" in data else Get() 79 | profile.post = Post._deserialize(data["post"] if "post" in data else Post()) 80 | profile.stager = Stager._deserialize(data["stager"] if "stager" in data else Stager()) 81 | profile.sleeptime = int(data["sleeptime"]) if "sleeptime" in data else 60000 82 | profile.jitter = int(data["jitter"]) if "jitter" in data else 0 83 | except Exception as e: 84 | MalleableError.throw(cls, "_deserialize", "An error occurred: " + str(e)) 85 | return profile 86 | 87 | @classmethod 88 | def _pattern(cls): 89 | """Define the pattern to recognize a Profile object while parsing a file. 90 | 91 | Returns: 92 | pyparsing object 93 | """ 94 | return ZeroOrMore( 95 | cls.COMMENT | 96 | (Literal("set") + Group(cls.FIELD + cls.VALUE) + cls.SEMICOLON) | 97 | Get._pattern() | 98 | Post._pattern() | 99 | Stager._pattern()) 100 | 101 | def _parse(self, data): 102 | """Store the information from a parsed pyparsing result. 103 | 104 | Args: 105 | data: pyparsing data 106 | """ 107 | if data: 108 | for group in [d for d in data if d]: 109 | for i in range(0, len(group), 2): 110 | item = group[i] 111 | arg = group[i+1] if len(group) > i+1 else None 112 | if item and arg: 113 | if item.lower() == "set" and len(arg) > 1: 114 | key, value = arg[0], arg[1] 115 | if key and value: 116 | setattr(self, key, value) 117 | elif item.lower() == "http-get": 118 | self.get._parse(arg) 119 | elif item.lower() == "http-post": 120 | self.post._parse(arg) 121 | elif item.lower() == "http-stager": 122 | self.stager._parse(arg) 123 | 124 | @property 125 | def useragent(self): 126 | """Get the profile useragent. 127 | 128 | Returns: 129 | str: useragent 130 | """ 131 | return self.get.client.headers["User-Agent"] if "User-Agent" in self.get.client.headers else None 132 | 133 | @useragent.setter 134 | def useragent(self, useragent): 135 | """Set the profile useragent. 136 | 137 | Args: 138 | useragent (str) 139 | """ 140 | self.get.client.headers["User-Agent"] = useragent 141 | self.post.client.headers["User-Agent"] = useragent 142 | self.stager.client.headers["User-Agent"] = useragent 143 | 144 | def validate(self): 145 | """Validate the profile to verify it will succeed when used. 146 | 147 | Returns: 148 | bool: True if no checks fail. 149 | 150 | Raises: 151 | MalleableError: If a check fails. 152 | """ 153 | host = "http://domain.com:80" 154 | data = string.printable 155 | for format, p in [("base", self), ("clone", self._clone()), ("serialized", Profile._deserialize(self._serialize()))]: 156 | test = p.get.construct_client(host, data) 157 | clone = MalleableRequest() 158 | clone.url = test.url 159 | clone.verb = test.verb 160 | clone.headers = test.headers 161 | clone.body = test.body 162 | if self.get.extract_client(clone) != data: 163 | MalleableError.throw(self.__class__, "validate", "Data-integrity check failed: %s-get-client-metadata" % format) 164 | 165 | test = p.get.construct_server(data) 166 | clone = MalleableResponse() 167 | clone.headers = test.headers 168 | clone.body = test.body 169 | if self.get.extract_server(clone) != data: 170 | MalleableError.throw(self.__class__, "validate", "Data-integrity check failed: %s-get-server-output" % format) 171 | 172 | test = p.post.construct_client(host, data, data) 173 | clone = MalleableRequest() 174 | clone.url = test.url 175 | clone.verb = test.verb 176 | clone.headers = test.headers 177 | clone.body = test.body 178 | id, output = self.post.extract_client(clone) 179 | if id != data: 180 | MalleableError.throw(self.__class__, "validate", "Data-integrity check failed: %s-post-client-id" % format) 181 | if output != data: 182 | MalleableError.throw(self.__class__, "validate", "Data-integrity check failed: %s-post-client-output" % format) 183 | 184 | test = p.post.construct_server(data) 185 | clone = MalleableResponse() 186 | clone.headers = test.headers 187 | clone.body = test.body 188 | if self.post.extract_server(clone) != data: 189 | MalleableError.throw(self.__class__, "validate", "Data-integrity check failed: %s-post-server-output" % format) 190 | 191 | test = p.stager.construct_client(host, data) 192 | clone = MalleableRequest() 193 | clone.url = test.url 194 | clone.verb = test.verb 195 | clone.headers = test.headers 196 | clone.body = test.body 197 | if self.stager.extract_client(clone) != data: 198 | MalleableError.throw(self.__class__, "validate", "Data-integrity check failed: %s-stager-client-metadata" % format) 199 | 200 | test = p.stager.construct_server(data) 201 | clone = MalleableResponse() 202 | clone.headers = test.headers 203 | clone.body = test.body 204 | if self.stager.extract_server(clone) != data: 205 | MalleableError.throw(self.__class__, "validate", "Data-integrity check failed: %s-stager-server-output" % format) 206 | 207 | if set(self.get.client.uris).intersection(set(self.post.client.uris)) or \ 208 | set(self.post.client.uris).intersection(set(self.stager.client.uris)) or \ 209 | set(self.stager.client.uris).intersection(set(self.get.client.uris)) or \ 210 | len(self.get.client.uris + (self.post.client.uris if self.post.client.uris else ["/"])) == 0 or \ 211 | len(self.post.client.uris + (self.stager.client.uris if self.stager.client.uris else ["/"])) == 0 or \ 212 | len(self.stager.client.uris + (self.get.client.uris if self.get.client.uris else ["/"])) == 0 or \ 213 | ("/" in self.get.client.uris and len(self.post.client.uris) == 0) or \ 214 | ("/" in self.get.client.uris and len(self.stager.client.uris) == 0) or \ 215 | ("/" in self.post.client.uris and len(self.stager.client.uris) == 0) or \ 216 | ("/" in self.post.client.uris and len(self.get.client.uris) == 0) or \ 217 | ("/" in self.stager.client.uris and len(self.get.client.uris) == 0) or \ 218 | ("/" in self.stager.client.uris and len(self.post.client.uris) == 0): 219 | MalleableError.throw(self.__class__, "validate", "Cannot have duplicate uris: %s - %s - %s" % ( 220 | self.get.client.uris if self.get.client.uris else ["/"], 221 | self.post.client.uris if self.post.client.uris else ["/"], 222 | self.stager.client.uris if self.stager.client.uris else ["/"] 223 | )) 224 | 225 | return True 226 | 227 | def ingest(self, file): 228 | """Ingest a profile file into the Profile object. 229 | 230 | Args: 231 | file (str): Filename to be read and parsed. 232 | """ 233 | if not file or not os.path.isfile(file): 234 | MalleableError.throw(self.__class__, "ingest", "Invalid file: %s" % str(file)) 235 | 236 | content = None 237 | with open(file) as f: 238 | content = f.read() 239 | 240 | if not content: 241 | MalleableError.throw(self.__class__, "ingest", "Empty file: %s" % str(file)) 242 | 243 | self._parse(self._pattern().searchString(content)) 244 | -------------------------------------------------------------------------------- /malleable/implementation.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | from pyparsing import * 3 | from utility import MalleableError, MalleableUtil, MalleableObject 4 | from transformation import Transform, Terminator, Container 5 | from transaction import MalleableRequest, MalleableResponse, Transaction 6 | 7 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 8 | # IMPLEMENTATION 9 | # 10 | # Defining the specific implementations of an interaction 11 | # between a web client request and a web server response. 12 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 13 | 14 | class Get(Transaction): 15 | """The Get implementation of a Transaction. 16 | 17 | The Get Transaction is used to fetch tasking from the server. In a Get Transaction, 18 | the following data is transmitted: 19 | - (Client) metadata (Session metadata) 20 | - (Server) output (Beacon's tasks) 21 | """ 22 | 23 | def _defaults(self): 24 | """Default initialization for the Get Transaction.""" 25 | super(Get, self)._defaults() 26 | self.client.metadata = Container() 27 | self.server.output = Container() 28 | self.client.verb = "GET" 29 | 30 | def _clone(self): 31 | """Deep copy of the Get Transaction. 32 | 33 | Returns: 34 | Get Transaction 35 | """ 36 | new = super(Get, self)._clone() 37 | new.client.metadata = self.client.metadata._clone() 38 | new.server.output = self.server.output._clone() 39 | new.client.verb = self.client.verb 40 | return new 41 | 42 | def _serialize(self): 43 | """Serialize the Get Transaction. 44 | 45 | Returns: 46 | dict (str, obj) 47 | """ 48 | d = super(Get, self)._serialize() 49 | d["client"] = dict((d["client"].items() if "client" in d else []) + { 50 | "metadata" : self.client.metadata._serialize() 51 | }.items()) 52 | d["server"] = dict((d["server"].items() if "server" in d else []) + { 53 | "output" : self.server.output._serialize() 54 | }.items()) 55 | return dict(d.items() + { 56 | 57 | }.items()) 58 | 59 | @classmethod 60 | def _deserialize(cls, data): 61 | """Deserialize data into a Get Transaction. 62 | 63 | Args: 64 | data (dict (str, obj)): Serialized data (json) 65 | 66 | Returns: 67 | Get Transaction 68 | """ 69 | get = super(Get, cls)._deserialize(data) 70 | if data: 71 | get.client.metadata = Container._deserialize(data["client"]["metadata"]) if ("client" in data and "metadata" in data["client"]) else Container() 72 | get.server.output = Container._deserialize(data["server"]["output"]) if ("server" in data and "output" in data["server"]) else Container() 73 | return get 74 | 75 | @classmethod 76 | def _pattern(cls): 77 | """Define the pattern to recognize a Get object while parsing a file. 78 | 79 | Returns: 80 | pyparsing object 81 | """ 82 | return Literal("http-get") + super(Get, cls)._pattern() 83 | 84 | def _parse(self, data): 85 | """Store the information from a parsed pyparsing result. 86 | 87 | Args: 88 | data: pyparsing data 89 | """ 90 | super(Get, self)._parse(data) 91 | if data: 92 | for i in range(0, len(data), 2): 93 | item = data[i] 94 | arg = data[i+1] if len(data) > i+1 else None 95 | if item and arg: 96 | if item.lower() == "client": 97 | for j in range(0, len(arg), 2): 98 | item2 = arg[j] 99 | arg2 = arg[j+1] if len(arg) > j+1 else None 100 | if item2 and arg2: 101 | if item2.lower() == "metadata": 102 | self.client.metadata._parse(arg2) 103 | elif item.lower() == "server": 104 | for j in range(0, len(arg), 2): 105 | item2 = arg[j] 106 | arg2 = arg[j+1] if len(arg) > j+1 else None 107 | if item2 and arg2: 108 | if item2.lower() == "output": 109 | self.server.output._parse(arg2) 110 | 111 | def construct_client(self, host, metadata): 112 | """Construct a Client request using the provided metadata to the provided host. 113 | 114 | Args: 115 | host (str): Host to direct client request to. 116 | metadata (str): Metadata to include in the request. 117 | 118 | Returns: 119 | Transaction.Client: Constructed Client request. 120 | """ 121 | request = self.client._clone() 122 | request.host = host 123 | request.path = self.client.random_uri() 124 | request.store(self.client.metadata.transform(metadata), self.client.metadata.terminator) 125 | return request 126 | 127 | def extract_client(self, request): 128 | """Extract the metadata from the provided MalleableRequest. 129 | 130 | Args: 131 | request (MalleableRequest) 132 | 133 | Returns: 134 | str: metadata 135 | """ 136 | for u in (self.client.uris if self.client.uris else ["/"]): 137 | if u.lower() in request.path.lower(): 138 | metadata = request.extract(self.client, self.client.metadata.terminator) 139 | if metadata: 140 | m = self.client.metadata.transform_r(metadata) 141 | return m 142 | return None 143 | 144 | def construct_server(self, output): 145 | """Construct a Server response using the provided output. 146 | 147 | Args: 148 | output (str): Output to include in the request. 149 | 150 | Returns: 151 | Transaction.Server: Constructed Server response. 152 | """ 153 | response = self.server._clone() 154 | response.store(self.server.output.transform(output), self.server.output.terminator) 155 | return response 156 | 157 | def extract_server(self, response): 158 | """Extract the output from the provided MalleableResponse. 159 | 160 | Args: 161 | response (MalleableResponse) 162 | 163 | Returns: 164 | str: output 165 | """ 166 | output = response.extract(self.server, self.server.output.terminator) 167 | return self.server.output.transform_r(output) if output else None 168 | 169 | class Post(Transaction): 170 | """The Post implementation of a Transaction. 171 | 172 | The Post Transaction is used to exchange information with the server. In a Post Transaction, 173 | the following data is transmitted: 174 | - (Client) id (Session ID) 175 | - (Client) output (Beacon's responses) 176 | - (Server) output (Empty) 177 | """ 178 | 179 | def _defaults(self): 180 | """Default initialization for the Post Transaction.""" 181 | super(Post, self)._defaults() 182 | self.client.id = Container() 183 | self.client.output = Container() 184 | self.server.output = Container() 185 | self.client.verb = "POST" 186 | 187 | def _clone(self): 188 | """Deep copy of the Post Transaction. 189 | 190 | Returns: 191 | Post Transaction 192 | """ 193 | new = super(Post, self)._clone() 194 | new.client.id = self.client.id._clone() 195 | new.client.output = self.client.output._clone() 196 | new.server.output = self.server.output._clone() 197 | new.client.verb = self.client.verb 198 | return new 199 | 200 | def _serialize(self): 201 | """Serialize the Post Transaction. 202 | 203 | Returns: 204 | dict (str, obj) 205 | """ 206 | d = super(Post, self)._serialize() 207 | d["client"] = dict((d["client"].items() if "client" in d else []) + { 208 | "id" : self.client.id._serialize(), 209 | "output" : self.client.output._serialize() 210 | }.items()) 211 | d["server"] = dict((d["server"].items() if "server" in d else []) + { 212 | "output" : self.server.output._serialize() 213 | }.items()) 214 | return dict(d.items() + { 215 | 216 | }.items()) 217 | 218 | @classmethod 219 | def _deserialize(cls, data): 220 | """Deserialize data into a Post Transaction. 221 | 222 | Args: 223 | data (dict (str, obj)): Serialized data (json) 224 | 225 | Returns: 226 | Post Transaction 227 | """ 228 | post = super(Post, cls)._deserialize(data) 229 | if data: 230 | post.client.id = Container._deserialize(data["client"]["id"]) if ("client" in data and "id" in data["client"]) else Container() 231 | post.client.output = Container._deserialize(data["client"]["output"]) if ("client" in data and "output" in data["client"]) else Container() 232 | post.server.output = Container._deserialize(data["server"]["output"]) if ("server" in data and "output" in data["server"]) else Container() 233 | return post 234 | 235 | @classmethod 236 | def _pattern(cls): 237 | """Define the pattern to recognize a Post object while parsing a file. 238 | 239 | Returns: 240 | pyparsing object 241 | """ 242 | return Literal("http-post") + super(Post, cls)._pattern() 243 | 244 | def _parse(self, data): 245 | """Store the information from a parsed pyparsing result. 246 | 247 | Args: 248 | data: pyparsing data 249 | """ 250 | super(Post, self)._parse(data) 251 | if data: 252 | for i in range(0, len(data), 2): 253 | item = data[i] 254 | arg = data[i+1] if len(data) > i+1 else None 255 | if item and arg: 256 | if item.lower() == "client": 257 | for j in range(0, len(arg), 2): 258 | item2 = arg[j] 259 | arg2 = arg[j+1] if len(arg) > j+1 else None 260 | if item2 and arg2: 261 | if item2.lower() == "id": 262 | self.client.id._parse(arg2) 263 | elif item2.lower() == "output": 264 | self.client.output._parse(arg2) 265 | elif item.lower() == "server": 266 | for j in range(0, len(arg), 2): 267 | item2 = arg[j] 268 | arg2 = arg[j+1] if len(arg) > j+1 else None 269 | if item2 and arg2: 270 | if item2.lower() == "output": 271 | self.server.output._parse(arg2) 272 | 273 | def construct_client(self, host, id, output): 274 | """Construct a Client request using the provided id and output to the provided host. 275 | 276 | Args: 277 | host (str): Host to direct client request to. 278 | id (str): id to include in the request. 279 | output (str): output to include in the request. 280 | 281 | Returns: 282 | Transaction.Client: Constructed Client request. 283 | """ 284 | request = self.client._clone() 285 | request.host = host 286 | request.path = self.client.random_uri() 287 | request.store(self.client.id.transform(id), self.client.id.terminator) 288 | request.store(self.client.output.transform(output), self.client.output.terminator) 289 | return request 290 | 291 | def extract_client(self, request): 292 | """Extract the id and output from the provided MalleableRequest. 293 | 294 | Args: 295 | request (MalleableRequest) 296 | 297 | Returns: 298 | tuple: id, output 299 | """ 300 | for u in (self.client.uris if self.client.uris else ["/"]): 301 | if u.lower() in request.path.lower(): 302 | id = request.extract(self.client, self.client.id.terminator) 303 | output = request.extract(self.client, self.client.output.terminator) 304 | return ( 305 | self.client.id.transform_r(id) if id else None, 306 | self.client.output.transform_r(output) if output else None 307 | ) 308 | return (None, None) 309 | 310 | def construct_server(self, output): 311 | """Construct a Server response using the provided output. 312 | 313 | Args: 314 | output (str): Output to include in the request. 315 | 316 | Returns: 317 | Transaction.Server: Constructed Server response. 318 | """ 319 | response = self.server._clone() 320 | response.store(self.server.output.transform(output), self.server.output.terminator) 321 | return response 322 | 323 | def extract_server(self, response): 324 | """Extract the output from the provided MalleableResponse. 325 | 326 | Args: 327 | response (MalleableResponse) 328 | 329 | Returns: 330 | str: output 331 | """ 332 | output = response.extract(self.server, self.server.output.terminator) 333 | return self.server.output.transform_r(output) if output else None 334 | 335 | class Stager(Transaction): 336 | """The Stager implementation of a Transaction. 337 | 338 | The Stager Transaction is used to fetch a payload stage corresponding to the provided 339 | metadata. In a Stager Transaction, the following data is transmitted: 340 | - (Client) metadata (Session info) 341 | - (Server) output (Encoded payload stage) 342 | """ 343 | 344 | def _defaults(self): 345 | """Default initialization for the Stager Transaction.""" 346 | super(Stager, self)._defaults() 347 | self.client.metadata = Container() 348 | self.server.output = Container() 349 | self.client.verb = "GET" 350 | 351 | def _clone(self): 352 | """Deep copy of the Stager Transaction. 353 | 354 | Returns: 355 | Stager Transaction 356 | """ 357 | new = super(Stager, self)._clone() 358 | new.client.metadata = self.client.metadata._clone() 359 | new.server.output = self.server.output._clone() 360 | new.client.verb = self.client.verb 361 | return new 362 | 363 | def _serialize(self): 364 | """Serialize the Stager Transaction. 365 | 366 | Returns: 367 | dict (str, obj) 368 | """ 369 | d = super(Stager, self)._serialize() 370 | d["client"] = dict((d["client"].items() if "client" in d else []) + { 371 | "metadata" : self.client.metadata._serialize() 372 | }.items()) 373 | d["server"] = dict((d["server"].items() if "server" in d else []) + { 374 | "output" : self.server.output._serialize() 375 | }.items()) 376 | return dict(d.items() + { 377 | 378 | }.items()) 379 | 380 | @classmethod 381 | def _deserialize(cls, data): 382 | """Deserialize data into a Stager Transaction. 383 | 384 | Args: 385 | data (dict (str, obj)): Serialized data (json) 386 | 387 | Returns: 388 | Stager Transaction 389 | """ 390 | stager = super(Stager, cls)._deserialize(data) 391 | if data: 392 | stager.client.metadata = Container._deserialize(data["client"]["metadata"]) if ("client" in data and "metadata" in data["client"]) else Container() 393 | stager.server.output = Container._deserialize(data["server"]["output"]) if ("server" in data and "output" in data["server"]) else Container() 394 | return stager 395 | 396 | @classmethod 397 | def _pattern(cls): 398 | """Define the pattern to recognize a Stager object while parsing a file. 399 | 400 | Returns: 401 | pyparsing object 402 | """ 403 | return Literal("http-stager") + super(Stager, cls)._pattern() 404 | 405 | def _parse(self, data): 406 | """Store the information from a parsed pyparsing result. 407 | 408 | Args: 409 | data: pyparsing data 410 | """ 411 | super(Stager, self)._parse(data) 412 | if data: 413 | for i in range(0, len(data), 2): 414 | item = data[i] 415 | arg = data[i+1] if len(data) > i+1 else None 416 | if item and arg: 417 | if item.lower() == "client": 418 | for j in range(0, len(arg), 2): 419 | item2 = arg[j] 420 | arg2 = arg[j+1] if len(arg) > j+1 else None 421 | if item2 and arg2: 422 | if item2.lower() == "metadata": 423 | self.client.metadata._parse(arg2) 424 | elif item.lower() == "server": 425 | for j in range(0, len(arg), 2): 426 | item2 = arg[j] 427 | arg2 = arg[j+1] if len(arg) > j+1 else None 428 | if item2 and arg2: 429 | if item2.lower() == "output": 430 | self.server.output._parse(arg2) 431 | 432 | def construct_client(self, host, metadata): 433 | """Construct a Client request using the provided metadata to the provided host. 434 | 435 | Args: 436 | host (str): Host to direct client request to. 437 | 438 | Returns: 439 | Transaction.Client: Constructed Client request. 440 | """ 441 | request = self.client._clone() 442 | request.host = host 443 | request.path = self.client.random_uri() 444 | request.store(self.client.metadata.transform(metadata), self.client.metadata.terminator) 445 | return request 446 | 447 | def extract_client(self, request): 448 | """Extract the metadata from the provided MalleableRequest. 449 | 450 | Args: 451 | request (MalleableRequest) 452 | 453 | Returns: None 454 | """ 455 | for u in (self.client.uris if self.client.uris else ["/"]): 456 | if u.lower() in request.path.lower(): 457 | metadata = request.extract(self.client, self.client.metadata.terminator) 458 | if metadata: 459 | return self.client.metadata.transform_r(metadata) 460 | return None 461 | 462 | def construct_server(self, output): 463 | """Construct a Server response using the provided output. 464 | 465 | Args: 466 | output (str): Output to include in the request. 467 | 468 | Returns: 469 | Transaction.Server: Constructed Server response. 470 | """ 471 | response = self.server._clone() 472 | response.store(self.server.output.transform(output), self.server.output.terminator) 473 | return response 474 | 475 | def extract_server(self, response): 476 | """Extract the output from the provided MalleableResponse. 477 | 478 | Args: 479 | response (MalleableResponse) 480 | 481 | Returns: 482 | str: output 483 | """ 484 | output = response.extract(self.server, self.server.output.terminator) 485 | return self.server.output.transform_r(output) if output else None 486 | -------------------------------------------------------------------------------- /malleable/transformation.py: -------------------------------------------------------------------------------- 1 | 2 | import os, base64, urllib 3 | from pyparsing import * 4 | from utility import MalleableError, MalleableUtil, MalleableObject 5 | 6 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 7 | # TRANSFORMATION 8 | # 9 | # Defining the model through which arbitrary data can undergo 10 | # reversable transformations. 11 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 12 | 13 | class Transform(MalleableObject): 14 | """A class housing the arbitrary functionality of a reversable transformation. 15 | 16 | Attributes: 17 | type (int): Type of Transform to implement. 18 | arg (str): Argument to pass to the implementation. 19 | 20 | Functions: 21 | transform (func): Execute the forward transformation on an arbitrary string. 22 | Args: 23 | data (str): The data to be transformed. 24 | Returns: 25 | str: The transformed data. 26 | transform_r (func): Execute the reverse transformation on an arbitrary string. 27 | Args: 28 | data (str): The data to be transformed. 29 | Returns: 30 | str: The transformed data. 31 | generate_python (func): Generate the python code that would execute the 32 | transformation on an arbitrary string using the given variable name. 33 | Args: 34 | var (str): The variable name to be used in the code. 35 | Returns: 36 | str: The python code that would execute the transformation on an arbitrary 37 | string. 38 | generate_python_r (func): Generate the python code that would execute the 39 | reverse transformation on an arbitrary string using the given variable name. 40 | Args: 41 | var (str): The variable name to be used in the code. 42 | Returns: 43 | str: The python code that would execute the reverse transformation on an 44 | arbitrary string. 45 | generate_powershell (func): Generate the powershell code that would execute the 46 | transformation on an arbitrary string using the given variable name. 47 | Args: 48 | var (str): The variable name to be used in the code. 49 | Returns: 50 | str: The powershell code that would execute the transformation on an arbitrary 51 | string. 52 | generate_powershell_r (func): Generating the powershell code that would 53 | execute the reverse transformation on an arbitrary string using the given variable name. 54 | Args: 55 | var (str): The variable name to be used in the code. 56 | Returns: 57 | str: The powershell code that would execute the reverse transformation on an 58 | arbitrary string. 59 | """ 60 | NONE = 0 61 | APPEND = 1 62 | BASE64 = 2 63 | BASE64URL = 3 64 | MASK = 4 65 | NETBIOS = 5 66 | NETBIOSU = 6 67 | PREPEND = 7 68 | 69 | def __init__(self, type=0, arg=None): 70 | """Constructor for a Transform object. 71 | 72 | Args: 73 | type (int, optional): Choose the type of Transform to implement. 74 | arg (str, optional): Argument to pass to the implementation. 75 | """ 76 | self.type = type 77 | self.arg = arg 78 | super(Transform, self).__init__() 79 | 80 | def _defaults(self): 81 | """Default initialization for the Transform object.""" 82 | super(Transform, self)._defaults() 83 | self.apply_transform(self.type, self.arg) 84 | 85 | def _clone(self): 86 | """Deep copy of a Transform object. 87 | 88 | Returns: 89 | Transform object 90 | """ 91 | new = super(Transform, self)._clone() 92 | new.type = self.type 93 | new.arg = self.arg 94 | new.apply_transform(new.type, new.arg) 95 | return new 96 | 97 | def _serialize(self): 98 | """Serialize the Transform object. 99 | 100 | Returns: 101 | dict (str, obj) 102 | """ 103 | return dict(super(Transform, self)._serialize().items() + { 104 | "type" : self.type, 105 | "arg" : self.arg if self.type != Transform.MASK else MalleableUtil.to_hex(self.arg[0]) 106 | }.items()) 107 | 108 | @classmethod 109 | def _deserialize(cls, data): 110 | """Deserialize data into a Transform object. 111 | 112 | Args: 113 | dict (str, obj): Serialized data (json) 114 | 115 | Returns: 116 | Transform object 117 | """ 118 | transform = super(Transform, cls)._deserialize(data) 119 | if data: 120 | transform.type = data["type"] if "type" in data else Transform.NONE 121 | transform.arg = (data["arg"] if transform.type != Transform.MASK else MalleableUtil.from_hex(data["arg"])) if "arg" in data else None 122 | transform.apply_transform(transform.type, transform.arg) 123 | return transform 124 | 125 | @classmethod 126 | def _pattern(cls): 127 | """Define the pattern to recognize a Transform object while parsing a file. 128 | 129 | Returns: 130 | pyparsing object 131 | """ 132 | return ( 133 | Group(Literal("append") + cls.VALUE) | 134 | Group(Literal("base64url")) | 135 | Group(Literal("base64")) | 136 | Group(Literal("mask")) | 137 | Group(Literal("netbiosu")) | 138 | Group(Literal("netbios")) | 139 | Group(Literal("prepend") + cls.VALUE) 140 | ) + cls.SEMICOLON 141 | 142 | def apply_transform(self, type, arg): 143 | """Apply the appropriate transformation to the Transform object. 144 | 145 | Args: 146 | type (int): Type of Transform to implement. 147 | arg (str): Argument to pass to the implementation. 148 | """ 149 | if type == Transform.APPEND: self._append(arg) 150 | elif type == Transform.BASE64: self._base64() 151 | elif type == Transform.BASE64URL: self._base64url() 152 | elif type == Transform.MASK: self._mask(arg) 153 | elif type == Transform.NETBIOS: self._netbios() 154 | elif type == Transform.NETBIOSU: self._netbiosu() 155 | elif type == Transform.PREPEND: self._prepend(arg) 156 | else: self._none() 157 | 158 | def _none(self): 159 | """Configure the `none` Transform, which does nothing.""" 160 | self.transform = lambda data: data, 161 | self.transform_r = lambda data: data, 162 | self.generate_python = lambda var: "", 163 | self.generate_python_r = lambda var: "", 164 | self.generate_powershell = lambda var: "", 165 | self.generate_powershell_r = lambda var: "" 166 | 167 | def _append(self, string): 168 | """Configure the `append` Transform, which appends a static string to an arbitrary input. 169 | 170 | Args: 171 | string (str): The static string to be appended. 172 | 173 | Raises: 174 | MalleableError: If `string` is null. 175 | """ 176 | if string is None: 177 | MalleableError.throw(Transform.__class__, "append", "string argument must not be null") 178 | self.transform = lambda data: data + string 179 | self.transform_r = lambda data: data[:-len(string)] 180 | self.generate_python = lambda var: "%(var)s+=b'%(string)s'\n" % {"var":var, "string":string} 181 | self.generate_python_r = lambda var: "%(var)s=%(var)s[:-%(len)i]\n" % {"var":var, "len":len(string)} 182 | self.generate_powershell = lambda var: "%(var)s+='%(string)s';" % {"var":var, "string":string} 183 | self.generate_powershell_r = lambda var: "%(var)s=%(var)s.Substring(0,%(var)s.Length-%(len)i);" % {"var":var, "len":len(string)} 184 | 185 | def _base64(self): 186 | """Configure the `base64` Transform, which base64 encodes an arbitrary input.""" 187 | self.transform = lambda data: base64.b64encode(data) 188 | self.transform_r = lambda data: base64.b64decode(data) 189 | self.generate_python = lambda var: "%(var)s=base64.b64encode(%(var)s)\n" % {"var":var} 190 | self.generate_python_r = lambda var: "%(var)s=base64.b64decode(%(var)s)\n" % {"var":var} 191 | self.generate_powershell = lambda var: "%(var)s=[Convert]::ToBase64String([System.Text.Encoding]::Default.GetBytes(%(var)s));" % {"var":var} 192 | self.generate_powershell_r = lambda var: "%(var)s=[System.Text.Encoding]::Default.GetString([System.Convert]::FromBase64String(%(var)s));" % {"var":var} 193 | 194 | def _base64url(self): 195 | """Configure the `base64url` Transform, which base64 encodes an arbitary input using url-safe characters.""" 196 | self.transform = lambda data: urllib.quote(base64.b64encode(data)) 197 | self.transform_r = lambda data: base64.b64decode(urllib.unquote(data)) 198 | self.generate_python = lambda var: "%(var)s=urllib.quote(base64.b64encode(%(var)s))\n" % {"var":var} 199 | self.generate_python_r = lambda var: "%(var)s=base64.b64decode(urllib.unquote(%(var)s))\n" % {"var":var} 200 | self.generate_powershell = lambda var: "Add-Type -AssemblyName System.Web;%(var)s=[System.Web.HttpUtility]::UrlEncode([System.Convert]::ToBase64string([System.Text.Encoding]::Default.GetBytes(%(var)s)));" % {"var":var} 201 | self.generate_powershell_r = lambda var: "Add-Type -AssemblyName System.Web;%(var)s=[System.Text.Encoding]::Default.GetString([System.Convert]::FromBase64String([System.Web.HttpUtility]::UrlDecode(%(var)s)));" % {"var":var} 202 | 203 | def _mask(self, key): 204 | """Configure the `mask` Transform, which encodes an arbitrary input using the XOR operation 205 | and a random key. 206 | 207 | Args: 208 | key (str): The key with which to encode / decode. 209 | 210 | Raises: 211 | MalleableError: If `key` is null or empty. 212 | """ 213 | if not key: 214 | MalleableError.throw(Transform.__class__, "mask", "key argument must not be empty") 215 | self.transform = lambda data: "".join([chr(ord(c)^ord(key[0])) for c in data]) 216 | self.transform_r = self.transform 217 | self.generate_python = lambda var: "f_ord=ord if __import__('sys').version_info[0]<3 else int;%(var)s=''.join([chr(f_ord(_)^%(key)s) for _ in %(var)s])\n" % {"key":ord(key[0]), "var":var} 218 | self.generate_python_r = self.generate_python 219 | self.generate_powershell = lambda var: "%(var)s=[System.Text.Encoding]::Default.GetString($(for($_=0;$_ -lt %(var)s.length;$_++){[System.Text.Encoding]::Default.GetBytes(%(var)s)[$_] -bxor %(key)s}));" % {"key":ord(key[0]), "var":var} 220 | self.generate_powershell_r = self.generate_powershell 221 | 222 | def _netbios(self): 223 | """Configure the `netbios` Transform, which encodes an arbitrary input using the lower-case 224 | netbios algorithm.""" 225 | self.transform = lambda data: "".join([chr((ord(c)>>4)+0x61)+chr((ord(c)&0xF)+0x61) for c in data]) 226 | self.transform_r = lambda data: "".join([chr(((ord(data[i])-0x61)<<4)|((ord(data[i+1])-0x61)&0xF)) for i in range(0, len(data), 2)]) 227 | self.generate_python = lambda var: "f_ord=ord if __import__('sys').version_info[0]<3 else int;%(var)s=''.join([chr((f_ord(_)>>4)+0x61)+chr((f_ord(_)&0xF)+0x61) for _ in %(var)s])\n" % {"var":var} 228 | self.generate_python_r = lambda var: "f_ord=ord if __import__('sys').version_info[0]<3 else int;%(var)s=''.join([chr(((f_ord(%(var)s[_])-0x61)<<4)|((f_ord(%(var)s[_+1])-0x61)&0xF)) for _ in range(0,len(%(var)s),2)])\n" % {"var":var} 229 | self.generate_powershell = lambda var: "%(var)s=[System.Text.Encoding]::Default.GetString($(for($_=0;$_ -lt %(var)s.length;$_++){([System.Text.Encoding]::Default.GetBytes(%(var)s)[$_] -shr 4)+97;([System.Text.Encoding]::Default.GetBytes(%(var)s)[$_] -band 15)+97;}));" % {"var":var} 230 | self.generate_powershell_r = lambda var: "%(var)s=[System.Text.Encoding]::Default.GetString($(for($_=0;$_ -lt %(var)s.length;$_+=2){(([System.Text.Encoding]::Default.GetBytes(%(var)s)[$_]-97) -shl 4) -bor (([System.Text.Encoding]::Default.GetBytes(%(var)s)[$_+1]-97) -band 15);}));" % {"var":var} 231 | 232 | def _netbiosu(self): 233 | """Configure the `netbiosu` Transform, which encodes an arbitrary input using the upper-case 234 | netbios algorithm.""" 235 | self.transform = lambda data: "".join([chr((ord(c)>>4)+0x41)+chr((ord(c)&0xF)+0x41) for c in data]) 236 | self.transform_r = lambda data: "".join([chr(((ord(data[i])-0x41)<<4)|((ord(data[i+1])-0x41)&0xF)) for i in range(0, len(data), 2)]) 237 | self.generate_python = lambda var: "f_ord=ord if __import__('sys').version_info[0]<3 else int;%(var)s=''.join([chr((f_ord(_)>>4)+0x41)+chr((f_ord(_)&0xF)+0x41) for _ in %(var)s])\n" % {"var":var} 238 | self.generate_python_r = lambda var: "f_ord=ord if __import__('sys').version_info[0]<3 else int;%(var)s=''.join([chr(((f_ord(%(var)s[_])-0x41)<<4)|((f_ord(%(var)s[_+1])-0x41)&0xF)) for _ in range(0,len(%(var)s),2)])\n" % {"var":var} 239 | self.generate_powershell = lambda var: "%(var)s=[System.Text.Encoding]::Default.GetString($(for($_=0;$_ -lt %(var)s.length;$_++){([System.Text.Encoding]::Default.GetBytes(%(var)s)[$_] -shr 4)+65;([System.Text.Encoding]::Default.GetBytes(%(var)s)[$_] -band 15)+65;}));" % {"var":var} 240 | self.generate_powershell_r = lambda var: "%(var)s=[System.Text.Encoding]::Default.GetString($(for($_=0;$_ -lt %(var)s.length;$_+=2){(([System.Text.Encoding]::Default.GetBytes(%(var)s)[$_]-65) -shl 4) -bor (([System.Text.Encoding]::Default.GetBytes(%(var)s)[$_+1]-65) -band 15);}));" % {"var":var} 241 | 242 | def _prepend(self, string): 243 | """Configure the `prepend` Transform, which prepends a static string to an arbitrary input. 244 | 245 | Args: 246 | string (str): The static string to be prepended. 247 | 248 | Raises: 249 | MalleableError: If `string` is null. 250 | """ 251 | if string is None: 252 | MalleableError.throw(Transform.__class__, "prepend", "string argument must not be null") 253 | self.transform = lambda data: string + data 254 | self.transform_r = lambda data: data[len(string):] 255 | self.generate_python = lambda var: "%(var)s=b'%(string)s'+%(var)s\n" % {"var":var, "string":string} 256 | self.generate_python_r = lambda var: "%(var)s=%(var)s[%(len)i:]\n" % {"var":var, "len":len(string)} 257 | self.generate_powershell = lambda var: "%(var)s='%(string)s'+%(var)s;" % {"var":var, "string":string} 258 | self.generate_powershell_r = lambda var: "%(var)s=%(var)s.substring(%(len)i,%(var)s.Length-%(len)i);" % {"var":var, "len":len(string)} 259 | 260 | class Terminator(MalleableObject): 261 | """A class housing the arbitrary functionality of a reversable data storage mechanism. 262 | 263 | The Terminator defines where data is stored after completing a Transform sequence and where 264 | to retrieve data before starting a reverse Transform sequence. 265 | 266 | Attributes: 267 | type (int): Type of Terminator to implement. 268 | arg (str): Argument to pass to the implementation. 269 | """ 270 | NONE = 0 271 | PRINT = 1 272 | HEADER = 2 273 | PARAMETER = 3 274 | URIAPPEND = 4 275 | 276 | def __init__(self, type=1, arg=None): 277 | """Constructor for a Terminator object 278 | 279 | Args: 280 | type (int, optional): Type of Terminator to implement. 281 | arg (str, optional): Argument to pass to the implementation. 282 | """ 283 | self.type = type 284 | self.arg = arg 285 | super(Terminator, self).__init__() 286 | 287 | def _clone(self): 288 | """Deep copy of a Terminator object. 289 | 290 | Returns: 291 | Terminator 292 | """ 293 | new = super(Terminator, self)._clone() 294 | new.type = self.type 295 | new.arg = self.arg 296 | return new 297 | 298 | def _serialize(self): 299 | """Serialize the Terminator object. 300 | 301 | Returns: 302 | dict (str, obj) 303 | """ 304 | return dict(super(Terminator, self)._serialize().items() + { 305 | "type" : self.type, 306 | "arg" : self.arg 307 | }.items()) 308 | 309 | @classmethod 310 | def _deserialize(cls, data): 311 | """Deserialize data into a Terminator object. 312 | 313 | Args: 314 | dict (str, obj): Serialized data (json) 315 | 316 | Returns: 317 | Terminator object 318 | """ 319 | terminator = super(Terminator, cls)._deserialize(data) 320 | if data: 321 | terminator.type = data["type"] if "type" in data else Terminator.NONE 322 | terminator.arg = data["arg"] if "arg" in data else None 323 | return terminator 324 | 325 | @classmethod 326 | def _pattern(cls): 327 | """Define the pattern to recognize a Terminator object while parsing a file. 328 | 329 | Returns: 330 | pyparsing object 331 | """ 332 | return ( 333 | Group(Literal("header") + cls.VALUE) | 334 | Group(Literal("parameter") + cls.VALUE) | 335 | Group(Literal("print")) | 336 | Group(Literal("uri-append")) 337 | ) + cls.SEMICOLON 338 | 339 | class Container(MalleableObject): 340 | """A class housing a sequence of Transforms. 341 | 342 | Once initialized, a Container object can be used in the following ways: 343 | - Add a Transform to the existing sequence. 344 | - Assign a Terminator to the Transform sequence. 345 | - Execute the sequence of Transforms. 346 | 347 | Attributes: 348 | transforms (list (Transform)) 349 | terminator (Terminator) 350 | """ 351 | 352 | def _defaults(self): 353 | """Default initialization for the Container object.""" 354 | super(Container, self)._defaults() 355 | self.transforms = [] 356 | self.terminator = Terminator() 357 | 358 | def _clone(self): 359 | """Deep copy of the Container object. 360 | 361 | Returns: 362 | Container 363 | """ 364 | new = super(Container, self)._clone() 365 | new.transforms = [t._clone() for t in self.transforms] 366 | new.terminator = self.terminator._clone() 367 | return new 368 | 369 | def _serialize(self): 370 | """Serialize the Container object. 371 | 372 | Returns: 373 | dict (str, obj): Serialized data (json) 374 | """ 375 | return dict(super(Container, self)._serialize().items() + { 376 | "transforms" : [t._serialize() for t in self.transforms], 377 | "terminator" : self.terminator._serialize() 378 | }.items()) 379 | 380 | @classmethod 381 | def _deserialize(cls, data): 382 | """Deserialize data into a Container object. 383 | 384 | Args: 385 | dict (str, obj): Serialized data (json) 386 | 387 | Returns: 388 | Container object 389 | """ 390 | container = super(Container, cls)._deserialize(data) 391 | if data: 392 | container.transforms = [Transform._deserialize(d) for d in data["transforms"]] if "transforms" in data else [] 393 | container.terminator = Terminator._deserialize(data["terminator"]) if "terminator" in data else Terminator() 394 | return container 395 | 396 | @classmethod 397 | def _pattern(cls): 398 | """Define the pattern to recognize a Container object while parsing a file. 399 | 400 | Returns: 401 | pyparsing object 402 | """ 403 | return (cls.FIELD + Group(Suppress("{") + ZeroOrMore( 404 | cls.COMMENT | 405 | Transform._pattern() | 406 | Terminator._pattern() 407 | ) + Suppress("}"))) 408 | 409 | def _parse(self, data): 410 | """Store the information from a parsed pyparsing result. 411 | 412 | Args: 413 | data: pyparsing data 414 | """ 415 | if data: 416 | for item in [d for d in data if d]: 417 | type = item[0] 418 | arg = item[1] if len(item) > 1 else None 419 | if type: 420 | type = type.lower() 421 | if type == "append": self.append(arg) 422 | elif type == "base64": self.base64() 423 | elif type == "base64url": self.base64url() 424 | elif type == "mask": self.mask() 425 | elif type == "netbios": self.netbios() 426 | elif type == "netbiosu": self.netbiosu() 427 | elif type == "prepend": self.prepend(arg) 428 | 429 | elif type == "print": self.print_() 430 | elif type == "header": self.header(arg) 431 | elif type == "parameter": self.parameter(arg) 432 | elif type == "uri-append": self.uriappend() 433 | 434 | def append(self, string): 435 | """Add the `append` Transform to the Container's Transform sequence. 436 | 437 | Args: 438 | string (str): The static string to be appended. 439 | """ 440 | self.transforms.append(Transform(type=Transform.APPEND, arg=string)) 441 | 442 | def base64(self): 443 | """Add the `base64` Transform to the Container's Transform sequence.""" 444 | self.transforms.append(Transform(type=Transform.BASE64)) 445 | 446 | def base64url(self): 447 | """Add the `base64url` Transform to the Container's Transform sequence.""" 448 | self.transforms.append(Transform(type=Transform.BASE64URL)) 449 | 450 | def mask(self, key=None): 451 | """Add the `mask` Transform to the Container's Transform sequence. 452 | 453 | Args: 454 | key (str, optional): The key with which to encode / decode. 455 | """ 456 | if not key: 457 | key = os.urandom(1) 458 | while (ord(key) < 0 or ord(key) > 127): key = os.urandom(1) # Requirement for powershell 459 | self.transforms.append(Transform(type=Transform.MASK, arg=key)) 460 | 461 | def netbios(self): 462 | """Add the `netbios` Transform to the Container's Transform sequence.""" 463 | self.transforms.append(Transform(type=Transform.NETBIOS)) 464 | 465 | def netbiosu(self): 466 | """Add the `netbiosu` Transform to the Container's Transform sequence.""" 467 | self.transforms.append(Transform(type=Transform.NETBIOSU)) 468 | 469 | def prepend(self, string): 470 | """Add the `prepend` Transform to the Container's Transform sequence. 471 | 472 | Args: 473 | string (str): The static string to be prepended. 474 | """ 475 | self.transforms.append(Transform(type=Transform.PREPEND, arg=string)) 476 | 477 | def print_(self): 478 | """Specify that the data be stored in the request body after transformation.""" 479 | self.terminator = Terminator(type=Terminator.PRINT) 480 | 481 | def header(self, header): 482 | """Use the specified header to store the data after transformation. 483 | 484 | Args: 485 | header (str) 486 | 487 | Rasie: 488 | MalleableError: If `header` is empty. 489 | """ 490 | if not header: 491 | MalleableError.throw(Container, "header", "argument must not be null") 492 | self.terminator = Terminator(type=Terminator.HEADER, arg=header) 493 | 494 | def parameter(self, parameter): 495 | """Use the specified parameter to store the data after transformation. 496 | 497 | Args: 498 | parameter (str) 499 | 500 | Rasie: 501 | MalleableError: If `parameter` is empty. 502 | """ 503 | if not parameter: 504 | MalleableError.throw(Container, "parameter", "argument must not be null") 505 | self.terminator = Terminator(type=Terminator.PARAMETER, arg=parameter) 506 | 507 | def uriappend(self): 508 | """Specify that the data append to the uri after transformation.""" 509 | self.terminator = Terminator(type=Terminator.URIAPPEND) 510 | 511 | def transform(self, data): 512 | """Transform the provided data using the sequence of Transforms. 513 | 514 | Args: 515 | data (str): The data to be transformed. 516 | 517 | Returns: 518 | str: The transformed data. 519 | """ 520 | if data is None: data = "" 521 | for t in self.transforms: 522 | data = t.transform(data) 523 | return data 524 | 525 | def transform_r(self, data): 526 | """Transform the provided data using the sequence of Transforms in reverse. 527 | 528 | Args: 529 | data (str): The data to be reverse-transformed. 530 | 531 | Returns: 532 | str: The reverse-transformed data. 533 | """ 534 | if data is None: data = "" 535 | for t in self.transforms[::-1]: 536 | data = t.transform_r(data) 537 | return data 538 | 539 | def generate_python(self, var): 540 | """Generate python code that would transform arbitrary data using the sequence 541 | of Transforms. 542 | 543 | Args: 544 | var (str): The variable name to be used in the python code. 545 | 546 | Returns: 547 | str: The python code. 548 | 549 | Raises: 550 | MalleableError: If `var` is empty. 551 | """ 552 | if not var: 553 | MalleableError.throw(Container, "generate_python", "var must not be empty") 554 | code = "" 555 | for t in self.transforms: 556 | code += t.generate_python(var) 557 | return code 558 | 559 | def generate_python_r(self, var): 560 | """Generate python code that would transform arbitrary data using the sequence 561 | of Transforms in reverse. 562 | 563 | Args: 564 | var (str): The variable name to be used in the python code. 565 | 566 | Returns: 567 | str: The python code. 568 | 569 | Raises: 570 | MalleableError: If `var` is empty. 571 | """ 572 | if not var: 573 | MalleableError.throw(Container, "generate_python_r", "var must not be empty") 574 | code = "" 575 | for t in self.transforms[::-1]: 576 | code += t.generate_python_r(var) 577 | return code 578 | 579 | def generate_powershell(self, var): 580 | """Generate powershell code that would transform arbitrary data using the sequence 581 | of Transforms. 582 | 583 | Args: 584 | var (str): The variable name to be used in the powershell code. 585 | 586 | Returns: 587 | str: The powershell code. 588 | 589 | Raises: 590 | MalleableError: If `var` is empty. 591 | """ 592 | if not var: 593 | MalleableError.throw(Container, "generate_powershell", "var must not be empty") 594 | code = "" 595 | for t in self.transforms: 596 | code += t.generate_powershell(var) 597 | return code 598 | 599 | def generate_powershell_r(self, var): 600 | """Generate powershell code that would transform arbitrary data using the sequence 601 | of Transforms in reverse. 602 | 603 | Args: 604 | var (str): The variable name to be used in the powershell code. 605 | 606 | Returns: 607 | str: The powershell code. 608 | 609 | Raises: 610 | MalleableError: If `var` is empty. 611 | """ 612 | if not var: 613 | MalleableError.throw(Container, "generate_powershell_r", "var must not be empty") 614 | code = "" 615 | for t in self.transforms[::-1]: 616 | code += t.generate_powershell_r(var) 617 | return code 618 | -------------------------------------------------------------------------------- /malleable/transaction.py: -------------------------------------------------------------------------------- 1 | import random, urlparse, urllib 2 | from pyparsing import * 3 | from utility import MalleableError, MalleableUtil, MalleableObject 4 | from transformation import Transform, Terminator, Container 5 | 6 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 7 | # TRANSACTION 8 | # 9 | # Defining the core components of an interaction between a web 10 | # client request and a web server response. 11 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 12 | 13 | class MalleableRequest(MalleableObject): 14 | """A generic request class used to transfer the contents of an html request. 15 | 16 | Attributes: 17 | _url (urlparse.SplitResult) 18 | url (str, property) 19 | verb (str) 20 | scheme (str, property) 21 | netloc (str, property) 22 | host (str, property) 23 | port (str, property) 24 | path (str, property) 25 | parameters (dict (str, str)) 26 | extra (str) 27 | headers (dict (str, str)) 28 | body (str) 29 | """ 30 | 31 | def _defaults(self): 32 | """Default initialization for the MalleableRequest object.""" 33 | super(MalleableRequest, self)._defaults() 34 | self._url = urlparse.SplitResult("http","","/","","") 35 | self.verb = "GET" 36 | self.extra = "" 37 | self.headers = {} 38 | self.body = "" 39 | 40 | def _clone(self): 41 | """Deep copy of the MalleableRequest object. 42 | 43 | Returns: 44 | MalleableRequest 45 | """ 46 | new = super(MalleableRequest, self)._clone() 47 | new._url = self._url 48 | new.verb = self.verb 49 | new.extra = self.extra 50 | new.headers = {k:v for k,v in self.headers.items()} 51 | new.body = self.body 52 | return new 53 | 54 | def _serialize(self): 55 | """Serialize the MalleableRequest object. 56 | 57 | Returns: 58 | dict (str, obj) 59 | """ 60 | return dict(super(MalleableRequest, self)._serialize().items() + { 61 | "url" : self.url, 62 | "verb" : self.verb, 63 | "extra" : self.extra, 64 | "headers" : self.headers, 65 | "body" : self.body 66 | }.items()) 67 | 68 | @classmethod 69 | def _deserialize(cls, data): 70 | """Deserialize data into a MalleableRequest object. 71 | 72 | Args: 73 | data (dict (str, obj)): Serialized data (json) 74 | 75 | Returns: 76 | MalleableRequest object 77 | """ 78 | request = super(MalleableRequest, cls)._deserialize(data) 79 | if data: 80 | request.url = data["url"] if "url" in data else "" 81 | request.verb = data["verb"] if "verb" in data else "GET" 82 | request.extra = data["extra"] if "extra" in data else "" 83 | request.headers = data["headers"] if "headers" in data else {} 84 | request.body = data["body"] if "body" in data else "" 85 | return request 86 | 87 | @classmethod 88 | def _pattern(cls): 89 | """Define the pattern to recognize a MalleableRequest object while parsing a file. 90 | 91 | Returns: 92 | pyparsing object 93 | """ 94 | return Group(Suppress("{") + ZeroOrMore( 95 | cls.COMMENT | 96 | (Literal("header") + Group(cls.VALUE + cls.VALUE) + cls.SEMICOLON) | 97 | (Literal("parameter") + Group(cls.VALUE + cls.VALUE) + cls.SEMICOLON) | 98 | Container._pattern() 99 | ) + Suppress("}")) 100 | 101 | def _parse(self, data): 102 | """Store the information from a parsed pyparsing result. 103 | 104 | Args: 105 | data: pyparsing data 106 | """ 107 | if data: 108 | for i in range(0, len(data), 2): 109 | item = data[i] 110 | arg = data[i+1] if len(data) > i+1 else None 111 | if item and arg: 112 | if item.lower() == "header" and len(arg) > 1: 113 | key, value = arg[0], arg[1] 114 | if key and value: 115 | self.header(key, value) 116 | elif item.lower() == "parameter" and len(arg) > 1: 117 | key, value = arg[0], arg[1] 118 | if key and value: 119 | self.parameter(key, value) 120 | 121 | def _replace(self, scheme=None, host=None, port=None, path=None, parameters=None, verb=None, extra=None, headers=None, body=None): 122 | """Clone the MalleableRequest object while replacing the provided attributes. 123 | 124 | Args: 125 | scheme (str, optional) 126 | host (str, optional) 127 | port (str, optional) 128 | path (str, optional) 129 | parameters (dict (str, str), optional) 130 | verb (str, optional) 131 | extra (str, optional) 132 | headers (dict (str, str), optional) 133 | body (str, optional) 134 | 135 | Returns: 136 | MalleableRequest object 137 | """ 138 | new = self._clone() 139 | if scheme is not None: new.scheme = scheme 140 | if host is not None: new.host = host 141 | if port is not None: new.port = port 142 | if path is not None: new.path = path 143 | if parameters is not None: new.parameters = parameters 144 | if verb is not None: new.verb = verb 145 | if extra is not None: new.extra = extra 146 | if headers is not None: new.headers = headers 147 | if body is not None: new.body = body 148 | return new 149 | 150 | @property 151 | def url(self): 152 | """Getter for the full url. 153 | 154 | Note: Actually generates from the urlparse.SplitResult _url. 155 | 156 | Returns: 157 | str: url 158 | """ 159 | return urlparse.urlunsplit(self._url) + self.extra 160 | 161 | @url.setter 162 | def url(self, url): 163 | """Setter for the full url. 164 | 165 | Note: Actually sets parses the input into the urlparse.SplitResult _url. 166 | 167 | Args: 168 | url (str) 169 | """ 170 | if "://" in url: 171 | if "http://" not in url.lower() and "https://" not in url.lower(): 172 | MalleableError.throw(self.__class__, "url", "Scheme not supported: %s" % url) 173 | else: 174 | url = "http://" + url 175 | self._url = urlparse.urlsplit(url) 176 | 177 | @property 178 | def scheme(self): 179 | """Getter for the scheme. 180 | 181 | Returns: 182 | str: scheme 183 | """ 184 | return self._url.scheme 185 | 186 | @scheme.setter 187 | def scheme(self, scheme): 188 | """Setter for the scheme. 189 | 190 | Args: 191 | scheme (str) 192 | """ 193 | self._url = self._url._replace(scheme=scheme.lower() if scheme else "") 194 | 195 | @property 196 | def netloc(self): 197 | """Getter for the netloc. 198 | 199 | Returns: 200 | str: netloc 201 | """ 202 | return self._url.netloc 203 | 204 | @netloc.setter 205 | def netloc(self, netloc): 206 | """Setter for the netloc. 207 | 208 | Args: 209 | netloc (str) 210 | """ 211 | self._url = self._url._replace(netloc=netloc) 212 | 213 | @property 214 | def host(self): 215 | """Getter for the host. 216 | 217 | Returns: 218 | str: host 219 | """ 220 | return self._url.hostname 221 | 222 | @host.setter 223 | def host(self, host): 224 | """Setter for the host. 225 | 226 | Args: 227 | host (str) 228 | 229 | Raises: 230 | MalleableError: If scheme not supported. 231 | """ 232 | if "://" in host: 233 | if "http://" in host: 234 | host = host.lstrip("http://") 235 | self.scheme = "http" 236 | elif "https://" in host: 237 | host = host.lstrip("https://") 238 | self.scheme = "https" 239 | else: 240 | MalleableError.throw(self.__class__, "host", "Scheme not supported: %s" % host) 241 | if ":" not in host and self._url.port: 242 | host += ":" + str(self._url.port) 243 | self._url = self._url._replace(netloc=host) 244 | 245 | @property 246 | def port(self): 247 | """Getter for the port. 248 | 249 | Returns: 250 | str: port 251 | """ 252 | return self._url.port 253 | 254 | @port.setter 255 | def port(self, port): 256 | """Setter for the port. 257 | 258 | Args: 259 | port (int) 260 | """ 261 | hostname = self._url.hostname 262 | netloc = (str(hostname) if hostname else "") + ((":"+str(port)) if port else "") 263 | self._url = self._url._replace(netloc=netloc) 264 | 265 | @property 266 | def path(self): 267 | """Getter for the path. 268 | 269 | Returns: 270 | str: path 271 | """ 272 | return self._url.path 273 | 274 | @path.setter 275 | def path(self, path): 276 | """Setter for the path. 277 | 278 | Args: 279 | path (str) 280 | """ 281 | self._url = self._url._replace(path=path) 282 | 283 | @property 284 | def query(self): 285 | """Getter for the query string. 286 | 287 | Returns: 288 | str: query 289 | """ 290 | return self._url.query 291 | 292 | @query.setter 293 | def query(self, query): 294 | """Setter for the query string. 295 | 296 | Args: 297 | query (str) 298 | """ 299 | self._url = self._url._replace(query=query) 300 | 301 | @property 302 | def parameters(self): 303 | """Getter for the parameters. 304 | 305 | Note: Actually generated from urlparse.SplitResult _url. 306 | 307 | Returns: 308 | dict (str, str): parameters 309 | """ 310 | return dict(urlparse.parse_qsl(self._url.query)) 311 | 312 | @parameters.setter 313 | def parameters(self, parameters): 314 | """Setter for the parameters. 315 | 316 | Note: Actually sets the query string in urlparse.SplitResult _url. 317 | 318 | Args: 319 | parameters (dict(str, str)) 320 | """ 321 | query = urllib.urlencode(parameters) if parameters else "" 322 | self._url = self._url._replace(query=query) 323 | 324 | def parameter(self, parameter, value): 325 | """Add a single parameter. 326 | 327 | Args: 328 | parameter (str) 329 | value (str) 330 | """ 331 | p = self.parameters 332 | p[parameter] = value 333 | self.parameters = p 334 | 335 | def get_parameter(self, parameter): 336 | """Get a single parameter value. 337 | 338 | Args: 339 | parameter (str) 340 | 341 | Returns: 342 | parameter value if exists else None. 343 | """ 344 | if parameter: 345 | parameter = parameter.lower() 346 | if self.parameters: 347 | parameters = {k.lower():v for k,v in self.parameters.items()} 348 | if parameter in parameters: 349 | return parameters[parameter] 350 | return None 351 | 352 | def header(self, header, value): 353 | """Set a single header value. 354 | 355 | Args: 356 | header (str) 357 | value (str) 358 | """ 359 | self.headers[header] = value 360 | 361 | def get_header(self, header): 362 | """Get a single header value. 363 | 364 | Args: 365 | header (str) 366 | 367 | Returns: 368 | header value if exists else None. 369 | """ 370 | if header: 371 | header = header.lower() 372 | if self.headers: 373 | headers = {k.lower():v for k,v in self.headers.items()} 374 | if header in headers: 375 | return headers[header] 376 | return None 377 | 378 | def store(self, data, terminator): 379 | """Store the data according to the specified terminator. 380 | 381 | Args: 382 | data (str): The data to be stored. 383 | terminator (Terminator): The terminator specifying where to store the data. 384 | """ 385 | if terminator.type == Terminator.HEADER: self.header(terminator.arg, data) 386 | elif terminator.type == Terminator.PARAMETER: self.parameter(terminator.arg, data) 387 | elif terminator.type == Terminator.URIAPPEND: self.extra = data 388 | elif terminator.type == Terminator.PRINT: self.body = data 389 | 390 | def extract(self, original, terminator): 391 | """Extract the data according to the specified terminator. 392 | 393 | Args: 394 | original (MalleableRequest): The original request to compare to. 395 | terminator (Terminator): The terminator specifying where the data is stored. 396 | """ 397 | data = None 398 | if terminator.type == Terminator.HEADER: 399 | data = self.get_header(terminator.arg) 400 | if data: data = urllib.unquote(data) 401 | elif terminator.type == Terminator.PARAMETER: 402 | data = self.get_parameter(terminator.arg) 403 | if data: data = urllib.unquote(data) 404 | elif terminator.type == Terminator.URIAPPEND: 405 | if self.extra: 406 | data = urllib.unquote(self.extra) 407 | elif original.parameters: 408 | for p in sorted(original.parameters, key=len, reverse=True): 409 | known = original.parameters[p] 410 | shown = self.get_parameter(p) 411 | if shown and known.lower() in shown.lower() and len(shown) > len(known): 412 | data = known.split(known)[-1] 413 | if data: data = urllib.unquote(data) 414 | break 415 | else: 416 | for known in sorted(original.uris, key=len, reverse=True): 417 | shown = self.path 418 | if known.lower() in shown.lower() and len(shown) > len(known): 419 | data = shown.split(known)[-1] 420 | if data: data = urllib.unquote(data) 421 | break 422 | elif terminator.type == Terminator.PRINT: data = self.body 423 | return data 424 | 425 | class MalleableResponse(MalleableObject): 426 | """A generate response class used to transfer the contents of an html response. 427 | 428 | Attributes: 429 | code (int) 430 | headers (dict (str, str)) 431 | body (str) 432 | """ 433 | 434 | def _defaults(self): 435 | """Default initialization for the MalleableResponse object.""" 436 | super(MalleableResponse, self)._defaults() 437 | self.code = 200 438 | self.headers = {} 439 | self.body = "" 440 | 441 | def _clone(self): 442 | """Deep copy of the MalleableResponse object. 443 | 444 | Returns: 445 | MalleableResponse 446 | """ 447 | new = super(MalleableResponse, self)._clone() 448 | new.code = self.code 449 | new.headers = {k: v for k,v in self.headers.items()} 450 | new.body = self.body 451 | return new 452 | 453 | def _serialize(self): 454 | """Serialize the MalleableResponse object. 455 | 456 | Returns: 457 | dict (str, obj) 458 | """ 459 | return dict(super(MalleableResponse, self)._serialize().items() + { 460 | "code" : self.code, 461 | "headers" : self.headers, 462 | "body" : self.body 463 | }.items()) 464 | 465 | @classmethod 466 | def _deserialize(cls, data): 467 | """Deserialize data into a MalleableReponse object. 468 | 469 | Args: 470 | data (str): Serialized data (json) 471 | 472 | Returns: 473 | MalleableResponse object 474 | """ 475 | response = super(MalleableResponse, cls)._deserialize(data) 476 | if data: 477 | response.code = data["code"] if "code" in data else 200 478 | response.headers = data["headers"] if "headers" in data else {} 479 | response.body = data["body"] if "body" in data else "" 480 | return response 481 | 482 | @classmethod 483 | def _pattern(cls): 484 | """Define the pattern to recognize a MalleableResponse object while parsing a file. 485 | 486 | Returns: 487 | pyparsing object 488 | """ 489 | return Group(Suppress("{") + ZeroOrMore( 490 | cls.COMMENT | 491 | (Literal("header") + Group(cls.VALUE + cls.VALUE) + cls.SEMICOLON) | 492 | Container._pattern() 493 | ) + Suppress("}")) 494 | 495 | def _parse(self, data): 496 | """Store the information from a parsed pyparsing result. 497 | 498 | Args: 499 | data: pyparsing data 500 | """ 501 | if data: 502 | for i in range(0, len(data), 2): 503 | item = data[i] 504 | arg = data[i+1] if len(data) > i+1 else None 505 | if item and arg: 506 | if item.lower() == "header" and len(arg) > 1: 507 | key, value = arg[0], arg[1] 508 | if key and value: 509 | self.header(key, value) 510 | 511 | def get_header(self, header): 512 | """Get a single header value. 513 | 514 | Args: 515 | header (str) 516 | 517 | Returns: 518 | header value if exists else None. 519 | """ 520 | if header: 521 | header = header.lower() 522 | if self.headers: 523 | headers = {k.lower():v for k,v in self.headers.items()} 524 | if header in headers: 525 | return headers[header] 526 | return None 527 | 528 | def header(self, header, value): 529 | """Set a single header value. 530 | 531 | Args: 532 | header (str) 533 | value (str) 534 | """ 535 | self.headers[header] = value 536 | 537 | def store(self, data, terminator): 538 | """Store the data according to the specified terminator. 539 | 540 | Args: 541 | data (str): The data to be stored. 542 | terminator (Terminator): The terminator specifying where to store the data. 543 | """ 544 | if terminator.type == Terminator.HEADER: self.header(terminator.arg, data) 545 | elif terminator.type == Terminator.PRINT: self.body = data 546 | 547 | def extract(self, original, terminator): 548 | """Extract the data according to the specified terminator. 549 | 550 | Args: 551 | original (MalleableResponse): The original request to compare to. 552 | terminator (Terminator): The terminator specifying where the data is stored. 553 | """ 554 | data = None 555 | if terminator.type == Terminator.HEADER: 556 | data = self.get_header(terminator.arg) 557 | if data: data = urllib.unquote(data) 558 | elif terminator.type == Terminator.PRINT: 559 | data = self.body 560 | return data 561 | 562 | class Transaction(MalleableObject): 563 | """A class housing the core components of an interaction between a web client request and 564 | a web server response. 565 | 566 | Attributes: 567 | client (MalleableRequest): Client object containing client request attributes. 568 | server (MalleableResponse): Server object containing server response attributes. 569 | """ 570 | 571 | def _defaults(self): 572 | """Default initialization for the Transaction object.""" 573 | super(Transaction, self)._defaults() 574 | self.client = Transaction.Client() 575 | self.server = Transaction.Server() 576 | 577 | def _clone(self): 578 | """Deep copy of the Transaction object. 579 | 580 | Returns: 581 | Transaction 582 | """ 583 | new = super(Transaction, self)._clone() 584 | new.client = self.client._clone() 585 | new.server = self.server._clone() 586 | return new 587 | 588 | def _serialize(self): 589 | """Serialize the Transaction object. 590 | 591 | Returns: 592 | dict (str, obj) 593 | """ 594 | return dict(super(Transaction, self)._serialize().items() + { 595 | "client" : self.client._serialize(), 596 | "server" : self.server._serialize() 597 | }.items()) 598 | 599 | @classmethod 600 | def _deserialize(cls, data): 601 | """Deserialize data into a Transaction object. 602 | 603 | Args: 604 | data (str): Serialized data (json) 605 | 606 | Returns: 607 | Transaction object 608 | """ 609 | transaction = super(Transaction, cls)._deserialize(data) 610 | if data: 611 | transaction.client = Transaction.Client._deserialize(data["client"]) if "client" in data else Transaction.Client() 612 | transaction.server = Transaction.Server._deserialize(data["server"]) if "server" in data else Transaction.Server() 613 | return transaction 614 | 615 | @classmethod 616 | def _pattern(cls): 617 | """Define the pattern to recognize a Transaction object while parsing a file. 618 | 619 | Returns: 620 | pyparsing object 621 | """ 622 | return Group(Suppress("{") + ZeroOrMore( 623 | cls.COMMENT | 624 | (Literal("set") + Group(cls.FIELD + cls.VALUE) + cls.SEMICOLON) | 625 | cls.Client._pattern() | 626 | cls.Server._pattern() 627 | ) + Suppress("}")) 628 | 629 | def _parse(self, data): 630 | """Store the information from a parsed pyparsing result. 631 | 632 | Args: 633 | data: pyparsing data 634 | """ 635 | if data: 636 | for i in range(0, len(data), 2): 637 | item = data[i] 638 | arg = data[i+1] if len(data) > i+1 else None 639 | if item and arg: 640 | if item.lower() == "set" and len(arg) > 1: 641 | key, value = arg[0], arg[1] 642 | if key and value: 643 | if key.lower() == "uri": 644 | for u in value.split(): 645 | self.client.uri(u) 646 | elif key.lower() == "uri_x86": 647 | for u in value.split(): 648 | self.client.uri(u, x86=True) 649 | elif key.lower() == "uri_x64": 650 | for u in value.split(): 651 | self.client.uri(u, x64=True) 652 | else: 653 | setattr(self, key, value) 654 | elif item.lower() == "client": 655 | self.client._parse(arg) 656 | elif item.lower() == "server": 657 | self.server._parse(arg) 658 | 659 | @property 660 | def verb(self): 661 | """Getter for the verb attribute. 662 | 663 | Returns: 664 | verb (str) 665 | """ 666 | return self.client.verb 667 | 668 | @verb.setter 669 | def verb(self, verb): 670 | """Setter for the verb attribute. 671 | 672 | Note: verb actually lives in the client, this is here for compatibility. 673 | 674 | Args: 675 | verb (str) 676 | """ 677 | self.client.verb = verb 678 | 679 | class Client(MalleableRequest): 680 | """A class housing the core components of a web client request. 681 | 682 | Attributes: 683 | uris (list (str)): Uris to which client traffic will be directed. 684 | uris_x86 (list (str)): x86-only uris to which client traffic will be directed. 685 | uris_x64 (list (str)): x64-only uris to which client traffic will be directed. 686 | """ 687 | 688 | def _defaults(self): 689 | """Default initialization for the Client object.""" 690 | super(Transaction.Client, self)._defaults() 691 | self.uris = [] 692 | self.uris_x86 = [] 693 | self.uris_x64 = [] 694 | 695 | def _clone(self): 696 | """Deep copy of the Client object. 697 | 698 | Returns: 699 | Client 700 | """ 701 | new = super(Transaction.Client, self)._clone() 702 | new.uris = [u for u in self.uris] 703 | new.uris_x86 = [u for u in self.uris_x86] 704 | new.uris_x64 = [u for u in self.uris_x64] 705 | return new 706 | 707 | def _serialize(self): 708 | """Serialize the Client object. 709 | 710 | Returns: 711 | dict (str, obj) 712 | """ 713 | return dict(super(Transaction.Client, self)._serialize().items() + { 714 | "uris" : self.uris, 715 | "uris_x86" : self.uris_x86, 716 | "uris_x64" : self.uris_x64 717 | }.items()) 718 | 719 | @classmethod 720 | def _deserialize(self, data): 721 | """Deserialize data into a Client object. 722 | 723 | Args: 724 | data (str): Serialized data (json) 725 | 726 | Returns: 727 | Client object 728 | """ 729 | client = super(Transaction.Client, self)._deserialize(data) 730 | if data: 731 | client.uris = data["uris"] if "uris" in data else [] 732 | client.uris_x86 = data["uris_x86"] if "uris_x86" in data else [] 733 | client.uris_x64 = data["uris_x64"] if "uris_x64" in data else [] 734 | return client 735 | 736 | @classmethod 737 | def _pattern(cls): 738 | """Define the pattern to recognize a Transaction Client object while parsing a file. 739 | 740 | Returns: 741 | pyparsing object 742 | """ 743 | return Literal("client") + super(Transaction.Client, cls)._pattern() 744 | 745 | def stringify(self): 746 | """Serialize into a string compatible with Powershell Empire.""" 747 | return "|".join([ 748 | ",".join(self.uris if self.uris else ["/"]), 749 | str(self.get_header("User-Agent")) 750 | ] + [":".join([h,v]) for h,v in self.headers.items() if h.lower() != "user-agent"]) 751 | 752 | def uri(self, uri, x86=False, x64=False): 753 | """Add a uri to the list of uris 754 | 755 | Args: 756 | uri (str) 757 | """ 758 | self.uris = list(set(self.uris).union(set([uri]))) 759 | if x86: 760 | self.uris_x86 = list(set(self.uris_x86).union(set([uri]))) 761 | if x64: 762 | self.uris_x64 = list(set(self.uris_x64).union(set([uri]))) 763 | 764 | def random_uri(self, x86=False, x64=False, default="/"): 765 | """Returns a random uri from the list. 766 | 767 | Args: 768 | x86 (bool, optional): Only include x86 uris 769 | x64 (bool, optional): Only include x64 uris 770 | default (str, optional): Default uri to use if list is empty 771 | """ 772 | if x86 and x64: 773 | uris = self.uris 774 | elif x86: 775 | uris = self.uris_x86 776 | elif x64: 777 | uris = self.uris_x64 778 | else: 779 | uris = self.uris 780 | return (random.choice(uris) if uris else default) 781 | 782 | class Server(MalleableResponse): 783 | """A class housing the core components of a web server response.""" 784 | 785 | @classmethod 786 | def _pattern(cls): 787 | """Define the pattern to recognize a Transaction Server object while parsing a file. 788 | 789 | Returns: 790 | pyparsing object 791 | """ 792 | return Literal("server") + super(Transaction.Server, cls)._pattern() 793 | --------------------------------------------------------------------------------