class Ability(ns.ThreadedAbilityBase): _option_list = [ ns.PathOpt(ns.OptNames.PATH_DST, None, 'File to write the pcap to', must_exist=False) ] _info = ns.AbilityInfo(name='Save to Pcap', description='Save received Ether() frames to PCAP', authors=[ 'Florian Maury', ], tags=[ns.Tag.TCP_STACK_L1], type=ns.AbilityType.COMPONENT) def main(self): if self.path_dst is None: self._view.error('Missing filename') return pcapwr = scapy.utils.PcapWriter(self.path_dst) try: while not self.is_stopped(): if self._poll(0.1): s = self._recv() if s: p = scapy.layers.l2.Ether(s) pcapwr.write(p) except (IOError, EOFError): pass pcapwr.close()
class Ability(ns.ThreadedAbilityBase): _option_list = [ ns.PathOpt(ns.OptNames.PATH_DST, default=None, comment='File to write the pcap to', must_exist=False, optional=True) ] _info = ns.AbilityInfo(name='Save to Pcap', description='Save received Ether() frames to PCAP', authors=[ 'Florian Maury', ], tags=[ns.Tag.TCP_STACK_L1], type=ns.AbilityType.COMPONENT) def main(self): if self.path_dst is None: self._view.error('Missing filename') return pcapwr = scapy.utils.PcapWriter(self.path_dst) try: while not self.is_stopped(): if self._poll(0.1): s = self._recv() if s: p = scapy.layers.l2.Ether(s) pcapwr.write(p) except (IOError, EOFError): pass pcapwr.close() @classmethod def check_preconditions(cls, module_factory): l_dep = [] if not HAS_SCAPY: l_dep.append('Scapy support missing or broken. ' 'Please install it or proceed to an update.') l_dep += super(Ability, cls).check_preconditions(module_factory) return l_dep
class Ability(ns.ThreadedAbilityBase): _option_list = [ ns.PathOpt(ns.OptNames.PATH_SRC, None, 'Pcap file from which the packets are read', must_exist=True, readable=True) ] _info = ns.AbilityInfo(name='Read from Pcap', description='Read frames from PCAP', authors=[ 'Florian Maury', ], tags=[ns.Tag.TCP_STACK_L1], type=ns.AbilityType.COMPONENT) def main(self): if self.path_src is None: self._view.error('Missing filename') return pcaprd = scapy.utils.PcapReader(self.path_src) p = True while p: p = pcaprd.read_packet(size=65535) if p: try: self._send(str(p)) except (IOError, EOFError): break pcaprd.close() @classmethod def check_preconditions(cls, module_factory): l = [] if not HAS_SCAPY: l.append('Scapy support missing or broken.') l += super(Ability, cls).check_preconditions(module_factory) return l
class Ability(ns.ThreadedAbilityBase): _option_list = [ ns.PathOpt( 'fake_zone', must_exist=True, readable=True, is_dir=False, comment= "Zone file containing the names for which a lie must be returned." ), ns.PathOpt( 'policy_zone', must_exist=True, readable=True, is_dir=False, optional=True, comment= "Zone file containing the policy to apply to incoming requests. If no match is found, there is an implicit FAKE policy" ), ns.BoolOpt( 'resolver', default=False, comment= 'Whether we are spoofing a resolver (or an authoritative server'), ns.BoolOpt( 'authentic', default=False, comment= 'If resolver is true, our answers are flagged as authentic, unless checking is disabled' ), ns.BoolOpt('quiet', default=True, comment='Whether we should log stuff on error'), ] _info = ns.AbilityInfo( name='DNSProxy Server', description='DNSProxy "server", which answers lies', authors=[ 'Florian Maury', ], tags=[ns.Tag.TCP_STACK_L5, ns.Tag.THREADED, ns.Tag.DNS], type=ns.AbilityType.COMPONENT) _dependencies = [] FAKE_POLICY = 0 SERVFAIL_POLICY = 1 NXDOMAIN_POLICY = 2 NODATA_POLICY = 3 TCP_POLICY = 4 PASSTHRU_POLICY = 5 DECISION_DICT = { NODATA_POLICY: lambda self, *args, **kwargs: self._send_nodata(*args, **kwargs), NXDOMAIN_POLICY: lambda self, *args, **kwargs: self._send_nxdomain(*args, **kwargs), SERVFAIL_POLICY: lambda self, *args, **kwargs: self._send_servfail(*args, **kwargs), TCP_POLICY: lambda self, *args, **kwargs: self._send_truncated(*args, **kwargs), PASSTHRU_POLICY: lambda self, *args, **kwargs: self._send_passthru(*args, **kwargs), FAKE_POLICY: lambda self, *args, **kwargs: self._send_fake(*args, **kwargs), } @classmethod def check_preconditions(cls, module_factory): l = [] if not HAS_DNSPYTHON: l.append( 'DNSPython support missing or broken. Please install dnspython or proceed to an update.' ) l += super(Ability, cls).check_preconditions(module_factory) return l def _parse_zones(self): try: pz = zone_parser.from_file(self.policy_zone, origin='.', relativize=False, check_origin=False) except: if not self.quiet: self._view.error('Invalid policy zone file') return None, None try: fz = zone_parser.from_file(self.fake_zone, origin='.', relativize=False, check_origin=False) except: if not self.quiet: self._view.error('Invalid fake zone file') return None, None return fz, pz def _find_zone_match(self, zone, name, rdtype): try: while name != dns.name.root: found_rrset = None try: found_rrset = zone.find_rrset(name, rdtype) except KeyError: pass if found_rrset is None: try: found_rrset = zone.find_rrset(name, dns.rdatatype.CNAME) except KeyError: pass if found_rrset is None: try: found_rrset = zone.find_rrset(name, dns.rdatatype.DNAME) except KeyError: pass if found_rrset is not None: return found_rrset if name.labels[0] == '*': name = name.parent() name = dns.name.Name(['*'] + list(name.parent().labels)) except dns.name.NoParent: pass raise KeyError('No matching record for {} {}'.format(name, rdtype)) def _set_flags(self, dns_msg): dns_msg.flags |= dns.flags.QR if self.resolver: if dns_msg.flags & dns.flags.RD != 0: dns_msg.flags |= dns.flags.RA # If flag AD and authentic are both set, then nothing happens, and this is all good if self.authentic: if dns_msg.ednsflags & dns.flags.DO != 0 and dns_msg.flags & dns.flags.CD == 0: dns_msg.flags |= dns.flags.AD else: dns_msg.flags &= (2**11) - 1 - dns.flags.AD # Revert AD byte else: dns_msg.flags |= dns.flags.AA def _fake_answer(self, fz, metadata, dns_msg, fake_rrset): qdrrset = dns_msg.question[0] self._set_flags(dns_msg) if qdrrset.rdtype in [ dns.rdatatype.NS, dns.rdatatype.MX, dns.rdatatype.SRV ]: if qdrrset.rdtype in [dns.rdatatype.NS, dns.rdatatype.SRV]: names = [rr.target for rr in fake_rrset.items] elif qdrrset.rdtype == dns.rdatatype.MX: names = [rr.exchange for rr in fake_rrset.items] addr_rrset = [] for name in names: try: addr_rrset.append( self._find_zone_match(fz, name, dns.rdatatype.A)) except KeyError: pass try: addr_rrset.append( self._find_zone_match(fz, name, dns.rdatatype.AAAA)) except KeyError: pass dns_msg.additional = addr_rrset dns_msg.answer.append(fake_rrset) self._send('\x00{}{}{}'.format(struct.pack('!H', len(metadata)), metadata, dns_msg.to_wire())) def _send_empty_response(self, metadata, parsed, rcode): parsed.set_rcode(rcode) parsed.answer = [] parsed.authority = [] parsed.additional = [] self._send('\x00{}{}{}'.format(struct.pack('!H', len(metadata)), metadata, str(parsed.to_wire()))) def _send_negative_answer(self, fz, metadata, parsed, rcode): self._set_flags(parsed) name = parsed.question[0].name forged_soa = dns.rrset.from_text( name, 3600 * 3, dns.rdataclass.IN, dns.rdatatype.from_text('SOA'), 'ns1.{} hostmaster.{} 1 {} {} {} {}'.format( name.to_text(), name.to_text(), 3 * 3600, 3600, 86400 * 7, 3 * 3600)) parsed.set_rcode(rcode) parsed.answer = [] parsed.authority = [forged_soa] parsed.additional = [] self._send('\x00{}{}{}'.format(struct.pack('!H', len(metadata)), metadata, str(parsed.to_wire()))) def _send_nodata(self, fz, metadata=None, parsed=None): if parsed is None or metadata is None: if not self.quiet: self._view.error('Parsed packet unavailable. Dropping') return self._send_negative_answer(fz, metadata, parsed, 0) def _send_nxdomain(self, fz, metadata=None, parsed=None): if parsed is None or metadata is None: if not self.quiet: self._view.error('Parsed packet unavailable. Dropping') return self._send_negative_answer(fz, metadata, parsed, 3) def _send_servfail(self, fz, metadata=None, parsed=None): if parsed is None or metadata is None: if not self.quiet: self._view.error('Parsed packet unavailable. Dropping') return self._set_flags(parsed) self._send_empty_response(metadata, parsed, 2) def _send_truncated(self, fz, metadata=None, parsed=None): if parsed is None or metadata is None: if not self.quiet: self._view.error('Parsed DNS message unavailable. Dropping') return self._set_flags(parsed) parsed.flags |= dns.flags.TC self._send_empty_response(metadata, parsed, 0) def _send_passthru(self, fz, metadata=None, raw=None, parsed=None): if (raw is None and parsed is None) or metadata is None: if not self.quiet: self._view.error('Raw DNS message unavailable. Dropping') self._send('\xFF{}{}{}'.format( struct.pack('!H', len(metadata)), metadata, raw if raw is not None else str(parsed.to_wire()))) def _send_fake(self, fz, metadata=None, parsed=None): if parsed is None or metadata is None: if not self.quiet: self._view.error('Parsed DNS message unavailable. Dropping') return try: rrset = self._find_zone_match(fz, parsed.question[0].name, parsed.question[0].rdtype) if rrset.name.to_text().startswith('*'): rrset.name = parsed.question[0].name self._fake_answer(fz, metadata, parsed, rrset) except KeyError: if not self.quiet: self._view.error( 'Fake policy but not matching fake record. Dropping') return def _find_policy(self, pz, rrset): try: policy_rrset = self._find_zone_match(pz, rrset.name, dns.rdatatype.TXT) verdict = None for item in policy_rrset.items: for string in item.strings: rrtype, policy = string.split(' ') if rrtype == 'ANY' or dns.rdatatype.from_text( rrtype) == rrset.rdtype: if policy == 'NXDOMAIN': if rrtype == 'ANY': verdict = self.NXDOMAIN_POLICY elif verdict is None: verdict = self.NODATA_POLICY elif policy == 'NODATA': verdict = self.NODATA_POLICY elif policy == 'SERVFAIL': verdict = self.SERVFAIL_POLICY elif policy == 'PASSTHRU': verdict = self.PASSTHRU_POLICY elif policy == 'TCP': verdict = self.TCP_POLICY else: # Policy == FAKE or whatever else verdict = self.FAKE_POLICY if verdict is not None: return verdict return None except Exception as e: print e return None def _handle_query(self, metadata, data, fz, pz): try: dns_msg = message_parser.from_wire(data) except (message_parser.ShortHeader, message_parser.TrailingJunk, dns.name.BadLabelType): self._view.error( 'Error while parsing DNS message. Pass Thru policy applied.') self.DECISION_DICT[self.PASSTHRU_POLICY](self, fz, metadata=metadata, raw=data) return # Figuring out which policy to apply verdict = self._find_policy(pz, dns_msg.question[0]) if verdict is None: self._view.error('Could not determine a verdict. Dropping.') return self.DECISION_DICT[verdict](self, fz, metadata=metadata, parsed=dns_msg) def main(self): fz, pz = self._parse_zones() if fz is None or pz is None: return try: while not self.is_stopped(): if self._poll(0.05): s = self._recv() metadata_len, = struct.unpack('!H', s[:2]) metadata = s[2:metadata_len + 2] data = s[metadata_len + 2:] self._handle_query(metadata, data, fz, pz) except (IOError, EOFError): pass
class Ability(ns.ThreadedAbilityBase): _option_list = [ ns.PathOpt('fake_zone', must_exist=True, readable=True, is_dir=False), ns.PathOpt('policy_zone', must_exist=True, readable=True, is_dir=False), ns.IpOpt(ns.OptNames.IP_SRC, default=None, optional=True), ns.IpOpt(ns.OptNames.IP_DST, default=None, optional=True), ns.PortOpt(ns.OptNames.PORT_DST, optional=True, default=53), ns.NICOpt(ns.OptNames.INPUT_INTERFACE), ns.NICOpt(ns.OptNames.OUTPUT_INTERFACE, default=None, optional=True), ns.BoolOpt('quiet', default=True) ] _info = ns.AbilityInfo( name='DNSProxy', description='Replacement for DNSProxy', authors=['Florian Maury', ], tags=[ns.Tag.TCP_STACK_L5, ns.Tag.THREADED, ns.Tag.DNS], type=ns.AbilityType.STANDALONE ) _dependencies = [ 'mitm', ('dnsproxysrv', 'base', 'DNSProxy Server'), ('scapy_splitter', 'base', 'DNS Metadata Extractor'), ('scapy_unsplitter', 'base', 'DNS Metadata Reverser'), ] def main(self): dns_srv_abl = self.get_dependency( 'dnsproxysrv', fake_zone=self.fake_zone, policy_zone=self.policy_zone, quiet=self.quiet ) mitm_abl = self.get_dependency( 'mitm', interface=self.interface, outerface=self.outerface, ip_src=self.ip_src, ip_dst=self.ip_dst, port_dst=self.port_dst, protocol='udp', mux=True ) scapy_dns_metadata_splitter = self.get_dependency('scapy_splitter', quiet=self.quiet) scapy_dns_metadata_reverser = self.get_dependency('scapy_unsplitter', quiet=self.quiet) mitm_abl | scapy_dns_metadata_splitter | dns_srv_abl | scapy_dns_metadata_reverser | mitm_abl self._start_wait_and_stop( [dns_srv_abl, mitm_abl, scapy_dns_metadata_reverser, scapy_dns_metadata_splitter] ) def howto(self): print("""This DNS proxy intercepts DNS requests at OSI layer 2. For each intercepted request, this proxy can either fake an answer and send it to the requester or forward the request to the original recipient. Fake answers are authoritative, and they may contain DNS records for IPv4 or IPv6 addresses, denials of existence (nxdomain or empty answers), errors (SERVFAIL or truncated answer), NS records, MX records, and in fact whatever record you can think of. Special handling is done for denials of existence, NS records, MX records, and SRV records, to either synthesize a SOA record or add the corresponding glues whenever available. Whether to fake answers is instructed through a DNS master file that contains policies. Policies are formated as TXT records whose first word is the mnemonic of a record type (A, AAAA, NS, etc.) or the "ANY" keyword. ANY means all types of records. The second word is the policy decision. It can be one of the following: * PASSTHRU: the request is forwarded, unaltered, to the original destination. * NODATA: the request is answered with the indication that there is no such DNS record for this record type at the requested domain name. * NXDOMAIN: the request is answered with the indication that the requested domain name does not exist and that no records can be found at this name and under it. This policy only works only with the keyword "ANY". * SERVFAIL: the request is answered with the indication that the server is unable to provide a valid answer at the moment. This will generally force implementations to retry the request against another server, whenever possible. * TCP: the request is answered with an empty answer and the indication that the complete answer would truncated. This will force RFC-compliant implementation to retry the request over TCP. TCP is currently unsupported by this ability. * FAKE: the request is answered with fake data as specified in the fake zone, as described hereunder. The policy zone file must contain records whose owner name is fully qualified domain names. For instance, to fake a request for the IPv4 address of ssi.gouv.fr, one would write in the policy file: ssi.gouv.fr. IN TXT "A FAKE" The policy zone file can use wildcards to cover all domain names under some domain name. For instance, to let through all requests for all domain names under the fr TLD, one would write: *.fr IN TXT "ANY PASSTHRU" The wildcard matching is similar to that of the DNS. That means that if both previous policies are in the policy file, all requests for any records and names under the fr TLD would be let through, save for a request for the IPv4 of ssi.gouv.fr. If two policies are defined for a given name (be it an ANY policy and a record type-specific policy or two ANY policies or even two exact match policies), the first record to match is used. Thus, one can write a default policy using the wildcard expression "*.". For instance, to answer that there is no record for any NAPTR record, whatever the requested name is, and unless there is an explicit other policy to apply, one would write: *. IN TXT "NAPTR NODATA" If no policy can be found for a domain name and a record type, the request is dropped. If the received request cannot be parsed into a valid DNS message, the "packet" is let through. We think this is a reasonable behaviour, because it might not be at all a DNS request. The fake zone file is also a DNS master file containing all the records required to synthesize the fake answer, as instructed by the policy. For instance, according to the previously described policy for ssi.gouv.fr IPv4, one would have to write something among the likes of: ssi.gouv.fr. 3600 IN A 127.0.0.1 This would instruct this ability to answer "ssi.gouv.fr. A?" requests with a fake answer with a TTL of 1 hour, and an IPv4 address equal to 127.0.0.1. All domain names in the fake zone file must also be fully-qualified, and wildcards also apply, as described before. For example, the following files could be used: --- Policy file: --- *. IN TXT "NAPTR NODATA" *.fr. IN TXT "ANY PASSTHRU" ssi.gouv.fr. IN TXT "A FAKE" ssi.gouv.fr. IN TXT "AAAA FAKE" --- Fake zone file: --- ssi.gouv.fr. 3600 IN A 127.0.0.1 ssi.gouv.fr. 7200 IN AAAA 2001:db8::1 --- The IP parameters and the destination port serves to better target the requests for which to answer fake records. The input NIC is the network card connected to the victim. The output NIC is optional; it may be specified if the real DNS server is connected to a different card than the victim. """)
class Ability(ns.AbilityBase): _info = ns.AbilityInfo( name='Demo options', description='Demonstrate all available options', tags=[ns.Tag.EXAMPLE], ) _option_list = [ ns.ChoiceOpt('option', ['normal', 'bypass_cache'], default='normal', comment='Define if cache must be bypassed ' 'when using generators (except "nb")'), ns.NumOpt('nb', default=3, comment='Times to display everything'), ns.IpOpt(ns.OptNames.IP_DST, default='127.0.0.1', comment='use as default the standardized dst_ip option name'), ns.StrOpt('msg', default='my message', comment='A string message'), ns.PortOpt(ns.OptNames.PORT_DST, default=2222, comment='A string message'), ns.MacOpt(ns.OptNames.MAC_SRC, default='Mac00', comment='Source MAC address'), ns.BoolOpt('a_bool', default=True, comment='A True/False value'), ns.PathOpt('path', default='pw.ini', comment='Path to an existing file') # must_exist=True), ] def display(self): for i in range(self.nb): self._view.delimiter('Round {}'.format(i + 1)) self._view.info('[{}] - {} - {}'.format(self.mac_src, self.ip_dst, self.port_dst)) self._view.progress('{}'.format(self.msg)) self._view.debug('{}'.format(self.a_bool)) self._view.warning('{} (abs: {})'.format( self.path, os.path.abspath(self.path))) self._view.delimiter() self._view.info('') def display_bypass_cache(self): for i in range(self.nb): self._view.delimiter('Round {}'.format(i + 1)) self._view.info('[{}] - {} - {}'.format( self.get_opt('mac_src', bypass_cache=True), self.get_opt('ip_dst', bypass_cache=True), self.get_opt('port_dst', bypass_cache=True), )) self._view.progress('{}'.format( self.get_opt('msg', bypass_cache=True))) self._view.debug('{}'.format( self.get_opt('a_bool', bypass_cache=True))) self._view.warning('{} (abs: {})'.format( self.get_opt('path', bypass_cache=True), os.path.abspath(self.get_opt('path', bypass_cache=True)))) self._view.delimiter() self._view.info('') def main(self): if self.nb <= 0: self._view.error( 'The number must be greater than 0 ({} given)'.format(self.nb)) return elif self.nb > 2000: self._view.warning('{} rounds is quite a lot! ' 'Please try with a lower number.'.format( self.nb)) return if self.option == 'normal': self.display() elif self.option == 'bypass_cache': self.display_bypass_cache() self._view.success('Done!') return 'Done' def howto(self): self._view.delimiter('Module option demonstration') self._view.info(""" This ability make use of all the PacketWeaver framework supported options. Their names are either specified using a label, or a predefined value using a OptNames.VAL . The latter solution is preferred as it helps getting a clean input interface across different abilities. You may play with the different options, modifying their value with either: - a fixed value - a fixed value randomly drawn (e.g RandIP4() for the dst_ip) - a random generator (e.g RandIP4) The ability will display their value three times so you can see how they behave. """)