DNS Black List / RBL Checking in Python

Following on from performing basic DNS Lookups in Python, it’s relatively trivial to begin testing DNS Block Lists/Real Time Black Lists for blocked mail server IP addresses. To assist in preventing spam, a number of public and private RBLs are available. These track the IP addresses of mail servers that are known to produce spam, thus allowing recipient mail servers to deny delivery from known spammers.

RBLs operate over DNS. In order to test a RBL, a DNS query is made. As an example, zen.spamhaus.org is a popular RBL. If I wanted to test IP address 148.251.196.147 against the zen.spamhaus.org blocklist, I would reverse the octets in the IP address and then append ‘.zen.spamhaus.org’, i.e. 147.196.251.148.zen.spamhaus.org. I then perform an ‘A’ record lookup on said host:

root@w:~/tmp# host -t a 147.196.251.148.zen.spamhaus.org
Host 147.196.251.148.zen.spamhaus.org not found: 3(NXDOMAIN)

Excellent. IP 148.251.196.147 was not found in zen.spamhaus.org. NXDOMAIN is returned.

Now, to take a known spammer’s IP: 144.76.252.9:

root@w:~/tmp# host -t a 9.252.76.144.zen.spamhaus.org
9.252.76.144.zen.spamhaus.org has address 127.0.0.4

IP 144.76.252.9 IS listed at zen.spamhaus.org. We can now query the TXT record to find out any accompanying data that zen.spamhaus.org provides:

root@w:~/tmp# host -t txt 9.252.76.144.zen.spamhaus.org
9.252.76.144.zen.spamhaus.org descriptive text "http://www.spamhaus.org/query/bl?ip=144.76.252.9"

Moving on.. we can now implement these tests programatically within Python. Here’s a commented example:

import dns.resolver
bl = "zen.spamhaus.org"
myIP = "144.76.252.9"

try:
	my_resolver = dns.resolver.Resolver() #create a new resolver
	query = '.'.join(reversed(str(myIP).split("."))) + "." + bl #convert 144.76.252.9 to 9.252.76.144.zen.spamhaus.org
	answers = my_resolver.query(query, "A") #perform a record lookup. A failure will trigger the NXDOMAIN exception
	answer_txt = my_resolver.query(query, "TXT") #No exception was triggered, IP is listed in bl. Now get TXT record
	print 'IP: %s IS listed in %s (%s: %s)' %(myIP, bl, answers[0], answer_txt[0])
except dns.resolver.NXDOMAIN:
	print 'IP: %s is NOT listed in %s' %(myIP, bl)

This code produces output:

IP: 144.76.252.9 IS listed in zen.spamhaus.org(127.0.0.4: "http://www.spamhaus.org/query/bl?ip=144.76.252.9")

Finally, we can implement multiple blocklists and have the script accept command line input:

import dns.resolver
import sys

bls = ["zen.spamhaus.org", "spam.abuse.ch", "cbl.abuseat.org", "virbl.dnsbl.bit.nl", "dnsbl.inps.de", 
	"ix.dnsbl.manitu.net", "dnsbl.sorbs.net", "bl.spamcannibal.org", "bl.spamcop.net", 
	"xbl.spamhaus.org", "pbl.spamhaus.org", "dnsbl-1.uceprotect.net", "dnsbl-2.uceprotect.net", 
	"dnsbl-3.uceprotect.net", "db.wpbl.info"]

if len(sys.argv) != 2:
	print 'Usage: %s <ip>' %(sys.argv[0])
	quit()

myIP = sys.argv[1]

for bl in bls:
	try:
		my_resolver = dns.resolver.Resolver()
		query = '.'.join(reversed(str(myIP).split("."))) + "." + bl
		answers = my_resolver.query(query, "A")
		answer_txt = my_resolver.query(query, "TXT")
		print 'IP: %s IS listed in %s (%s: %s)' %(myIP, bl, answers[0], answer_txt[0])
	except dns.resolver.NXDOMAIN:
		print 'IP: %s is NOT listed in %s' %(myIP, bl)

This produces the following output:

root@w:~/tmp# ./bl.py 144.76.252.9
IP: 144.76.252.9 IS listed in zen.spamhaus.org (127.0.0.4: "http://www.spamhaus.org/query/bl?ip=144.76.252.9")
IP: 144.76.252.9 is NOT listed in spam.abuse.ch
IP: 144.76.252.9 IS listed in cbl.abuseat.org (127.0.0.2: "Blocked - see http://cbl.abuseat.org/lookup.cgi?ip=144.76.252.9")
IP: 144.76.252.9 is NOT listed in virbl.dnsbl.bit.nl
IP: 144.76.252.9 is NOT listed in dnsbl.inps.de
IP: 144.76.252.9 IS listed in ix.dnsbl.manitu.net (127.0.0.2: "Your e-mail service was detected by mx.selfip.biz (NiX Spam) as spamming at Sat, 22 Nov 2014 11:17:11 +0100. Your admin should visit http://www.dnsbl.manitu.net/lookup.php?value=144.76.252.9")
IP: 144.76.252.9 IS listed in dnsbl.sorbs.net (127.0.0.6: "Currently Sending Spam See: http://www.sorbs.net/lookup.shtml?144.76.252.9")
IP: 144.76.252.9 is NOT listed in bl.spamcannibal.org
IP: 144.76.252.9 IS listed in bl.spamcop.net (127.0.0.2: "Blocked - see http://www.spamcop.net/bl.shtml?144.76.252.9")
IP: 144.76.252.9 IS listed in xbl.spamhaus.org (127.0.0.4: "http://www.spamhaus.org/query/bl?ip=144.76.252.9")
IP: 144.76.252.9 is NOT listed in pbl.spamhaus.org
IP: 144.76.252.9 IS listed in dnsbl-1.uceprotect.net (127.0.0.2: "IP 144.76.252.9 is UCEPROTECT-Level 1 listed. See http://www.uceprotect.net/rblcheck.php?ipr=144.76.252.9")
IP: 144.76.252.9 is NOT listed in dnsbl-2.uceprotect.net
IP: 144.76.252.9 is NOT listed in dnsbl-3.uceprotect.net
IP: 144.76.252.9 IS listed in db.wpbl.info (127.0.0.2: "Spam source - http://wpbl.info/record?ip=144.76.252.9")

Tags: , , , , , , , ,

2 Responses to “DNS Black List / RBL Checking in Python”

  1. I wrote a small script for DNSBL checking that also uses dnspython:

    https://github.com/gsauthof/utility#check-dnsbl

    Looking at your example I have 2 comments:

    1) You can miss some listing in cases where the A query succeeds but the TXT one fails with NXDOMAIN. RFC 5782 states that a list should have TXT – but there is no guarantee – and a MTA/psam-check-app likely just cares about the A one.

    2) dnspython also provides a method for reversing the address -example:

    str(dns.reversename.from_address(‘127.0.0.1’).split(3)[0])

    Advantage: also works for IPv6 addresses.

Leave a Reply