/*
auth-relaytest.c - DSBL SMTP AUTH open relay tester
Copyright (C) 2002 Ian Gulliver
Copyright (C) 2003 Paul Howarth

This program is free software; you can redistribute it and/or modify
it under the terms of version 2 of the GNU General Public License as
published by the Free Software Foundation.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <firedns.h>
#include <firestring.h>
#include "testlib.h"
#include "hmac_md5.h"

#define AUTH_LOGIN 0
#define AUTH_NTLM 1
#define AUTH_CRAM_MD5 2

#define BASE64MAXLEN 256

#define NTLM_ANON_USER "TlRMTVNTUAABAAAAB4IAgAAAAAAAAAAAAAAAAAAAAAA="
#define NTLM_ANON_PASS "TlRMTVNTUAADAAAAAQABAEAAAAAAAAAAQQAAAAAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAABBAAAABYIAAAA="

#define FULL_DISCLOSURE 0

static char headers[4096];
static char *challenge;
static int verbose = 0;

char *getline(int fd) {
	static char buffer[16384];
	static char line[16384];
	static int bufpos = 0;
	int i;
	char *tempchr;

	tempchr = memchr(buffer, '\n' ,bufpos);
	while (tempchr == NULL) {
		i = recv(fd, &buffer[bufpos], 16383 - bufpos, 0);
		if (i <= 0) /* eof of error, let the parent figure out what to do */
			return NULL;
		bufpos += i;
		tempchr = memchr(buffer, '\n', bufpos);
		if (tempchr == NULL && bufpos == 16383) { /* line too long (hostile act) */
			fprintf(stderr, "Input buffer overflow\n");
			exit(2);
		}
	}
	if (tempchr != NULL) {
		memcpy(line, buffer, tempchr - buffer);
		line[tempchr - buffer] = '\0';
		bufpos -= (tempchr - buffer) + 1;
		memcpy(buffer, &tempchr[1], bufpos);
		return line;
	}
	return NULL;
}

int getresponse(int fd) {
	char *line;
	
	while (1) {
		line = getline(fd);
		if (line == NULL) /* eof */
			return 9;
		if (verbose)
			printf("<<< %s\n", line);
		if (strlen(line) >= 4 && line[3] == '-')
			continue;
		else if (line[0] == '1')
			return 1;
		else if (line[0] == '2')
			return 2;
		else if (line[0] == '3') {
			challenge = &line[4];
			return 3;
		} else if (line[0] == '4')
			return 4;
		else if (line[0] == '5')
			return 5;
		else
			return 8; /* bad code */
	}
}

const char *days[] = {
	        "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};

const char *months[] = {
	        "Jan", "Feb", "Mar", "Apr", "May",  "Jun", "Jul",  "Aug",  "Sep", "Oct", "Nov", "Dec"
};

void buildheaders(char *cookie, const char *source, const char *target_user, const char *target_domain) {
	struct tm *timestruct;
	time_t temptime;

	time(&temptime);
	timestruct = gmtime(&temptime);
	firestring_snprintf(headers, 4096, "Message-ID: <%s@%s>\r\n"
			"Date: %s, %d %s %d %02d:%02d:%02d +0000\r\n"
			"To: <%s@%s>\r\n"
			"Subject: Open Relay Test Message\r\n",
			cookie ,source, days[timestruct->tm_wday], timestruct->tm_mday,
			months[timestruct->tm_mon], 1900 + timestruct->tm_year, timestruct->tm_hour,
			timestruct->tm_min, timestruct->tm_sec, target_user, target_domain);
}

int sendtest(int fd, char *intip, const char *cookie, const char *sender, const char *source,
		const char *target_user, const char *target_domain,
		const char *message, const char *extra_info, int smtp_port) {
	char buffer[8192];
	char port_info[32];
	int i;

	if (smtp_port == 25)
		port_info[0] = '\0';
	else
		firestring_snprintf(port_info, 32, "SMTP Port: %d\r\n", smtp_port);

	i = firestring_snprintf(buffer, 8192, "MAIL FROM:<%s@%s>\r\n", sender, source);
	if (verbose)
		printf(">>> MAIL FROM:<%s@%s>\r\n", sender, source);
	if (send(fd, buffer, i, 0) != i)
		return 100;

	i = getresponse(fd);
	if (i != 2)
		return 1;
	
	i = firestring_snprintf(buffer, 8192, "RCPT TO:<%s@%s>\r\n", target_user, target_domain);
	if (verbose)
		printf(">>> RCPT TO:<%s@%s>\r\n", target_user, target_domain);
	if (send(fd, buffer, i, 0) != i)
		return 100;

	i = getresponse(fd);
	if (i != 2)
		return 1;

	firestring_strncpy(buffer, "DATA\r\n", 8192);
	if (verbose)
		printf(">>> DATA\n");
	if (send(fd, buffer, 6, 0) != 6)
		return 100;

	i = getresponse(fd);
	if (i != 3)
		return 1;

	i = firestring_snprintf(buffer, 8192, "%s\r\n"
                       "DSBL LISTME: smtp %s\r\n"
		       "%s\r\n"
		       "%s"
		       "%s"
		       "MAIL FROM:<%s@%s>\r\n"
		       "RCPT TO:<%s@%s>\r\n"
		       "DSBL END\r\n"
		       "\r\n"
		       "%s\r\n"
		       ".\r\n", headers, intip, cookie, port_info, extra_info,
			sender, source, target_user, target_domain, message);

	if (verbose)
		printf(">>> (message)\n");
	if (send(fd, buffer, i, 0) != i)
		return 100;
	
	i = getresponse(fd);
	if (i != 2)
		return 1;

	return 0;
}

/* copy message contents while replacing bare linefeeds */
void copymessage(char *outbuf, const char *inbuf) {
	int l,o,i;
	l = strlen(inbuf);
	o = 0;
	for (i = 0; i < l; i++) {
		if (o > 8189)
			break;
		if (inbuf[i] == '\n') {
			outbuf[o++] = '\r';
			outbuf[o++] = '\n';
		} else
			outbuf[o++] = inbuf[i];
	}
	outbuf[o] = '\0';
}

/* try to authenticate using SMTP AUTH, LOGIN method */
int auth_login(int fd, const struct firestring_estr_t *username, const struct firestring_estr_t *password) {
	int i;
	char buffer[1024];
	struct firestring_estr_t b64u, b64p;

	i = firestring_snprintf(buffer, 1024, "AUTH LOGIN\r\n");
	if (verbose)
		printf(">>> AUTH LOGIN\n");
	if (send(fd, buffer, i, 0) != i)
		return 100;

	i = getresponse(fd);
	if (i != 3)
		return 1;

	/* Create base64-encoded versions of username and password */
	firestring_estr_alloc(&b64u, username->l * 4 / 3 + 5);
	firestring_estr_alloc(&b64p, password->l * 4 / 3 + 5);
	firestring_estr_base64_encode(&b64u, username);
	firestring_estr_base64_encode(&b64p, password);
	firestring_estr_0(&b64u);
	firestring_estr_0(&b64p);

	i = firestring_snprintf(buffer, 1024, "%s\r\n", b64u.s);
	if (verbose)
		printf(">>> %s\n", b64u.s);
	if (send(fd, buffer, i, 0) != i) {
		firestring_estr_free(&b64u);
		firestring_estr_free(&b64p);
		return 100;
	}

	i = getresponse(fd);
	if (i != 3) {
		firestring_estr_free(&b64u);
		firestring_estr_free(&b64p);
		return 1;
	}

	i = firestring_snprintf(buffer, 1024, "%s\r\n", b64p.s);
	if (verbose)
		printf(">>> %s\n", b64p.s);
	firestring_estr_free(&b64u);
	firestring_estr_free(&b64p);
	if (send(fd, buffer, i, 0) != i)
		return 100;

	i = getresponse(fd);
	if (i != 2)
		return 1;

	return i;
}

/* try to authenticate using SMTP AUTH, NTLM method */
/* Only anonymous authentication currently supported; username & password must be pre-encoded */
int auth_ntlm(int fd, const struct firestring_estr_t *username, const struct firestring_estr_t *password) {
	int i;
	char buffer[1024];

	i = firestring_snprintf(buffer, 1024, "AUTH NTLM %s\r\n", username->s);
	if (verbose)
		printf(">>> AUTH NTLM %s\n", username->s);
	if (send(fd, buffer, i, 0) != i)
		return 100;

	i = getresponse(fd);
	if (i != 3)
		return 1;

	i = firestring_snprintf(buffer, 1024, "%s\r\n", password->s);
	if (verbose)
		printf(">>> %s\n", password->s);
	if (send(fd, buffer, i, 0) != i)
		return 100;

	i = getresponse(fd);
	if (i != 2)
		return 1;

	return i;
}

/* try to authenticate using SMTP AUTH, CRAM-MD5 method */
int auth_cram_md5(int fd, const struct firestring_estr_t *username, const struct firestring_estr_t *password) {
	int i;
	char buffer[1024];
	unsigned char digest[16], digest_ascii[33];
	char *hextab = "0123456789abcdef";
	struct firestring_estr_t encoded_challenge, decoded_challenge;
	struct firestring_estr_t encoded_response, unencoded_response;

	i = firestring_snprintf(buffer, 1024, "AUTH CRAM-MD5\r\n");
	if (verbose)
		printf(">>> AUTH CRAM-MD5\n");
	if (send(fd, buffer, i, 0) != i)
		return 100;

	i = getresponse(fd);
	if (i != 3)
		return 1;

	/* Get the challenge and decode it */
	if ((i = strlen(challenge)) >= BASE64MAXLEN) {
		fprintf(stderr, "auth-relaytest: challenge too long (max %d)\n", BASE64MAXLEN);
		return 100;
	}

	firestring_estr_alloc(&encoded_challenge, i + 1);
	firestring_estr_alloc(&decoded_challenge, i * 3 / 4 + 4);
	firestring_estr_strcpy(&encoded_challenge, challenge);
	firestring_estr_base64_decode(&decoded_challenge, &encoded_challenge);
	firestring_estr_0(&encoded_challenge);
	firestring_estr_0(&decoded_challenge);

	/* Compute the response */
	hmac_md5(decoded_challenge.s, decoded_challenge.l, (char *)password->s, password->l, digest);
	for (i = 0; i < 16; i++) {
		digest_ascii[2*i]   = hextab[digest[i] >> 4];
		digest_ascii[2*i+1] = hextab[digest[i] & 0xf];
	}
	digest_ascii[32] = '\0';
	firestring_estr_alloc(&unencoded_response, 35 + username->l);
	firestring_estr_alloc(&encoded_response, 55 + (4 * username->l / 3));
	firestring_estr_sprintf(&unencoded_response, "%s %s", username->s, digest_ascii);
	firestring_estr_base64_encode(&encoded_response, &unencoded_response);
	firestring_estr_0(&unencoded_response);
	firestring_estr_0(&encoded_response);

	/* Send the response */
	i = firestring_snprintf(buffer, 1024, "%s\r\n", encoded_response.s);
	if (verbose)
		printf(">>> %s\n", encoded_response.s);
	if (send(fd, buffer, i, 0) != i)
		return 100;

	i = getresponse(fd);
	if (i != 2)
		return 1;

	return i;
}

int auth_relaytest(
	struct sockaddr_in *host_addr,
	char *intip,
	int method,
	const char *auth_details,
	struct firestring_estr_t *username,
	struct firestring_estr_t *password,
	const char *cookie,
	const char *sender_user,
	const char *sender_domain,
	const char *target_user,
	const char *target_domain,
	const char *msgbuf,
	int smtp_port) {
	int i, fd;
	char buffer[1024];

	fd = socket(PF_INET, SOCK_STREAM, 0);
	if (fd == -1)
		return 100;

	if (verbose)
		printf("Connecting to %s...", intip);
	if (connect(fd, (struct sockaddr *)host_addr, sizeof(struct sockaddr_in)) != 0) {
		if (verbose)
			printf(" failed.\n");
		return 100;
	} else {
		if (verbose)
			printf(" done.\n");
	}

	i = getresponse(fd);
	if (i != 2)
		return 1;

	i = firestring_snprintf(buffer, 1024, "EHLO %s\r\n", sender_domain);
	if (verbose)
		printf(">>> EHLO %s\n", sender_domain);
	if (send(fd, buffer, i, 0) != i)
		return 100;

	i = getresponse(fd);
	if (i != 2)
		return 1;

	switch (method) {
		case AUTH_LOGIN:
			i = auth_login(fd, username, password);
			break;
		case AUTH_NTLM:
			i = auth_ntlm(fd, username, password);
			break;
		case AUTH_CRAM_MD5:
			i = auth_cram_md5(fd, username, password);
			break;
		default:
			fprintf(stderr, "Bad SMTP AUTH method: %d\n", method);
			exit(100);
	}

	if (i == 2) {
		i = sendtest(fd, intip, cookie, sender_user, sender_domain, target_user, target_domain,
			msgbuf, auth_details, smtp_port);
		if (i != 0)
			i = 4; /* AUTH accepted but message not accepted */
	}

	if (verbose)
		printf(">>> QUIT\n");
	send(fd, "QUIT\r\n", 6, 0);
	(void)getresponse(fd);
	close(fd);

	return i;
}

void usage(const char *prog) {
	fprintf(stderr,
		"auth-relaytest: usage: %s [-tv] ntlm <ip-address>[:port]\n"
		"                       %s [-tv] (login|cram-md5) <username> <password> <ip-address>[:port]\n",
		prog, prog);
	exit(100);
}

/* return values:
 *   0 - host accepted some tests, may relay
 *   1 - host accepted no tests, won't relay
 *   2 - host appears to be blocking tester (or may just be seriously broken)
 *   3 - test timed out (alarm)
 *   4 - host accepted authentication but still didn't relay
 * 100 - format or internal error
 */

int main(int argc, char **argv) {
	struct in_addr *in;
	struct sockaddr_in host_addr;
	char *cookie;
	char exploit[1024];
	int i;
	int ret = 1;
	char *tempchr;
	const char *sender_user,
		*sender_domain,
		*target_user,
		*target_domain;
	const char *message;
	char msgbuf[8192];
	char intip[16];
	int smtp_port = 25;
	int auth_method = -1;
	char *method = NULL;
	int testmode = 0;
	struct firestring_estr_t user, pass;
	char *username = "", *password = "", *ip = "";

	setalarm();

	/* Check for options */
	while (argc > 1 && *argv[1] == '-') {
		for (i = 1; argv[1][i] ;i++) {
			switch (argv[1][i]) {
			case 't':	testmode = 1;
					break;
			case 'v':	verbose = 1;
					break;
			default:	usage(argv[0]);
			}
		}
		for (i = 2; i < argc ;i++)
			argv[i-1] = argv[i];
		argc--;
	}

	/* Check for correct parameters */
	if (argc == 5) {
		if (firestring_strcasecmp(argv[1], "login") == 0) {
			auth_method = AUTH_LOGIN;
			method = "LOGIN";
			username = argv[2];
			password = argv[3];
			ip = argv[4];
		} else if (firestring_strcasecmp(argv[1], "cram-md5") == 0) {
			auth_method = AUTH_CRAM_MD5;
			method = "CRAM-MD5";
			username = argv[2];
			password = argv[3];
			ip = argv[4];
		} else {
			usage(argv[0]);
		}
	} else if (argc == 3) {
		if (firestring_strcasecmp(argv[1], "ntlm") == 0) {
			auth_method = AUTH_NTLM;
			method = "NTLM";
			username = NTLM_ANON_USER;
			password = NTLM_ANON_PASS;
			ip = argv[2];
		} else {
			usage(argv[0]);
		}
	} else {
		usage(argv[0]);
	}

	/* Create estring versions of username and password */
	firestring_estr_alloc(&user, strlen(username) + 1);
	firestring_estr_alloc(&pass, strlen(password) + 1);
	firestring_estr_strcpy(&user, username);
	firestring_estr_strcpy(&pass, password);
	firestring_estr_0(&user);
	firestring_estr_0(&pass);

	/* Check for sane lengths */
	if (user.l > BASE64MAXLEN) {
		fprintf(stderr, "auth-relaytest: username too long (max %d)\n", BASE64MAXLEN);
        	exit(100);
	}
	if (pass.l > BASE64MAXLEN) {
		fprintf(stderr, "auth-relaytest: password too long (max %d)\n", BASE64MAXLEN);
        	exit(100);
	}

	/* See if a port has been specified, and if so, extract it */
	tempchr = strchr(ip, ':');
	if (tempchr != NULL) {
		smtp_port = atoi(&tempchr[1]);
		tempchr[0] = '\0';
	}

	/* Get the IP address */
	in = firedns_aton4(ip);
	if (in == NULL) {
		fprintf(stderr, "auth-relaytest: invalid IP\n");
		exit(100);
	}
	memcpy(&host_addr.sin_addr, in, sizeof(struct in_addr));
	firestring_strncpy(intip, firedns_ntoa4(in), 16);
	host_addr.sin_family = AF_INET;
	host_addr.sin_port = htons(smtp_port);

	readconf();

	sender_user = firestring_conf_find(config, "sender_user");
	if (sender_user == NULL) {
		fprintf(stderr, "sender_user not set in config.\n");
		exit(100);
	}

	sender_domain = firestring_conf_find(config, "sender_domain");
	if (sender_domain == NULL) {
		fprintf(stderr, "sender_domain not set in config.\n");
		exit(100);
	}

	target_user = firestring_conf_find(config, "target_user");
	if (target_user == NULL) {
		fprintf(stderr, "target_user not set in config.\n");
		exit(100);
	}

	if (testmode == 1) {
		target_domain = firestring_conf_find(config, "test_target_domain");
		if (target_domain == NULL) {
			fprintf(stderr, "test_target_domain not set in config.\n");
			exit(100);
		}
	} else {
		target_domain = firestring_conf_find(config, "target_domain");
		if (target_domain == NULL) {
			fprintf(stderr, "target_domain not set in config.\n");
			exit(100);
		}
	}

	message = firestring_conf_find(config, "auth-message");
	if (message == NULL) {
		message = firestring_conf_find(config, "message");
	}
	if (message == NULL) {
		fprintf(stderr, "auth-relaytest: neither auth-message nor message set in config.\n");
		exit(100);
	}

	copymessage(msgbuf, message);

	cookie = getcookie();

	buildheaders(cookie, sender_domain, target_user, target_domain);

#if FULL_DISCLOSURE
	firestring_snprintf(exploit, 1024, "AUTH %s, user=%s, password=%s\r\n", method, user.s,
		*(pass.s) ? pass.s : "(null)");
#else
	firestring_snprintf(exploit, 1024, "AUTH %s, user=%s\r\n", method, user.s);
#endif
	if (auth_method == AUTH_NTLM) {
		firestring_strncpy(exploit, "AUTH NTLM, anonymous\r\n", 1024);
	}

	i = auth_relaytest(&host_addr, intip, auth_method, exploit, &user, &pass,
		cookie, sender_user, sender_domain, target_user, target_domain, msgbuf, smtp_port);
	if (i == 0 || i == 4)
		ret = i;

	exit(ret);
}
