Skip to content

Commit fcdcac4

Browse files
committed
Add support for forward dns compression pointers
- nslookup supports these and some implementations (likely avahi) will generate them - Careful attention was given to make sure we detect loops and do not create anti-patterns described in https://github.com/Forescout/namewreck/blob/main/rfc/draft-dashevskyi-dnsrr-antipatterns-00.txt
1 parent 4d30c25 commit fcdcac4

2 files changed

Lines changed: 283 additions & 84 deletions

File tree

tests/test_protocol.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,3 +761,150 @@ def test_records_same_packet_share_fate():
761761
first_time = dnsin.answers[0].created
762762
for answer in dnsin.answers:
763763
assert answer.created == first_time
764+
765+
766+
def test_dns_compression_invalid_skips_record():
767+
"""Test our wire parser can skip records we do not know how to parse."""
768+
packet = (
769+
b"\x00\x00\x84\x00\x00\x00\x00\x06\x00\x00\x00\x00\x04_hap\x04_tcp\x05local\x00\x00\x0c"
770+
b"\x00\x01\x00\x00\x11\x94\x00\x16\x13eufy HomeBase2-2464\xc0\x0c\x04Eufy\xc0\x16\x00/"
771+
b"\x80\x01\x00\x00\x00x\x00\x08\xc0\xa6\x00\x04@\x00\x00\x08\xc0'\x00/\x80\x01\x00\x00"
772+
b"\x11\x94\x00\t\xc0'\x00\x05\x00\x00\x80\x00@\xc0=\x00\x01\x80\x01\x00\x00\x00x\x00\x04"
773+
b"\xc0\xa8Dp\xc0'\x00!\x80\x01\x00\x00\x00x\x00\x08\x00\x00\x00\x00\xd1_\xc0=\xc0'\x00"
774+
b"\x10\x80\x01\x00\x00\x11\x94\x00K\x04c#=1\x04ff=2\x14id=38:71:4F:6B:76:00\x08md=T8010"
775+
b"\x06pv=1.1\x05s#=75\x04sf=1\x04ci=2\x0bsh=xaQk4g=="
776+
)
777+
parsed = r.DNSIncoming(packet)
778+
answer = r.DNSNsec(
779+
'eufy HomeBase2-2464._hap._tcp.local.',
780+
const._TYPE_NSEC,
781+
const._CLASS_IN | const._CLASS_UNIQUE,
782+
const._DNS_OTHER_TTL,
783+
'eufy HomeBase2-2464._hap._tcp.local.',
784+
[const._TYPE_TXT, const._TYPE_SRV],
785+
)
786+
assert answer in parsed.answers
787+
788+
789+
def test_dns_compression_points_forward():
790+
"""Test our wire parser can unpack nsec records with compression."""
791+
packet = (
792+
b"\x00\x00\x84\x00\x00\x00\x00\x07\x00\x00\x00\x00\x0eTV Beneden (2)"
793+
b"\x10_androidtvremote\x04_tcp\x05local\x00\x00\x10\x80\x01\x00\x00\x11"
794+
b"\x94\x00\x15\x14bt=D8:13:99:AC:98:F1\xc0\x0c\x00/\x80\x01\x00\x00\x11"
795+
b"\x94\x00\t\xc0\x0c\x00\x05\x00\x00\x80\x00@\tAndroid-3\xc01\x00/\x80"
796+
b"\x01\x00\x00\x00x\x00\x08\xc0\x9c\x00\x04@\x00\x00\x08\xc0l\x00\x01\x80"
797+
b"\x01\x00\x00\x00x\x00\x04\xc0\xa8X\x0f\xc0\x0c\x00!\x80\x01\x00\x00\x00"
798+
b"x\x00\x08\x00\x00\x00\x00\x19B\xc0l\xc0\x1b\x00\x0c\x00\x01\x00\x00\x11"
799+
b"\x94\x00\x02\xc0\x0c\t_services\x07_dns-sd\x04_udp\xc01\x00\x0c\x00\x01"
800+
b"\x00\x00\x11\x94\x00\x02\xc0\x1b"
801+
)
802+
parsed = r.DNSIncoming(packet)
803+
answer = r.DNSNsec(
804+
'TV Beneden (2)._androidtvremote._tcp.local.',
805+
const._TYPE_NSEC,
806+
const._CLASS_IN | const._CLASS_UNIQUE,
807+
const._DNS_OTHER_TTL,
808+
'TV Beneden (2)._androidtvremote._tcp.local.',
809+
[const._TYPE_TXT, const._TYPE_SRV],
810+
)
811+
assert answer in parsed.answers
812+
813+
814+
def test_dns_compression_points_to_itself():
815+
"""Test our wire parser does not loop forever when a compression pointer points to itself."""
816+
packet = (
817+
b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x06domain\x05local\x00\x00\x01"
818+
b"\x80\x01\x00\x00\x00\x01\x00\x04\xc0\xa8\xd0\x05\xc0(\x00\x01\x80\x01\x00\x00\x00"
819+
b"\x01\x00\x04\xc0\xa8\xd0\x06"
820+
)
821+
parsed = r.DNSIncoming(packet)
822+
assert len(parsed.answers) == 1
823+
824+
825+
def test_dns_compression_points_beyond_packet():
826+
"""Test our wire parser does not fail when the compression pointer points beyond the packet."""
827+
packet = (
828+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x06domain\x05local\x00\x00\x01'
829+
b'\x80\x01\x00\x00\x00\x01\x00\x04\xc0\xa8\xd0\x05\xe7\x0f\x00\x01\x80\x01\x00\x00'
830+
b'\x00\x01\x00\x04\xc0\xa8\xd0\x06'
831+
)
832+
parsed = r.DNSIncoming(packet)
833+
assert len(parsed.answers) == 1
834+
835+
836+
def test_dns_compression_generic_failure():
837+
"""Test our wire parser does not loop forever when dns compression is corrupt."""
838+
packet = (
839+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x06domain\x05local\x00\x00\x01'
840+
b'\x80\x01\x00\x00\x00\x01\x00\x04\xc0\xa8\xd0\x05-\x0c\x00\x01\x80\x01\x00\x00'
841+
b'\x00\x01\x00\x04\xc0\xa8\xd0\x06'
842+
)
843+
parsed = r.DNSIncoming(packet)
844+
assert len(parsed.answers) == 1
845+
846+
847+
def test_label_length_attack():
848+
"""Test our wire parser does not loop forever when the name exceeds 253 chars."""
849+
packet = (
850+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x01d\x01d\x01d\x01d\x01d\x01d'
851+
b'\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d'
852+
b'\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d'
853+
b'\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d'
854+
b'\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d'
855+
b'\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d'
856+
b'\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d'
857+
b'\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d'
858+
b'\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x01d\x00\x00\x01\x80'
859+
b'\x01\x00\x00\x00\x01\x00\x04\xc0\xa8\xd0\x05\xc0\x0c\x00\x01\x80\x01\x00\x00\x00'
860+
b'\x01\x00\x04\xc0\xa8\xd0\x06'
861+
)
862+
parsed = r.DNSIncoming(packet)
863+
assert len(parsed.answers) == 0
864+
865+
866+
def test_label_compression_attack():
867+
"""Test our wire parser does not loop forever when exceeding the maximum number of labels."""
868+
packet = (
869+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x03atk\x00\x00\x01\x80'
870+
b'\x01\x00\x00\x00\x01\x00\x04\xc0\xa8\xd0\x05\x03atk\x03atk\x03atk\x03atk\x03'
871+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
872+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
873+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
874+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
875+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
876+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
877+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
878+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
879+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
880+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
881+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
882+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
883+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
884+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
885+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
886+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
887+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
888+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03'
889+
b'atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\x03atk\xc0'
890+
b'\x0c\x00\x01\x80\x01\x00\x00\x00\x01\x00\x04\xc0\xa8\xd0\x06'
891+
)
892+
parsed = r.DNSIncoming(packet)
893+
assert len(parsed.answers) == 1
894+
895+
896+
def test_dns_compression_loop_attack():
897+
"""Test our wire parser does not loop forever when dns compression is in a loop."""
898+
packet = (
899+
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x07\x03atk\x03dns\x05loc'
900+
b'al\xc0\x10\x00\x01\x80\x01\x00\x00\x00\x01\x00\x04\xc0\xa8\xd0\x05\x04a'
901+
b'tk2\x04dns2\xc0\x14\x00\x01\x80\x01\x00\x00\x00\x01\x00\x04\xc0\xa8\xd0\x05'
902+
b'\x04atk3\xc0\x10\x00\x01\x80\x01\x00\x00\x00\x01\x00\x04\xc0\xa8\xd0'
903+
b'\x05\x04atk4\x04dns5\xc0\x14\x00\x01\x80\x01\x00\x00\x00\x01\x00\x04\xc0'
904+
b'\xa8\xd0\x05\x04atk5\x04dns2\xc0^\x00\x01\x80\x01\x00\x00\x00\x01\x00'
905+
b'\x04\xc0\xa8\xd0\x05\xc0s\x00\x01\x80\x01\x00\x00\x00\x01\x00'
906+
b'\x04\xc0\xa8\xd0\x05\xc0s\x00\x01\x80\x01\x00\x00\x00\x01\x00'
907+
b'\x04\xc0\xa8\xd0\x05'
908+
)
909+
parsed = r.DNSIncoming(packet)
910+
assert len(parsed.answers) == 0

0 commit comments

Comments
 (0)