package org.jonlin.security;

import java.io.*;
import java.util.*;

/**
 *	Copyright (c) 2000-2005, Comet Way, Inc.
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, 
 * with or without modification, are permitted provided 
 * that the following conditions are met:
 * 
 *   - Redistributions of source code must retain the 
 *     above copyright notice, this list of conditions 
 *     and the following disclaimer. 
 *   - Redistributions in binary form must reproduce the 
 *     above copyright notice, this list of conditions 
 *     and the following disclaimer in the documentation 
 *     and/or other materials provided with the distribution.
 *   - Neither the name of Comet Way, Inc. nor the names of 
 *     its contributors may be used to endorse or promote 
 *     products derived from this software without specific 
 *     priorwritten permission. 
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 *  ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *  LIMITED TO, THE IMPLIEDWARRANTIES OF MERCHANTABILITY AND FITNESS 
 *  FOR A PARTICULAR PURPOSE ARE DISCLAIMED.IN NO EVENT SHALL THE 
 *  REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 *  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 *  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 *  USE, DATA, OR PROFITS; ORBUSINESS INTERRUPTION) HOWEVER CAUSED AND 
 *  ON ANY THEORY OF LIABILITY, WHETHER INCONTRACT, STRICT LIABILITY, 
 *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
 *  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
 *  SUCH DAMAGE. 
 *
 * Revision history:
 * - Added multiple match strings
 * - Added log file roll checks, fixed small problem with copied rotation vs. move/rename rotation
 * - Changed the move/rename rotation check to modified time of a previously rolled log file
 * - Changed the rotation check to a file length/file pointer comparison.
 * - Added a Thread with a quicker sleep time to check if the log file has more to read.
 */

/**
 * This little class reads syslog data in the /var/log/secure file (must be root to run).
 * sshd output is read, after a 'connection from' following a 'root login denied', the IP
 * of the connection is logged and the execString is executed. The matched string (root login denied)
 * and the execute string (/sbin/iptables -A INPUT -p tcp --dport 22 -s $IP -j DROP) can be
 * configured via command line.
 *
 * No sorting, duplicate IP removal, or IP caching is implemented. The assumption here is that
 * the execString keeps this IP from brute forcing us.
 */
public class SSHBruteForce extends Thread
{
	/** this command is executed if a brute force attempt is detected, the $IP is replaced with the brute force IP */
	String execString = "/sbin/iptables -A INPUT -p tcp --dport 22 -s $IP -j DROP";
	/** locally kept log for brute force attempt IPs */
	String logFileName = "ssh_bomb.log";
	/** String used to match logfile String to determine if connection is a brute force attempt */
	Vector matchStrings;


	String filename = "/var/log/secure";

	/** RandomAccessFile pointing to /var/log/secure */
	RandomAccessFile raf;

	/** The Thread which checks if the logfile has more data to read */
	FileWatcher watcher;

	/** The sync object which is notified when there is more to read */
	Object sync;

	long length;

	static int debug = 0;


	/**
	 * Parse args, start thread.
	 */
	public SSHBruteForce(String[] args)
	{
		super();
		matchStrings = new Vector();

		if(args.length>0) {
			try {
				for(int x=0;x<args.length;x++) {
					if(args[x].equals("-exec")) {
						execString = args[++x];
					}
					else if(args[x].equals("-logfile")) {
						logFileName = args[++x];
					}
					else if(args[x].startsWith("-match")) {
						matchStrings.addElement(args[++x]);
					}
					else if(args[x].startsWith("-h")) {
						printHelp();
					}
					else if(args[x].startsWith("-debug")) {
						debug = Integer.parseInt(args[++x].trim());
					}
				}
			}
			catch(Exception e) {
				e.printStackTrace();
				printHelp();
			}
		}

		// default match strings here:
		if(matchStrings.size()==0) {
			matchStrings.addElement("root login denied");
			matchStrings.addElement("not allowed or account non-existent.");
		}

		System.out.println("Starting SSHBruteForce v0.8");

		sync = new Object();
		start();
		watcher = new FileWatcher();
	}

	/**
	 * Print help and System.exit();
	 */
	public static void printHelp()
	{
		System.out.println("java org.jonlin.security.SSHBruteForce [options]");
		System.out.println("  -exec <String>      Executes this String when an SSH Brute Force attempt is found");
		System.out.println("                      The parameter $IP can be used in the <String>, which will be");
		System.out.println("                      replaced with the IP address of the brute force attempt.");
		System.out.println("  -match <String>     The String used to match to determine if the SSH connection");
		System.out.println("                      is a brute force attempt. -match can be used more than once.");
		System.out.println("  -logfile <filename> This file will be used to log IPs of brute force attempts.");
		System.out.println("  -debug <num>        This sets the debug output level. Default is 0, which means");
		System.out.println("                      nothing is outputed. 1 means lines read from the logfile and");
		System.out.println("                      the execution strings are outputed. 2 means the log file rotate");
		System.out.println("                      watcher is outputed. 3 prints out a line by line sync and notify");
		System.out.println("                      output, which is used to determine how your system deals with file");
		System.out.println("                      locks, syncs and notifys, in an event of an unknown lockup.");
		System.out.println("  -h                  Prints this help message.\n");
		System.out.println("Example: java org.jonlin.security.SSHBruteForce -exec 'ipfwadm -I -a deny -P all -b -S \\$IP/32' -match 'Login to account test not allowed' -match 'Login to account user not allowed' -logfile ssh_brute_force.log\n");

		System.exit(0);
	}

	/**
	 * returns true if the String contains any of the matches in the matchStrings Vector.
	 */
	public boolean match(String input)
	{
		boolean rval = false;

		for(int x=0;x<matchStrings.size();x++) {
			if(input.indexOf(matchStrings.elementAt(x).toString())!=-1) {
				rval = true;
				break;
			}
		}

		return(rval);
	}


	/**
	 * Attempts to read a line from the /var/log/secure file. If no new lines have been added,
	 * sleep for a sec and try again.
	 */
	public String readLine()
	{
		String rval = null;
		try {
			synchronized(sync) {
				rval = raf.readLine();
			}
			while(rval==null) {
				synchronized(sync) {
					debug3("+");
					sync.wait(500);
					debug3("++");
					rval = raf.readLine();
					debug3("+++");
					if(rval!=null) {
						length = raf.getFilePointer();
					}
					debug3("++++");
				}
			}
		}
		catch(Exception e) {
			e.printStackTrace();
			System.exit(-1);
		}

		debug1("READ: "+rval);
		return(rval);
	}

	/**
	 * Calls readLine() to get next line from /var/log/secure. parses lines, if match found it executes
	 * the execString.
	 */
	public void run()
	{
		FileWriter logFile = null;

		try {
			// swap these 2 lines if you aren't using jdk1.4 or higher
			logFile = new FileWriter(new File(logFileName),true);
			//			logFile = new FileWriter(new File(logFileName));
		}
		catch(Exception e) {;}

		try {
			String line = null;
			raf = new RandomAccessFile(filename,"r");
			raf.seek(raf.length());
			
			while(true) {
				try {
					String ip = null;

					line = readLine();
					// if isn't a sshd "connection from", we start over at the end.
					if(line.indexOf("connection from")!=-1 && line.indexOf("sshd")!=-1) {

						// parse out the IP
						if(line.indexOf("\"")!=-1) {
							line = line.substring(line.indexOf("\"")+1);
							if(line.indexOf("\"")!=-1) {
								ip = line.substring(0,line.indexOf("\""));

								// get the next sshd line, this should either be a DNS lookup failure or the match
								line = readLine();
								while(line.indexOf("sshd")==-1) {
									line = readLine();
								}
								if(!match(line)) {
									// DNS lookup failure
									line = readLine();
									while(line.indexOf("sshd")==-1) {
										line = readLine();
									}
								}
								// If this isn't the match, then this isn't a brute force attempt, we ignore it
								if(match(line) && line.indexOf("sshd")!=-1) {

									// replace the $IP with the parsed IP
									String matchedExecString = execString;
									int index = execString.indexOf("$IP");
									if(index!=-1) {
										matchedExecString = execString.substring(0,index)+ip+execString.substring(index+3);
									}
									
									new ExecThread(matchedExecString);	

									// write to the brute force logfile
									try {
										logFile.write("BRUTE FORCE: "+ip+"\n");
										logFile.flush();
									}
									catch(Exception e) {;}
								}
							}
						}
					}					
				}
				catch(Exception e) {
					e.printStackTrace();
					try {
						// This shouldn't need to be called since the handle should be the same
						raf.close();
					}
					catch(Exception e2) {;}
					
					// in case of a royal fuck up, we try again. if fuck up again, we drop out of while()
					raf = new RandomAccessFile(filename,"r");
					raf.seek(raf.length());
				}					
			}
		}
		catch(Exception e) {
			e.printStackTrace();
		}

	}


	class FileWatcher extends Thread
	{
		public FileWatcher()
		{
			super();
			start();
		}


		public void run()
		{
			try {
				int count = 0;
				sleep(1000);
				length = raf.getFilePointer();
				while(true) {
					sleep(500);
					//					if(raf.getFilePointer() > (new File(filename)).length()) {
					if(count>40) {
						debug3("x");
						synchronized(sync) {
							try {
								debug3("xx");
								raf.close();
							}
							catch(Exception e) {;}
							try {
								debug3("xxx");
								raf = new RandomAccessFile(filename,"r");
								debug2("NEW FILE: length="+length+", new length="+raf.length());
								if(raf.length() >= length) {
									debug3("xxxx");
									raf.seek(length);
								}
								else {
									debug3("xxxxx");
									raf.seek(0);
								}
							}
							catch(Exception e) {;}
							debug3("xxxxxx");
							count = 0;
							debug3("xxxxxxx");
							if(raf.length()!=length) {
								sync.notify();
							}
							else {
								sleep(200);
							}
							debug3("xxxxxxxx");
						}
					}
					else {
						debug3("*");
						//						if(length < raf.length()) {
						if(raf.read()!=-1) {
							debug3("**");
							synchronized(sync) {
								raf.seek(raf.getFilePointer()-1);
								// wait a sec for the newline
								//								sleep(100);
								//								raf.seek(length);
								//								debug3("***");
								//								sync.notify();
								debug2("**** length="+length+" raf.pointer="+raf.getFilePointer()+" raf.length="+raf.length());
							}
							count = 0;
						}
						else {
							debug3("*****");
							count++;
						}
					}
				}
			}
			catch(Exception e) {
				e.printStackTrace();
			}
		}
	}



	class ExecThread extends Thread
	{
		String exec;

		public ExecThread(String execString)
		{
			super();
			exec = execString;
			start();
		}

		public void run()
		{
			Process process = null;
			debug1("Attempting to execute: "+exec);

			// execute the execString then destroy the process
			try {
				Runtime runtime = Runtime.getRuntime();
				process = runtime.exec(exec);
				process.waitFor();
			}
			catch(Exception e) {e.printStackTrace();}
			try {
				process.destroy();
			}
			catch(Exception e) {;}
			
			debug1("Finished executing: "+exec);
		}

	}



	public static void debug1(String s)
	{
		if(debug>=1) {
			System.out.println(s);
		}
	}

	public static void debug2(String s)
	{
		if(debug>=2) {
			System.out.println(s);
		}
	}

	public static void debug3(String s)
	{
		if(debug>=3) {
			System.out.println(s);
		}
	}




	public static void main(String[] args)
	{
		new SSHBruteForce(args);
	}
}

