Commit message: Reject replies to DNS PTR requests that contain invalid characters

Fixes issue #1033 reported by @ping86
CVE: CVE-2015-8702
CWE: CWE-416
Vulnerability description: The DNS::GetResult function in dns.cpp in InspIRCd before 2.0.19 allows remote DNS servers to cause a denial of service (netsplit) via an invalid character in a PTR response, as demonstrated by a "\032" (whitespace) character in a hostname.

Project: inspircd

DNS::DNS()
{
	ServerInstance->Logs->Log("RESOLVER",DEBUG,"DNS::DNS");
	/* Clear the Resolver class table */
	memset(Classes,0,sizeof(Classes));

	/* Clear the requests class table */
	memset(requests,0,sizeof(requests));

	/* DNS::Rehash() sets this to a valid ptr
	 */
	this->cache = NULL;

	/* Again, DNS::Rehash() sets this to a
	 * valid value
	 */
	this->SetFd(-1);

	/* Actually read the settings
	 */
	this->Rehash();

	this->PruneTimer = new CacheTimer(this);

	ServerInstance->Timers->AddTimer(this->PruneTimer);
}

Project: inspircd

bool DNS::AddResolverClass(Resolver* r)
{
	ServerInstance->Logs->Log("RESOLVER",DEBUG,"AddResolverClass 0x%08lx", (unsigned long)r);
	/* Check the pointers validity and the id's validity */
	if ((r) && (r->GetId() > -1))
	{
		/* Check the slot isnt already occupied -
		 * This should NEVER happen unless we have
		 * a severely broken DNS server somewhere
		 */
		if (!Classes[r->GetId()])
		{
			/* Set up the pointer to the class */
			Classes[r->GetId()] = r;
			return true;
		}
	}

	/* Pointer or id not valid, or duplicate id.
	 * Free the item and return
	 */
	delete r;
	return false;
}

Project: inspircd

DNSResult DNS::GetResult()
{
	/* Fetch dns query response and decide where it belongs */
	DNSHeader header;
	DNSRequest *req;
	unsigned char buffer[sizeof(DNSHeader)];
	irc::sockets::sockaddrs from;
	memset(&from, 0, sizeof(from));
	socklen_t x = sizeof(from);

	int length = ServerInstance->SE->RecvFrom(this, (char*)buffer, sizeof(DNSHeader), 0, &from.sa, &x);

	/* Did we get the whole header? */
	if (length < 12)
	{
		ServerInstance->Logs->Log("RESOLVER",DEBUG,"GetResult didn't get a full packet (len=%d)", length);
		/* Nope - something screwed up. */
		return DNSResult(-1,"",0,"");
	}

	/* Check wether the reply came from a different DNS
	 * server to the one we sent it to, or the source-port
	 * is not 53.
	 * A user could in theory still spoof dns packets anyway
	 * but this is less trivial than just sending garbage
	 * to the server, which is possible without this check.
	 *
	 * -- Thanks jilles for pointing this one out.
	 */
	if (from != myserver)
	{
		std::string server1 = from.str();
		std::string server2 = myserver.str();
		ServerInstance->Logs->Log("RESOLVER",DEBUG,"Got a result from the wrong server! Bad NAT or DNS forging attempt? '%s' != '%s'",
			server1.c_str(), server2.c_str());
		return DNSResult(-1,"",0,"");
	}

	/* Put the read header info into a header class */
	DNS::FillHeader(&header,buffer,length - 12);

	/* Get the id of this request.
	 * Its a 16 bit value stored in two char's,
	 * so we use logic shifts to create the value.
	 */
	unsigned long this_id = header.id[1] + (header.id[0] << 8);

	/* Do we have a pending request matching this id? */
	if (!requests[this_id])
	{
		/* Somehow we got a DNS response for a request we never made... */
		ServerInstance->Logs->Log("RESOLVER",DEBUG,"Hmm, got a result that we didn't ask for (id=%lx). Ignoring.", this_id);
		return DNSResult(-1,"",0,"");
	}
	else
	{
		/* Remove the query from the list of pending queries */
		req = requests[this_id];
		requests[this_id] = NULL;
	}

	/* Inform the DNSRequest class that it has a result to be read.
	 * When its finished it will return a DNSInfo which is a pair of
	 * unsigned char* resource record data, and an error message.
	 */
	DNSInfo data = req->ResultIsReady(header, length);
	std::string resultstr;

	/* Check if we got a result, if we didnt, its an error */
	if (data.first == NULL)
	{
		/* An error.
		 * Mask the ID with the value of ERROR_MASK, so that
		 * the dns_deal_with_classes() function knows that its
		 * an error response and needs to be treated uniquely.
		 * Put the error message in the second field.
		 */
		std::string ro = req->orig;
		delete req;
		return DNSResult(this_id | ERROR_MASK, data.second, 0, ro);
	}
	else
	{
		unsigned long ttl = req->ttl;
		char formatted[128];

		/* Forward lookups come back as binary data. We must format them into ascii */
		switch (req->type)
		{
			case DNS_QUERY_A:
				snprintf(formatted,16,"%u.%u.%u.%u",data.first[0],data.first[1],data.first[2],data.first[3]);
				resultstr = formatted;
			break;

			case DNS_QUERY_AAAA:
			{
				if (!inet_ntop(AF_INET6, data.first, formatted, sizeof(formatted)))
				{
					std::string ro = req->orig;
					delete req;
					return DNSResult(this_id | ERROR_MASK, "inet_ntop() failed", 0, ro);
				}

				resultstr = formatted;

				/* Special case. Sending ::1 around between servers
				 * and to clients is dangerous, because the : on the
				 * start makes the client or server interpret the IP
				 * as the last parameter on the line with a value ":1".
				 */
				if (*formatted == ':')
					resultstr.insert(0, "0");
			}
			break;

			case DNS_QUERY_CNAME:
				/* Identical handling to PTR */

			case DNS_QUERY_PTR:
			{
				/* Reverse lookups just come back as char* */
				resultstr = std::string((const char*)data.first);
				if (resultstr.find_first_not_of("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-") != std::string::npos)
				{
					std::string ro = req->orig;
					delete req;
					return DNSResult(this_id | ERROR_MASK, "Invalid char(s) in reply", 0, ro);
				}
			}
			break;

			default:
			break;
		}

		/* Build the reply with the id and hostname/ip in it */
		std::string ro = req->orig;
		DNSResult result = DNSResult(this_id,resultstr,ttl,ro,req->type);
		delete req;
		return result;
	}
}

Project: inspircd

CachedQuery::CachedQuery(const std::string &res, QueryType qt, unsigned int ttl) : data(res), type(qt)
{
	expires = ServerInstance->Time() + ttl;
}

Project: inspircd

void DNS::HandleEvent(EventType, int)
{
	/* Fetch the id and result of the next available packet */
	DNSResult res(0,"",0,"");
	res.id = 0;
	ServerInstance->Logs->Log("RESOLVER",DEBUG,"Handle DNS event");

	res = this->GetResult();

	ServerInstance->Logs->Log("RESOLVER",DEBUG,"Result id %d", res.id);

	/* Is there a usable request id? */
	if (res.id != -1)
	{
		/* Its an error reply */
		if (res.id & ERROR_MASK)
		{
			/* Mask off the error bit */
			res.id -= ERROR_MASK;
			/* Marshall the error to the correct class */
			if (Classes[res.id])
			{
				if (ServerInstance && ServerInstance->stats)
					ServerInstance->stats->statsDnsBad++;
				Classes[res.id]->OnError(RESOLVER_NXDOMAIN, res.result);
				delete Classes[res.id];
				Classes[res.id] = NULL;
			}
			return;
		}
		else
		{
			/* It is a non-error result, marshall the result to the correct class */
			if (Classes[res.id])
			{
				if (ServerInstance && ServerInstance->stats)
					ServerInstance->stats->statsDnsGood++;

				if (!this->GetCache(res.original.c_str()))
					this->cache->insert(std::make_pair(res.original.c_str(), CachedQuery(res.result, res.type, res.ttl)));

				Classes[res.id]->OnLookupComplete(res.result, res.ttl, false);
				delete Classes[res.id];
				Classes[res.id] = NULL;
			}
		}

		if (ServerInstance && ServerInstance->stats)
			ServerInstance->stats->statsDns++;
	}
}

Project: inspircd

DNSRequest* DNS::AddQuery(DNSHeader *header, int &id, const char* original)
{
	/* Is the DNS connection down? */
	if (this->GetFd() == -1)
		return NULL;

	/* Create an id */
	unsigned int tries = 0;
	do {
		id = ServerInstance->GenRandomInt(DNS::MAX_REQUEST_ID);
		if (++tries == DNS::MAX_REQUEST_ID*5)
		{
			// If we couldn't find an empty slot this many times, do a sequential scan as a last
			// resort. If an empty slot is found that way, go on, otherwise throw an exception
			id = -1;
			for (int i = 0; i < DNS::MAX_REQUEST_ID; i++)
			{
				if (!requests[i])
				{
					id = i;
					break;
				}
			}

			if (id == -1)
				throw ModuleException("DNS: All ids are in use");

			break;
		}
	} while (requests[id]);

	DNSRequest* req = new DNSRequest(this, id, original);

	header->id[0] = req->id[0] = id >> 8;
	header->id[1] = req->id[1] = id & 0xFF;
	header->flags1 = FLAGS_MASK_RD;
	header->flags2 = 0;
	header->qdcount = 1;
	header->ancount = 0;
	header->nscount = 0;
	header->arcount = 0;

	/* At this point we already know the id doesnt exist,
	 * so there needs to be no second check for the ::end()
	 */
	requests[id] = req;

	/* According to the C++ spec, new never returns NULL. */
	return req;
}

Project: inspircd

Resolver::Resolver(const std::string &source, QueryType qt, bool &cached, Module* creator) : Creator(creator), input(source), querytype(qt)
{
	ServerInstance->Logs->Log("RESOLVER",DEBUG,"Resolver::Resolver");
	cached = false;

	CQ = ServerInstance->Res->GetCache(source);
	if (CQ)
	{
		time_left = CQ->CalcTTLRemaining();
		if (!time_left)
		{
			ServerInstance->Res->DelCache(source);
		}
		else if (CQ->type == qt)
		{
			cached = true;
			return;
		}
		CQ = NULL;
	}

	switch (querytype)
	{
		case DNS_QUERY_A:
			this->myid = ServerInstance->Res->GetIP(source.c_str());
		break;

		case DNS_QUERY_PTR4:
			querytype = DNS_QUERY_PTR;
			this->myid = ServerInstance->Res->GetNameForce(source.c_str(), PROTOCOL_IPV4);
		break;

		case DNS_QUERY_PTR6:
			querytype = DNS_QUERY_PTR;
			this->myid = ServerInstance->Res->GetNameForce(source.c_str(), PROTOCOL_IPV6);
		break;

		case DNS_QUERY_AAAA:
			this->myid = ServerInstance->Res->GetIP6(source.c_str());
		break;

		case DNS_QUERY_CNAME:
			this->myid = ServerInstance->Res->GetCName(source.c_str());
		break;

		default:
			ServerInstance->Logs->Log("RESOLVER",DEBUG,"DNS request with unknown query type %d", querytype);
			this->myid = -1;
		break;
	}
	if (this->myid == -1)
	{
		throw ModuleException("Resolver: Couldn't get an id to make a request");
	}
	else
	{
		ServerInstance->Logs->Log("RESOLVER",DEBUG,"DNS request id %d", this->myid);
	}
}