Example #1
0
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()
Example #2
0
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
Example #3
0
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
Example #4
0
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
Example #5
0
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.
""")
Example #6
0
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.
        """)