Repository: secdev/scapy

Issue:
DNSRR problem: not able to populate rdata on type=TXT records properly
#### Brief description

DNSRR stack does not allow rdata field to be populated properly when the type of record is TXT.


#### Environment

- Scapy version: 2.4.0
- Python version: 3.6
- Operating System: Ubuntu 17.10

#### How to reproduce

I'm trying to populate rdata in DNSRR (type=TXT), and it seems it only allows one 'subfield' to be populated.
The format for a TXT record is (as per it is being used):
(on rdlen field for the entire DNSRR record) and N of the below:
TXT length subfield 1
TXT subfield 1 (key1=value1)
TXT length subfield 2
TXT subfield 2 (key2=value2)
........
TXT length subfield N
TXT subfield N (keyN=valueN)
It seems that the rdata in scapy for TXT records only allows one of those subfields above.
I do not know how I can insert more than one subfield of the above.
When I insert the first subfield, it goes OK. It automatically inserts the subfield length associated, and
updates the global rdlen. But I have no way to insert additional subfields. I have tried to do it "manually", providing a string with all strings concatenated, including their lengths, but scapy then considers it as  only one subfield, and the auto-calculated subfield length does not reflect the real structure.
Maybe I'm missing something.

#### Actual result

I cannot provide more than one field to rdata, which is needed for TXT records.

#### Expected result

Being able to add more than one subfield to the rdata field.

Thanks and regards.
-Bob V



Discussion:
Please provide an example of code.

Hello.

Ok.
I'm stacking three  additional resource records as part of the DNS reponse like this:

```
    # Answer section:
    packet[DNS].an = DNSRR(type='PTR', ttl=TTL_IN_RECORD)

    # Additional records section:
    ar1 = DNSRR(type='TXT', rclass=0x8001, ttl=TTL_TXT_IN_RECORD)
    ar2 = DNSRR(type='SRV', rclass=0x8001, ttl=TTL_IN_RECORD)
    ar3 = DNSRR(type='A', rclass=0x8001, ttl=TTL_IN_RECORD)

    # Stacking the three additional responses:
    packet[DNS].ar = ar1/ar2/ar3
```

Now, for the ar1 part, which is a TXT record, if I do, for instance:

`    packet[DNS].ar[0].rdata = '\x0agg=3242343\x10hh=kasdkfksadsad'
`
You can see that there are two subrecords:
gg=3242343
hh=kasdkfksadsad

each of them is prepended with their respective lengths byte. (0x0a = 10 bytes and 0x10 = 16 bytes)

When I send the packet out to the network, a wireshark(packet) statement shows ... (see attachment)

![screenshot from 2018-04-25 13-39-54](https://user-images.githubusercontent.com/16015875/39243593-426056aa-488e-11e8-9028-acdf28f4cf9a.png)


You can see in the selected section that the rdata field is not being considered as a "list" of pairs (length, field), but instead it is considered as one string. It puts the TXT length to 28 (total length). On top of that, you have the rdlen, which is OK.
I have tried different approaches: not including the length bytes, using a list of strings, ...

But none of them worked.

Not sure if somehow the RDataField class is not chopping the rdata field or something like that.

Thanks and best regards.
-Bob V



Scapy does not fill record itself:
```
>>> raw(DNSRR(type="A", rdata="1.2.3.4"))
'\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x04\x01\x02\x03\x04'
>>> raw(DNSRR(type="A"))
'\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00'
```
Providing data to the record works:
```
>>> dns = DNS()
>>> dns.an = DNSRR(type="TXT", rdata="hello")/ DNSRR(type="TXT", rdata="scapy")
>>> DNS(raw(dns))
<DNS  id=0 qr=0 opcode=QUERY aa=0 tc=0 rd=1 ra=0 z=0 ad=0 cd=0 rcode=ok qdcount=0 ancount=2 nscount=0 arcount=0 qd=None an=<DNSRR  rrname='.' type=TXT rclass=IN ttl=0 rdata='hello' |<DNSRR  rrname='.' type=TXT rclass=IN ttl=0 rdata='scapy' |>> ns=None ar=None |>
```
Did you try providing data to your records? Please reopen if this does not work.
Hi.
Thanks for the prompt followup on this.

But there is one thing I do not understand:
You are "concatenating" two individual DNSRR TXT records.
That is not exactly what I meant to say.
I was talking about having one individual DNSRR TXT record, with a certain number of "subrecords".
I mean, only one total rdlength should be generated for this DNSRR record, and that total length should be the total sum of the included TXT subrecords, each of them with its own length byte.
Does Scapy merge those two DNSRR TXT records into one automagically?
If so, what if you want them to be separated? I mean, having one DNSRR TXT record1, DNSRR SRV record2, and DNSRR TXT record3, without merging records 1 and 3? 

Thanks a lot.
-Bob V
Can you point me to a RFC? I never heard of a DNS sub-record before. 

It was just my own slang  :-)

If you take a look at the wireshark snapshot attached, you will understand what I'm trying to say:
![dns](https://user-images.githubusercontent.com/16015875/39356680-f1b24cac-4a10-11e8-82a8-64f0fd09b701.png)

It is a DNS response with three additional responses. As per my understanding, that would be 3 DNSRR "instances". The first of them is a TXT record. The other two are SRV and A types, but we can leave them out of this discussion.
So, focusing on the TXT record:
The rdlength is 180.
What I call subrecord is:
- Subrecord 1:
Length: 35
key: id
value: fa1cc.....
- Subrecord2:
Length: 35
key: cd
value: 0FA ....

So, to sum up, there is only one DNSRR record for this TXT section. In its rdata field, there is a bunch of key=value pairs, with their own length, but that is internal to the field itself. One one rdlength is included.

When you do: DNSRR(type="TXT", rdata="hello")/ DNSRR(type="TXT", rdata="scapy")
Are you creating one record or two (each with its own rdlength)?

Thanks a lot.
-Bob V

Long story short: I learnt something today, and Scapy does not correctly support TXT records =)

I will try to provide a fix "soon". The DNS layer being one of the oldest onem it might take long to fix.
Ok, no problem. Is there a quick way to somehow workaround it?
Something like passing a whole binary bunch of data to the enclosing DNSRR?
In fact that is one of the things I tried before and as soon as it sees type=TXT, the subrecord length is automatically added.
Not sure how to workaround it...

Thanks.
I can't thinkf of anything easier than patching the raw DNS packet.

Resolving patch:
diff --git a/scapy/layers/dns.py b/scapy/layers/dns.py
--- a/scapy/layers/dns.py
+++ b/scapy/layers/dns.py
@@ -199,6 +199,8 @@ def decodeRR(self, name, s, p):
 
 
 class RDataField(StrLenField):
+    islist = 1
+
     def m2i(self, pkt, s):
         family = None
         if pkt.type == 1:  # A
@@ -214,7 +216,7 @@ def m2i(self, pkt, s):
                 else:
                     s = DNSgetstr(s, 0)[0]
         elif pkt.type == 16:  # TXT
-            ret_s = b""
+            ret_s = list()
             tmp_s = s
             # RDATA contains a list of strings, each are prepended with
             # a byte containing the size of the following string.
@@ -222,7 +224,7 @@ def m2i(self, pkt, s):
                 tmp_len = orb(tmp_s[0]) + 1
                 if tmp_len > len(tmp_s):
                     warning("DNS RR TXT prematured end of character-string (size=%i, remaining bytes=%i)" % (tmp_len, len(tmp_s)))
-                ret_s += tmp_s[1:tmp_len]
+                ret_s.append(tmp_s[1:tmp_len])
                 tmp_s = tmp_s[tmp_len:]
             s = ret_s
         elif pkt.type == 28:  # AAAA
@@ -240,18 +242,17 @@ def i2m(self, pkt, s):
             if orb(s[-1]):
                 s += b"\x00"
         elif pkt.type == 16:  # TXT
-            if s:
-                s = raw(s)
-                ret_s = b""
+            ret_s = b""
+            for text in s:
                 # The initial string must be splitted into a list of strings
                 # prepended with theirs sizes.
-                while len(s) >= 255:
-                    ret_s += b"\xff" + s[:255]
-                    s = s[255:]
+                while len(text) >= 255:
+                    ret_s += b"\xff" + text[:255]
+                    text = text[255:]
                 # The remaining string is less than 255 bytes long
-                if len(s):
-                    ret_s += struct.pack("!B", len(s)) + s
-                s = ret_s
+                if len(text):
+                    ret_s += struct.pack("!B", len(text)) + raw(text)
+            s = ret_s
         elif pkt.type == 28:  # AAAA
             if s:
                 s = inet_pton(socket.AF_INET6, s)


Test patch:
diff --git a/test/dnssecRR.uts b/test/dnssecRR.uts
--- a/test/dnssecRR.uts
+++ b/test/dnssecRR.uts
@@ -116,6 +116,8 @@ t.rrname == b'isc.org.' and t.keytag == 12892 and t.algorithm == 5 and t.digestt
 
 = DNSRR(type="TXT") instanciation
 t = DNSRR(type="TXT", rdata="test")
+assert t.rdata == b"test"
+assert raw(DNSRR(type="TXT", rdata=["hello", "DNS", "!"])) == b'\x00\x00\x10\x00\x01\x00\x00\x00\x00\x00\x0c\x05hello\x03DNS\x01!'
 
 = Build DNSRR
 an = DNSRR(type='AAAA', rdata='2001::1')
@@ -124,7 +126,7 @@ assert an.rdata == '2001::1'
 
 = DNSRRR(), check parameters
 t = DNSRR(b'\x04test\x00\x00\x10\x00\x01\x00\x00\x00\x00\x018\xffScapy is an interactive packet manipulation program that enables you to sniff, mangle, send network packets ; test equipments ; probe and discover networks ; quickly develop new protocols. It can easily handle most classical tasks like scanning, tracerout7ing, probing, unit tests, attacks or network discovery.')
-t.type == 16 and t.rdlen == 312 and t.rdata[:5] == b"Scapy" and t.rdata[-10:] == b"discovery."
+t.type == 16 and t.rdlen == 312 and t.rdata[0][:5] == b"Scapy" and t.rdata[1][-10:] == b"discovery."
 
 + Test DNSRRTSIG RR