├── .gitattributes ├── .gitignore ├── README.md ├── artillery.py ├── artillery_start.bat ├── readme ├── CHANGELOG ├── CREDITS └── LICENSE ├── remove_ban.py ├── restart_server.py ├── setup.py └── src ├── __init__.py ├── anti_dos.py ├── apache_monitor.py ├── artillery_service ├── config.py ├── core.py ├── email_handler.py ├── events.py ├── ftp_monitor.py ├── globals.py ├── harden.py ├── honeypot.py ├── monitor.py ├── pyuac.py ├── ssh_monitor.py ├── startup_artillery ├── win_func.py └── windows ├── WindowsTODO.txt ├── banlist.bat ├── banlist.txt ├── del_Cache.bat └── launch.bat /.gitattributes: -------------------------------------------------------------------------------- 1 | #just some stuff to normalize line endings 2 | * text 3 | *.py text 4 | *.sh text 5 | *.bat text 6 | 7 | #this is for the eventual event message dll file. that is currently in progress 8 | *.dll binary 9 | 10 | 11 | #put this here because it will run on both platfoms. commented out for now 12 | #*.ps1 text 13 | #*.psm1 text 14 | 15 | 16 | #I grabbed all info from here. feel free to add/remove if incorrect for your env. 17 | #https://help.github.com/articles/dealing-with-line-endings/ 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | banlist.txt 3 | config 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Project Artillery - A project by Binary Defense Systems (https://www.binarydefense.com). 2 | 3 | Binary Defense Systems (BDS) is a sister company of TrustedSec, LLC 4 | ======= 5 | Artillery is a combination of a honeypot, monitoring tool, and alerting system. Eventually this will evolve into a hardening monitoring platform as well to detect insecure configurations from nix and windows systems. It's relatively simple, run ```./setup.py``` and hit yes, this will install Artillery in ```/var/artillery``` and edit your ```/etc/init.d/rc.local``` on linux to start artillery on boot up. On windows it will be installed to ```\Program Files (x86)\Artillery``` and a batch file is included for startup 6 | 7 | ### Features 8 | 9 | 1. It sets up multiple common ports that are attacked. If someone connects to these ports, it blacklists them forever (to remove blacklisted ip's, On Linux remove them from ```/var/artillery/banlist.txt```. On Windows remove them from```\Program Files (x86)\Artillery\banlist.txt```) 10 | 11 | 2. It monitors what folders you specify, by default it checks ```/var/www``` and ```/etc``` for modifications.(linux only) 12 | 13 | 3. It monitors the SSH logs and looks for brute force attempts.(linux only) 14 | 15 | 4. It will email you when attacks occur and let you know what the attack was. 16 | 17 | Be sure to edit the ```/var/artillery/config```on Linux or ```\Program Files (x86)\Artillery\config``` on Windows to turn on mail delivery, brute force attempt customizations, and what folders to monitor. 18 | 19 | 20 | ### Bugs and enhancements 21 | 22 | For bug reports or enhancements, please open an issue here https://github.com/BinaryDefense/artillery/issues 23 | 24 | ### Project structure 25 | 26 | For those technical folks you can find all of the code in the following structure: 27 | 28 | - ```Artillery.py``` - main program file 29 | - ```restart_server.py``` - handles restarting software 30 | - ```remove_ban.py``` - removes ips from banlist 31 | - ```src/anti_dos.py``` - main monitoring module for Dos attacks 32 | - ```src/apache_monitor.py``` - main monitoring module for Apache web service 33 | - ```src/config.py``` - main module for configuration settings 34 | - ```src/email_handler.py``` - main module for handling email 35 | - ```src/events.py``` - main module for trigering events on windows systems 36 | - ```src/ftp_monitor.py``` - main monitoring module for FTP bruteforcing 37 | - ```src/globals.py``` - main module for holding global variables for use in artillery 38 | - ```src/pyuac.py``` - main module for windows uac prompt 39 | - ```src/core.py``` - main central code reuse for things shared between each module 40 | - ```src/monitor.py``` - main monitoring module for changes to the filesystem 41 | - ```src/ssh_monitor.py``` - main monitoring module for SSH brute forcing 42 | - ```src/honeypot.py``` - main module for honeypot detection 43 | - ```src/harden.py``` - check for basic hardening to the OS 44 | - ```database/integrity.data``` - main database for maintaining sha512 hashes of filesystem 45 | - ```setup.py``` - on linux copies files to ```/var/artillery/``` then edits ```/etc/init.d/artillery``` to ensure artillery starts per each reboot 46 | - on windows copies files to ```\Program Files (x86)\Artillery\``` you have option to launch on install.(see below) 47 | 48 | ### Supported platforms 49 | 50 | - Linux 51 | - Windows 52 | 53 | On windows to install pywin32 is needed.Install version that matches the version of python installed ex: 32/64 bit. Download files to location of your choice.open a cmd prompt browse to directory that files are located. To run type "python setup.py". You will be prompted for credentials if you are not an admin. Artillery will be installed in ```"Program Files (x86)\Artillery```. After setup you have option to launch program. included is a batch file to launch once it is installed it is located in install directory.Console logging must be enabled in config. 54 | 55 | Project Artillery - A project by Binary Defense Systems (https://www.binarydefense.com). 56 | 57 | Binary Defense Systems (BDS) is a sister company of TrustedSec, LLC 58 | -------------------------------------------------------------------------------- /artillery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ################################################################################ 3 | # 4 | # Artillery - An active honeypotting tool and threat intelligence feed 5 | # 6 | # Written by Dave Kennedy (ReL1K) @HackingDave 7 | # 8 | # A Binary Defense Project (https://www.binarydefense.com) @Binary_Defense 9 | # 10 | ################################################################################ 11 | import time 12 | import sys 13 | # needed for backwards compatibility of python2 vs 3 - need to convert to threading eventually 14 | try: import thread 15 | except ImportError: import _thread as thread 16 | import os 17 | import subprocess 18 | from src.pyuac import * # added so that it prompts when launching from batch file 19 | 20 | import traceback 21 | 22 | # import artillery global variables 23 | import src.globals 24 | from src.core import * 25 | # 26 | init_globals() 27 | # Tested on win 7/8/10 also on kali rolling. left this here for when someone tries to launch this directly before using setup. 28 | if not os.path.isfile(src.globals.g_appfile): 29 | print("[*] Artillery is not installed, running setup.py..") 30 | import setup 31 | 32 | # from src.config import * # yaml breaks config reading - disabling 33 | 34 | if is_windows():#this is for launching script as admin from batchfile. 35 | if not isUserAdmin():# will prompt for user\pass and open in seperate window when you double click batchfile 36 | runAsAdmin() 37 | # 38 | if isUserAdmin(): 39 | check_config() 40 | #moved for issue #39 BinaryDefense to only import on windows. seemed like best place 41 | #not the best way but for now something will go into eventlog. 42 | #for people with subscriptions in there environment like myself. 43 | #will work on better way 44 | from src.events import ArtilleryStartEvent 45 | # let the local(txt))logfile know artillery has started successfully 46 | write_log("Artillery has started successfully.") 47 | # write to windows log to let know artillery has started 48 | ArtilleryStartEvent() 49 | #create temp datebase and continue 50 | if not os.path.isfile(src.globals.g_apppath + "\\database\\temp.database"): 51 | filewrite = open(src.globals.g_apppath + "\\database\\temp.database", "w") 52 | filewrite.write("") 53 | filewrite.close() 54 | 55 | #consolidated nix* variants 56 | if is_posix(): 57 | # Check to see if we are root 58 | try: # and delete folder 59 | if os.path.isdir("/var/artillery_check_root"): 60 | os.rmdir('/var/artillery_check_root') 61 | #if not thow error and quit 62 | except OSError as e: 63 | if (e.errno == errno.EACCES or e.errno == errno.EPERM): 64 | print ("[*] You must be root to run this script!\r\n") 65 | sys.exit(1) 66 | else: 67 | check_config() 68 | if not os.path.isdir(src.globals.g_apppath + "/database/"): 69 | os.makedirs(src.globals.g_apppath + "/database/") 70 | if not os.path.isfile(src.globals.g_apppath + "/database/temp.database"): 71 | filewrite = open(src.globals.g_apppath + "/database/temp.database", "w") 72 | filewrite.write("") 73 | filewrite.close() 74 | 75 | 76 | write_console("Artillery has started \nIf on Windows Ctrl+C to exit. \nConsole logging enabled.\n") 77 | write_console("Artillery is running from '%s'" % src.globals.g_apppath) 78 | 79 | # prep everything for artillery first run 80 | check_banlist_path() 81 | 82 | try: 83 | # update artillery 84 | if is_config_enabled("AUTO_UPDATE"): 85 | thread.start_new_thread(update, ()) 86 | 87 | # import base monitoring of fs 88 | if is_config_enabled("MONITOR") and is_posix(): 89 | from src.monitor import * 90 | 91 | # port ranges to spawn 92 | tcpport = read_config("TCPPORTS") 93 | udpport = read_config("UDPPORTS") 94 | 95 | # if we are running posix then lets create a new iptables chain 96 | if is_posix(): 97 | time.sleep(2) 98 | write_console("Creating iptables entries, hold on.") 99 | create_iptables_subset() 100 | write_console("iptables entries created.") 101 | if is_config_enabled("ANTI_DOS"): 102 | write_console("Activating anti DoS.") 103 | # start anti_dos 104 | import src.anti_dos 105 | 106 | # spawn honeypot 107 | write_console("Launching honeypot.") 108 | import src.honeypot 109 | 110 | # spawn ssh monitor 111 | if is_config_enabled("SSH_BRUTE_MONITOR") and is_posix(): 112 | write_console("Launching SSH Bruteforce monitor.") 113 | import src.ssh_monitor 114 | 115 | # spawn ftp monitor 116 | if is_config_enabled("FTP_BRUTE_MONITOR") and is_posix(): 117 | write_console("Launching FTP Bruteforce monitor.") 118 | import src.ftp_monitor 119 | 120 | # start monitor engine 121 | if is_config_enabled("MONITOR") and is_posix(): 122 | write_console("Launching monitor engines.") 123 | import src.monitor 124 | if is_config_enabled("SYSTEM_HARDENING") and is_posix(): 125 | # check hardening 126 | write_console("Check system hardening.") 127 | import src.harden 128 | 129 | # start the email handler 130 | if is_config_enabled("EMAIL_ALERTS") and is_posix(): 131 | write_console("Launching email handler.") 132 | import src.email_handler 133 | 134 | # check to see if we are a threat server or not 135 | if is_config_enabled("THREAT_SERVER"): 136 | write_console("Launching threat server thread.") 137 | thread.start_new_thread(threat_server, ()) 138 | 139 | # recycle IP addresses if enabled 140 | if is_config_enabled("RECYCLE_IPS"): 141 | write_console("Launching thread to recycle IP addresses.") 142 | thread.start_new_thread(refresh_log, ()) 143 | 144 | # pull additional source feeds from external parties other than artillery 145 | # - pulls every 2 hours or ATIF threat feeds 146 | write_console("Launching thread to get source feeds, if needed.") 147 | thread.start_new_thread(pull_source_feeds, ()) 148 | #removed turns out the issue was windows carriage returns in the init script i had. 149 | #note to self never edit linux service files on windows.doh 150 | #added to create pid file service would fail to start on kali 2017 151 | #if is_posix(): 152 | # if not os.path.isfile("/var/run/artillery.pid"): 153 | # pid = str(os.getpid()) 154 | # f = open('/var/run/artillery.pid', 'w') 155 | # f.write(pid) 156 | # f.close() 157 | 158 | 159 | # let the program to continue to run 160 | write_console("All set.") 161 | write_log("Artillery is up and running") 162 | while 1: 163 | try: 164 | time.sleep(100000) 165 | except KeyboardInterrupt: 166 | print("\n[!] Exiting Artillery... hack the gibson.\n") 167 | sys.exit() 168 | 169 | #except sys.excepthook as e: 170 | # print("Excepthook exception: " + format(e)) 171 | # pass 172 | 173 | except KeyboardInterrupt: 174 | sys.exit() 175 | 176 | except Exception as e: 177 | emsg = traceback.format_exc() 178 | print("General exception: " + format(e) + "\n" + emsg) 179 | write_log("Error launching Artillery\n%s" % (emsg),2) 180 | 181 | sys.exit() 182 | -------------------------------------------------------------------------------- /artillery_start.bat: -------------------------------------------------------------------------------- 1 | :: script to start artillery 2 | @echo off 3 | python "C:\Program Files (x86)\Artillery\artillery.py" 4 | exit 5 | exit 6 | exit 7 | -------------------------------------------------------------------------------- /readme/CHANGELOG: -------------------------------------------------------------------------------- 1 | ~~~~~~~~~~~~~~~~~~~~~~ 2 | version 2.3 12/29/2019 3 | ~~~~~~~~~~~~~~~~~~~~~~ 4 | 5 | * Created a “write_console” (use “write_console” instead of “print”). Write_console will only print something if console logging is enabled. 6 | * Changed the “write_log” function. Syslog messages now have a uniform format: Artillery[XXXXX] – Message, XXXXX can be INFO, WARN or ERROR 7 | * Add support for %time%, %ip% and %port% variables in the LOG_MESSAGE_BAN and LOG_MESSAGE_ALERT variables. The old “%s %s %s” sequence (time, ip, port) feature is still preserved, but new installations will use the new format. 8 | * The config file will be created by artillery (if it doesn’t exist already), and will be read/regenerated every time you launch artillery. 9 | * This allows you to define new config variables (and their default value) inside core.py, so everyone (with an existing/older installation of artillery) will also get the new variables 10 | * Removes the need to have a “config“ file in the github repository. 11 | * Overall, stability & error handling improvements: 12 | * Less likely for artillery to crash entirely (if, for instance, no TCPPORTS or UDPPORTS are given, or if something goes wrong during socket listener setup / request closure) 13 | * Tcp & udp honeypot servers will attempt to bind to a port for a number of times before giving up. (I’ve had a few cases where “systemctl restart artillery” actually failed to bind to a port, and the solution was just to wait a little longer…. ) 14 | * Improved the management of iptables rules – increased changes that the required chains are properly flushed, recreated and populated (without duplicates). 15 | * Introduced some global variables, to avoid hardcoding file paths 16 | * Improved check if IP/range is whitelisted 17 | * Verbose logging in syslog – artillery is writing a lot more info to syslog, hopefully not too much :) 18 | * Added ability to create a second banlist file, which will only contain the IP addresses/class C networks that have been detected / blocked on this instance of artillery. This means that, even if the artillery instance is using an external feed (which will get merged into banlist.txt), you can still serve the locally detected IP addresses/ranges as a feed to other systems. The second file is called localbanlist.txt. 19 | * Note: Since all entries in this localbanlist.txt file are also written to banlist.txt, the localbanlist.txt will not be imported during artillery load. 20 | * You can now also specify a local filepath (in addition to a URL) as THREAT_FEED (See also previous point – if you scp a localbanlist.txt file to another system, that other system can pick it up from the local filesystem) 21 | * You can provide multiple files as THREAT_SERVER (so technically you can set it to “banlist.txt,localbanlist.txt” and both files will be copied to the webserver path.) 22 | 23 | ~~~~~~~~~~~~~~~~~~~~~~ 24 | version 2.2 12/20/2019 25 | ~~~~~~~~~~~~~~~~~~~~~~ 26 | 27 | * added HONEYPOT_BAN_LOG_PREFIX which is a config option that allows you to specify tags for banned IP addresses (pr corelanc0d3r) 28 | * allow for full class C cidr notation to block subnet ranges of an IP that it begins to ban instead of just one (pr corelanc0d3r) 29 | * performance increase on loading in iptables for banlist (pr corelanc0d3r) 30 | * Send warning message when Artillery is unable to bind to a tcp or udp port (pr corelanc0d3r) 31 | * better logging and error handling around startup and fails to start (pr corelanc0d3r) 32 | 33 | 34 | ~~~~~~~~~~~~~~~~~~~~~~ 35 | version 2.1.1 6/2/2019 36 | ~~~~~~~~~~~~~~~~~~~~~~ 37 | 38 | * added dates to changelog to reflect how often updates are applied. 39 | Please Note this only applies to git history. svn history is unavailible 40 | # added silent install option 41 | * readme updates for windows install locations and also src file updates 42 | * removed basically entire copy of full readme as duplicate txt 43 | * added udp server to honeypot 44 | * updates to core files 45 | * added events for windows eventlog 46 | * fixed issue where on py 2.7 all files were being deleted 47 | * fixed issue when running artillery directly 48 | 49 | 50 | 51 | 52 | ~~~~~~~~~~~~~~~~~~~~~~ 53 | version 2.0 10/4/2018 54 | ~~~~~~~~~~~~~~~~~~~~~~ 55 | 56 | * added windows 7/8/10 install/uninstall support 57 | * Reorganized linux install/uninstall routine 58 | * added uac check(windows) 59 | * added root check(linux) 60 | 61 | 62 | 63 | 64 | 65 | ~~~~~~~~~~~~~~~~~~~~~~ 66 | version 1.8 12/19/2017 67 | ~~~~~~~~~~~~~~~~~~~~~~ 68 | 69 | * fixed setup.py for backwards compatiblity for python2/3 70 | * fixed issue that would cause restart_artillery to kill itself and not restart 71 | * fixed an issue that would cause ssh to pull more logs then needed and not to ssh banning 72 | * fixed an issue that would cause intelligence to still download if source feeds were on 73 | * turned anti-dos to off by default 74 | * fixed iptables trying to start when honeypot_ban was off 75 | * fixed email from not loading modules properly due to python2to3 compatibility 76 | * fixed syslog offloading 77 | 78 | ~~~~~~~~~~~~~~~~~~~~~~ 79 | version 1.7 2/2/2016 80 | ~~~~~~~~~~~~~~~~~~~~~~ 81 | 82 | * converstion to Python3 and adding support for python3 83 | * misc fixes for backwards compatibility for python2/3 84 | * added fix for 0.x.x.x subnets sitll being added under sort 85 | 86 | ~~~~~~~~~~~~~~~~~~~~~~ 87 | version 1.6.1 2/2/2016 88 | ~~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | * put everything into pep8 format 91 | * added password auth check thanks to benichmt1 git pull request 92 | 93 | ~~~~~~~~~~~~~~~~~~~~~~ 94 | version 1.6 2/1/2016 95 | ~~~~~~~~~~~~~~~~~~~~~~ 96 | 97 | * fixed issue where 0.x.x.x could show up on banlist 98 | * added OTX reputation IP lists to Artillery 99 | * sped up artillery boot time significantly when ban is turned off 100 | * sped up time for URL feeds and put them into one central function 101 | * greatly increased time for threat intelligence feed pulls and removed old intelligence update function and centralized into source feeds with appropriate checks 102 | * added sort IP after pulling in external source feeds and ATIF 103 | * cleaned up old codebase 104 | 105 | ~~~~~~~~~~~~~~~~~~~~~~ 106 | version 1.5.1 11/11/2015 107 | ~~~~~~~~~~~~~~~~~~~~~~ 108 | 109 | * fixed old git repo to new git repo at binarydefense 110 | 111 | ~~~~~~~~~~~~~~~~~~~~~~ 112 | version 1.5 11/1/2015 113 | ~~~~~~~~~~~~~~~~~~~~~~ 114 | 115 | * disabled yaml config as this breaks configuration reading 116 | * added better handling and ipv4 checking for remote feeds 117 | * changed threat feed to pull every 1 hour instead of 24 hours 118 | * fixed sort issue and malformed IP addresses due to sort not checking properly 119 | 120 | ~~~~~~~~~~~~~~~~~~~~~~ 121 | version 1.4.1 12/11/2015 122 | ~~~~~~~~~~~~~~~~~~~~~~ 123 | 124 | * added a new function for sorting IP addresses when adding to the banlist, when an IP address is added, it will sort the text file banlist after an append action 125 | * fixed an issue when artillery would initially launch it would not honor the ban() function for check to see if the IPs are already there. 126 | * fixed an issue when honeypot ban was specified to no, it would not populate the banlist even if it banned or not 127 | 128 | ~~~~~~~~~~~~~~~~~~~~~~ 129 | version 1.4 2/10/2015 130 | ~~~~~~~~~~~~~~~~~~~~~~ 131 | 132 | * added the ability to remove old records after a certain interval - this will overwrite the banlist and start from scratch based on certain time intervals - options are available under the config files 133 | * added multiple feeds based on the script from http://www.deepimpact.io/blog/splunkandfreeopen-sourcethreatintelligencefeeds - much appreciated. Artillery can now pull from multiple banlist IP addresses and will continue to add onto these as time goes on 134 | * added new functions to handle multiple different aspects including downloading files and new additions for parsing through banlist ips 135 | * turned banning to OFF by default and added threat intelligence feed to ON as defaults 136 | * added a check to turn on intelligence feed and also turn off ban if it is selected in the options this will allow you to pull intelligence feeds, check IP adddresses, and add to banlist, but not actively ban 137 | 138 | ~~~~~~~~~~~~~~~~~~~~~~ 139 | version 1.3.1 12/31/2014 140 | ~~~~~~~~~~~~~~~~~~~~~~ 141 | 142 | * added ability to change syslog port in the artillery config - if you are using a non-standard syslog port (defaults to 514) 143 | * removed old unused code in email handler and added better commenting 144 | * fixed an issue where starttls would cause open relays to bomb and require re-initiation 145 | * fixed a bug where from and to would not be properly sent from send mail function 146 | 147 | ~~~~~~~~~~~~~~~~~~~~~~ 148 | version 1.3 11/11/2014 149 | ~~~~~~~~~~~~~~~~~~~~~~ 150 | 151 | * added new function for grabbing date and time 152 | * added new function kill_artillery() to kill any running processes of artillery using signal and os.kill 153 | * added timestamp data for when a new instance of artillery is killed and restarted 154 | * removed old kill_artillery() legacy function in setup that did not properly terminate prior versions of artillery 155 | * added a write_log function that will notify when Artillery is unable to bind to a specific port and included timestamp data 156 | * fixed spacing issues inside of syslog on remote and local 157 | * corrected Issue identified spacing into one line in harden.py 158 | * added timestamp data to error artillery unable to log to mail server 159 | * fixed an issue that would cause the process to not properly be terminated on certain Linux versions 160 | * added datetime for when artillery successfully started from normal artillery.py located in /var/artillery 161 | * fixed a bug when using FILE as a designator that would cause an exception for alert not being defined (should have been assigned as variable) 162 | 163 | ~~~~~~~~~~~~~~~~~~~~~~ 164 | version 1.2 11/10/2014 165 | ~~~~~~~~~~~~~~~~~~~~~~ 166 | 167 | * switched company to Binary Defense Systems (BDS) 168 | 169 | ~~~~~~~~~~~~~~~~~~~~~~ 170 | version 1.1 10/17/2014 171 | ~~~~~~~~~~~~~~~~~~~~~~ 172 | 173 | * fixed loop issue when "local" was specified for syslog 174 | * added "file" as config option for saving to local files 175 | 176 | ~~~~~~~~~~~~~~~~~~~~~~ 177 | version 1.0.3 3/29/2014 178 | ~~~~~~~~~~~~~~~~~~~~~~ 179 | 180 | * removed a newline when honeypot puts an alert out 181 | 182 | ~~~~~~~~~~~~~~~~~~~~~~ 183 | version 1.0.2 2/24/2014 184 | ~~~~~~~~~~~~~~~~~~~~~~ 185 | 186 | * quick fix to add a check for database file, if its not there it'll create it 187 | 188 | ~~~~~~~~~~~~~~~~~~~~~~ 189 | version 1.0.1 2/20/2014 190 | ~~~~~~~~~~~~~~~~~~~~~~ 191 | 192 | * changed time delay if intelligence feed is turned to ON 193 | * turned intelligence feed to OFF by default in the config 194 | 195 | ~~~~~~~~~~~~~~~~~~~~~~ 196 | version 1.0 2/14/2014 197 | ~~~~~~~~~~~~~~~~~~~~~~ 198 | 199 | * added auto detection if artillery wasn't installed to kick off the setup.py script 200 | * fixed database creation error if directory wasn't already created during a fresh install 201 | * fixed an issue that would cause email_handler to throw an exceptions for mail() taking two arguments, needed to be changed to send_mail 202 | 203 | ~~~~~~~~~~~~~~~~~~~~~~ 204 | version 0.9 1/25/2014 205 | ~~~~~~~~~~~~~~~~~~~~~~ 206 | 207 | * removed smtp and merged into core 208 | * fixed duplicate mail() tag that was causing email alerts to not be sent 209 | * fixed a bug that would cause valid_ip() to throw an error due to library mismatch 210 | 211 | ~~~~~~~~~~~~~~~~~~~~~~ 212 | version 0.8.1 1/25/2014 213 | ~~~~~~~~~~~~~~~~~~~~~~ 214 | 215 | * added a better init.d script for startup, now supports stop/start. Will need to run setup.py in order to get the new file, or copy src/startup_artillery to /etc/init.d/artillery 216 | 217 | ~~~~~~~~~~~~~~~~~~~~~~ 218 | version 0.8 12/30/2013 219 | ~~~~~~~~~~~~~~~~~~~~~~ 220 | 221 | * large amounts of refactoring and recoding (thanks to plentz) 222 | * added ftp brute force detection (thanks johns3ej) 223 | 224 | ~~~~~~~~~~~~~~~~~~~~~~ 225 | version 0.7.4 10/18/2013 226 | ~~~~~~~~~~~~~~~~~~~~~~ 227 | 228 | * fixed issue with HONEYPOT_BAN being yes on occasions and ON on others. 229 | 230 | ~~~~~~~~~~~~~~~~~~~~~~ 231 | version 0.7.3 6/18/2013 232 | ~~~~~~~~~~~~~~~~~~~~~~ 233 | 234 | * fixed a bug that caused a 100% cpu spike on SSH monitoring 235 | 236 | ~~~~~~~~~~~~~~~~~~~~~~ 237 | version 0.7.2 6/11/2013 238 | ~~~~~~~~~~~~~~~~~~~~~~ 239 | 240 | * added detection for monitor_ssh in /var/log/faillog on debian 241 | * added better error handling within artillery.py in event of control-c or exceptions 242 | 243 | ~~~~~~~~~~~~~~~~~~~~~~ 244 | version 0.7.1 5/21/2013 245 | ~~~~~~~~~~~~~~~~~~~~~~ 246 | 247 | * allowed open mail relays for when artillery is deployed internally, will use connection to connect to SMTP servers without creds 248 | * added new flag to allow SMTP_FROM in the config, can now specify name coming from. For example "Artillery Alert Feed" 249 | * changed the mailSever.sendmail() to use the alert address as the to/from. 250 | * removed an old instance of code from src.core, no longer needed 251 | * added check for open relay to != None versus != "" 252 | * added new started to write_log message "Artillery has started successfully" 253 | * added ability to remove the default port check for the SSH harden config script, you can now turn this to off 254 | * added error handling to the mail program versus erroring out and closing artilery if the mail server was not properly found 255 | * added a check that looks for to see if honeypot_ban is active, if not the wording is changed to detected versus blocked 256 | * fixed a double import bug on src.core when import logging would not prompt anything more for the initial artillery service starting 257 | * added the start and restart via syslog, will not let you know via syslog when artillery is being restarted 258 | 259 | ~~~~~~~~~~~~~~~~~~~~~~ 260 | version 0.7 2/17/2013 261 | ~~~~~~~~~~~~~~~~~~~~~~ 262 | 263 | * changed everything over to github versus subversion 264 | * fixed installer to use github versus subversion 265 | * Artillery now stores logfiles for posix (nix based) in a syslog format. 266 | * You can now specify a local or remote syslog server in the config file 267 | * Bug fix for setup file would cause makedirs error, should be fixed 268 | 269 | ~~~~~~~~~~~~~~~~~~~~~~ 270 | version 0.6.7 2/2/2013 271 | ~~~~~~~~~~~~~~~~~~~~~~ 272 | 273 | * if monitoring is turned off, it will not import the module 274 | 275 | ~~~~~~~~~~~~~~~~~~~~~~ 276 | version 0.6.6 1/24/2013 277 | ~~~~~~~~~~~~~~~~~~~~~~ 278 | 279 | * fixed a typo that would cause artillery not to start in some cases 280 | * added folder create for src/program_junk and databases/ during installation of artillery 281 | 282 | ~~~~~~~~~~~~~~~~~~~~~~ 283 | version 0.6.5 1/20/2013 284 | ~~~~~~~~~~~~~~~~~~~~~~ 285 | 286 | * added new config option to disable root checks 287 | 288 | ~~~~~~~~~~~~~~~~~~~~~~ 289 | version 0.6.4 1/14/2013 290 | ~~~~~~~~~~~~~~~~~~~~~~ 291 | 292 | * Added the ability to detect what ports are being blocked via artillery honeypot (thanks spoonman) 293 | * Changed README to README.txt to fix an OSX bug 294 | 295 | ~~~~~~~~~~~~~~~~~~~~~~ 296 | version 0.6.3 12/31/2012 297 | ~~~~~~~~~~~~~~~~~~~~~~ 298 | 299 | * Move over to github 300 | 301 | ~~~~~~~~~~~~~~~~~~~~~~ 302 | version 0.6.2 303 | ~~~~~~~~~~~~~~~~~~~~~~ 304 | 305 | * Added better error handling around the threat intelligence feed 306 | * Added a better cleanup for Artlilery when uninstalling 307 | * Fixed a bug that caused Artillery to error out if banlist.txt was not found 308 | 309 | ~~~~~~~~~~~~~~~~~~~~~~ 310 | version 0.6.1 311 | ~~~~~~~~~~~~~~~~~~~~~~ 312 | 313 | * bugfix that would cause thread to die under certain circumstances 314 | * added terms to the banlist.txt file 315 | * fixed when config or artillery would start with comment signs 316 | 317 | ~~~~~~~~~~~~~~~~~~~~~~ 318 | version 0.6 319 | ~~~~~~~~~~~~~~~~~~~~~~ 320 | 321 | * fixed a bug in remove_ban that would not remove the ip address 322 | * added threat intelligence feed - this is an automatic feed that will pull from trustedsec webservers around attacker IP addresses 323 | * added ability to automatically block based on intelligence feed 324 | * daily checks added to banlist 325 | * fixed a bug when uninstall would not properly kill artillery 326 | * added a check in the uninstall to see if artillery is actually running 327 | * added some enhancements to the honeypot banning 328 | * added new flag for intelligence feed in the config file 329 | * added the ability to change threat feeds to a different server of your choice 330 | * added threading to reloading the IP tables matrix, was causing a hang on other imports 331 | * removed 3306 as a standard port, would cause conflicts at times if it was already installed 332 | * added the ability to specify the threat intelligence feed server 333 | * added the ability to configure your own threat intelligence feed server 334 | * added ability to change the public directory for the HTTP server 335 | * added ability to configure multiple threat feeds, can pull in multiple Artillery servers 336 | 337 | ~~~~~~~~~~~~~~~~~~~~~~ 338 | version 0.5.2 alpha 339 | ~~~~~~~~~~~~~~~~~~~~~~ 340 | 341 | * fixed a bug where ports were not properly starting 342 | * added the ability to uninstall artillery by re-running setup.py 343 | * performance improvement on honeypot 344 | 345 | ~~~~~~~~~~~~~~~~~~~~~~ 346 | version 0.5.1 alpha 347 | ~~~~~~~~~~~~~~~~~~~~~~ 348 | 349 | * added the ability to specify a NIC interface (thanks Niel) 350 | * fixed a bug when banlist.txt was not found, artillery would crash 351 | 352 | ~~~~~~~~~~~~~~~~~~~~~~ 353 | version 0.5 alpha 354 | ~~~~~~~~~~~~~~~~~~~~~~ 355 | 356 | * added OSX support for setup.py installation (thanks for the help Giulio Bortot) 357 | 358 | ~~~~~~~~~~~~~~~~~~~~~~ 359 | version 0.4 alpha 360 | ~~~~~~~~~~~~~~~~~~~~~~ 361 | 362 | * added ability to use cidr notations in the artillery config so you can do something like 127.0.0.1,localhost,192.168.235.1/24,etc. 363 | * code cleanup and commenting on multiple directories 364 | * added a number of new core modules, most specifically cidr notation support 365 | * changed install.py to be setup.py 366 | * moved root README to readme/ and deleted the old one 367 | * added better detection around restart_server.py if artillery was there 368 | * cleaned up some old threading syntax issues 369 | * made some changes to help support OSX (thanks for the help Giulio Bortot) 370 | 371 | ~~~~~~~~~~~~~~~~~~~~~~ 372 | version 0.3 alpha 373 | ~~~~~~~~~~~~~~~~~~~~~~ 374 | 375 | * added a check for ssh brute force on or off.. this was never implmeneted (thanks Jeff Bryner) 376 | * fixed a bug that referenced iptables chain INPU instead of ARTILLERY (thanks Jeff Bryner) 377 | * added the artillery chain to INPUT each time artillery starts (thanks Jeff Bryner) 378 | * cleaned up some old code in honeypot.py that was no longer needed 379 | * added better descriptions around why a specific IP address would be blocked 380 | * added timestamp data to when IP addresses are blocked in both email notifications as well as standard log under /var/artillery/log/ 381 | * added support for SMTP versus just gmail... its gmail out of the box however can configure any SMTP server now 382 | * added a check in artillery for ssh brute on or off 383 | 384 | ~~~~~~~~~~~~~~~~~~~~~~ 385 | version 0.2 alpha 386 | ~~~~~~~~~~~~~~~~~~~~~ 387 | 388 | * added a check to see if we are running on windows or linux 389 | * added a new anti-dos protection for linux, it will check connections and limit based on how many are connecting, you will probably want to adjust this per server 390 | * changed honeypot ban method to src.core through ban(ip) versus standalone call for iptables 391 | * changed iptable chains to be ARTILLERY versus piggy backing INPUT, much cleaner to view 392 | * fixed a bug that would cause duplicate entries into iptables and in banlist.txt 393 | * added functionality to support blacklisting via redirection routes on windows machines.. may have better alternatives but this works for now 394 | * added a ip check routine for when banning IP addresses, ensures sanitization if something crazy is inserted instead of an IP address 395 | * converted all core.py modules to be windows compliant 396 | * converted all of honeypot.py modules to be windows compliant 397 | * converted all of the monitor.py modules, this will only work for linux until I rewrite the module to support difflib versus the actual application diff 398 | * converted all of the ssh_brute.py modules to be windows compliant.. this will be linux only since nix is primarily used for SSH 399 | * converted all of the harden.py modules to be windows compliant.. this will be linux only since nix is primarily checked. Will expand later on others 400 | * fixed a bug that would not properly monitor the overall database for monitored files (thanks Pier) 401 | * fixed a bug that would not remove an ip properly 402 | 403 | ~~~~~~~~~~~~~~~~~~~~~~ 404 | version 0.1.7 alpha 405 | ~~~~~~~~~~~~~~~~~~~~~~ 406 | 407 | * fixed a bug that would not properly whitelist in the config file on honeypot or ssh_monitor 408 | * bug fix on honeypot that would cause it to ban without checking whitelist, thanks to Ryan Elkins 409 | * bug fix for the bug fix, copy and paste didn't work so well 410 | 411 | ~~~~~~~~~~~~~~~~~~~~~~ 412 | version 0.1.6 alpha 413 | ~~~~~~~~~~~~~~~~~~~~~~ 414 | 415 | * created new core function write_log for writing eventually to the logging directory 416 | * added writing of log file to /var/artillery/logs/alerts.log for all items regardless if email alerting is on or not 417 | * fixed an issue that would cause whitelist to not properly function in ssh ban 418 | 419 | ~~~~~~~~~~~~~~~~~~~~~~ 420 | version 0.1.5 alpha 421 | ~~~~~~~~~~~~~~~~~~~~~~ 422 | 423 | * added new email frequency handler for sending emails after a certain amount of time versus right away, should cut down on spam 424 | * added the new email frequency handler to all of the triggers/alerting systems 425 | * started to rewrite the monitor.py, it now hashes the two integrity databases before running diff, eventually will move completely off diff and into native python libraries 426 | 427 | ~~~~~~~~~~~~~~~~~~~~~~ 428 | version 0.1.4 alpha 429 | ~~~~~~~~~~~~~~~~~~~~~~ 430 | 431 | * added new large portions to ssh_monitor to fix potential issues 432 | * added whitelist capabilities into ssh_monitor.py 433 | * fixed a bug that caused multiple IP's on SSH ban to repeat and continue to ban the same IP address over and over 434 | * added new core module for adding path to check for config file 435 | * added remove_ban which will remove a banned IP address 436 | * turned auto-update off by default 437 | * added a fix in honeypot that would cause socket errors of a full connection wasnt made 438 | * added a fix in honeypot.py which would still continue to ban whitelisted ip addresses 439 | 440 | ~~~~~~~~~~~~~~~~~~~~~~ 441 | version 0.1.3 alpha 442 | ~~~~~~~~~~~~~~~~~~~~~~ 443 | 444 | * added better handling around list errors when parsing through filesystem 445 | * added the ban command through iptables to use -I INPUT 1 so that it's always first in the list (happens if other iptables are in affect) 446 | * fixed a counter issue when checking whitelist 447 | * added a new config option to leverage excludes for certain files that may change often 448 | * added a validate_ip routine to make sure the information being sent is in fact an IP address 449 | * removed almost all references to subprocess.popen and replaced with shutil, os.makedirs, os.stat, etc. 450 | * added restart_server.py 451 | 452 | ~~~~~~~~~~~~~~~~~~~~~ 453 | version 0.1.2 alpha 454 | ~~~~~~~~~~~~~~~~~~~~~ 455 | 456 | * added better detection of ssh logs for CentOS (thanks Quantum) to check for /var/log/secure as well as /var/log/auth.log 457 | * added a config option for automatic updates. if you want them automatic on or off (note you will need to whitelist svn.secmniac.com on port 80 for automatic updates 458 | * added config option for whitelisting IP addresses in the config file 459 | * fixed some code that on other systems may cause issues 460 | * fixed email delivery to harden.py and ssh_monitor, forgot to add import src.smtp 461 | * added email deliverys on honeypot blacklisting 462 | 463 | ~~~~~~~~~~~~~~~~~~~~~ 464 | version 0.1.1 alpha 465 | ~~~~~~~~~~~~~~~~~~~~~ 466 | 467 | * removed the majority of imports in artillery.py 468 | * added better handling over missing folders 469 | * fixed the installer 470 | * added better wording around what changes were detected 471 | * added time changes were detected 472 | * removed __init__.py, not needed 473 | * added directory checking for monitored folders..different platforms may not have the exact folders 474 | * changed port range to get detected through config versus hardcoded into src/honeypot.py..it will now use config to generate port ranges 475 | * added granularity if port was in use 476 | * added a generate random character sequence upon connect, will send a string between 5 and 30000 to the attacker...should be confusing :) 477 | * added src/harden.py which now checks for base configurations on a linux system that may be insecure in nature 478 | * added check for ssh log in harden.py to see if default port running on 22 and if running as root 479 | * added a check for /var/www to check permissions and ensure files are running as root 480 | * removed some un-necessary code in install and piped subprocess to stdout and stderr instead of /dev/null 481 | * added option to checkout svn during install, this will keep artillery up-to-date 482 | * added automatic-updating when artillery is launched 483 | * added threading to automatic-update to remove any delay that might happen if Internet isn't working 484 | * instead of using rc.local, install.py adds /etc/init.d/artillery then runs update-rc.d to add to startup scripts 485 | * added an automatic kill in install.py if Artillery was running on the system 486 | * added two more config options, one do you want to use the honeypot, second, do you want to automatically ban via honeypot 487 | * tweaked the database to lower CPU time and reduce overall footprint. New tests show it uses 0% of cpu at all times 488 | 489 | ~~~~~~~~~~~~~~~~~~~~~ 490 | version 0.1 alpha 491 | ~~~~~~~~~~~~~~~~~~~~~ 492 | 493 | * First release of Artillery 494 | -------------------------------------------------------------------------------- /readme/CREDITS: -------------------------------------------------------------------------------- 1 | Thanks to sp0rus for a bug fix in harden.py 2 | Thanks to bryogenic for email alerts in artillery 3 | Thanks to Ryan Elkins for bug report 4 | Thanks to Larry Spohn for finding the bugs in honeypot.py 5 | Thanks to Jeff Bryner for finding some bugs and sending source for additional enhancements 6 | Thanks to Giulio Bortot for adding some patches for OSX support and OSX support for setup.py 7 | Thanks to corelanc0d3r for is awesome additions to Artillery including cidr notations, better logging and failing messages, prefix ban lhandler, and more. 8 | Thanks to russhaun for his contributions on windows side of project 9 | -------------------------------------------------------------------------------- /readme/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020, Artillery 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer 8 | in the documentation and/or other materials provided with the distribution. 9 | * Neither the name of Artillery nor the names of its contributors may be used to endorse or promote products derived from 10 | this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 13 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 14 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 15 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 16 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 17 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 18 | 19 | The above licensing was taken from the BSD licensing and is applied to Artillery as well. 20 | 21 | Note that Artillery is provided as is, and is a royalty free open-source application. 22 | 23 | Feel free to modify, use, change, market, do whatever you want with it as long as you give the appropriate credit where credit 24 | is due (which means giving the authors the credit they deserve for writing it). Also note that by using this software, if you ever 25 | see the creator of Artillery in a bar, you are required to give him a hug and buy him a beer. Hug must last at least 5 seconds. Author 26 | holds the right to refuse the hug or the beer. 27 | -------------------------------------------------------------------------------- /remove_ban.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # simple remove banned ip 4 | # 5 | # 6 | import sys 7 | from src.core import * 8 | 9 | try: 10 | ipaddress = sys.argv[1] 11 | if is_valid_ipv4(ipaddress): 12 | path = check_banlist_path() 13 | fileopen = file(path, "r") 14 | data = fileopen.read() 15 | data = data.replace(ipaddress + "\n", "") 16 | filewrite = file(path, "w") 17 | filewrite.write(data) 18 | filewrite.close() 19 | 20 | print("Listing all iptables looking for a match... if there is a massive amount of blocked IP's this could take a few minutes..") 21 | proc = subprocess.Popen("iptables -L ARTILLERY -n -v --line-numbers | grep %s" % ( 22 | ipaddress), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 23 | 24 | for line in proc.stdout.readlines(): 25 | line = str(line) 26 | match = re.search(ipaddress, line) 27 | if match: 28 | # this is the rule number 29 | line = line.split(" ") 30 | line = line[0] 31 | print(line) 32 | # delete it 33 | subprocess.Popen("iptables -D ARTILLERY %s" % (line), 34 | stderr=subprocess.PIPE, stdout=subprocess.PIPE, shell=True) 35 | 36 | # if not valid then flag 37 | else: 38 | print("[!] Not a valid IP Address. Exiting.") 39 | sys.exit() 40 | 41 | except IndexError: 42 | print("Description: Simple removal of IP address from banned sites.") 43 | print("[!] Usage: remove_ban.py ") 44 | -------------------------------------------------------------------------------- /restart_server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # restart artillery 4 | # 5 | # 6 | import subprocess 7 | import os 8 | import signal 9 | from src.core import * 10 | 11 | init_globals() 12 | 13 | proc = subprocess.Popen( 14 | "ps -A x | grep artiller[y].py", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 15 | # kill running instance of artillery 16 | kill_artillery() 17 | 18 | print("[*] %s: Restarting Artillery Server..." % (grab_time())) 19 | if os.path.isfile("/var/artillery/artillery.py"): 20 | write_log("Restarting the Artillery Server process...",1) 21 | subprocess.Popen("python /var/artillery/artillery.py &", 22 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # quick script for installing artillery 4 | # 5 | 6 | import time 7 | import subprocess 8 | import re 9 | import os 10 | import shutil 11 | from src.core import * 12 | import sys 13 | import errno 14 | import argparse 15 | from src.pyuac import * # UAC Check Script found it here.https://gist.github.com/Preston-Landers/267391562bc96959eb41 all credit goes to him. 16 | try: input = raw_input 17 | except NameError: pass 18 | 19 | import src.globals 20 | 21 | # Argument parse. Aimed to provide automatic deployment options 22 | interactive = True # Flag to select interactive install, typically prompting user to answer [y/n] 23 | parser = argparse.ArgumentParser(description='-y, optional non interactive install/uninstall with automatic \'yes\' selection. It must roon with root/admin privileges') 24 | parser.add_argument("-y", action='store_true') 25 | args = parser.parse_args() 26 | if args.y: # Check if non-interactive install argument is provided using an apt parameter style, -y 27 | print("Running in non interactive mode with automatic \'yes\' selection") 28 | interactive = False; 29 | 30 | # Check to see if we are admin 31 | if is_windows(): 32 | if not isUserAdmin(): 33 | runAsAdmin()# will try to relaunch script as admin will prompt for user\pass and open in seperate window 34 | sys.exit(1) 35 | if isUserAdmin(): 36 | print(''' 37 | Welcome to the Artillery installer. Artillery is a honeypot, file monitoring, and overall security tool used to protect your nix systems. 38 | 39 | Written by: Dave Kennedy (ReL1K) 40 | ''') 41 | #create loop for install/uninstall not perfect but works saves answer for next step 42 | if not os.path.isfile("C:\\Program Files (x86)\\Artillery\\artillery.py"): 43 | if interactive: 44 | answer = input("[*] Do you want to install Artillery [y/n]: ") 45 | else: 46 | answer = 'y' 47 | #if above is false it must be installed so ask to uninstall 48 | else: 49 | if os.path.isfile("C:\\Program Files (x86)\\Artillery\\artillery.py") and interactive: 50 | #print("[*] [*] If you would like to uninstall hit y then enter") 51 | answer = input("[*] Artillery detected. Do you want to uninstall [y/n:] ") 52 | else: 53 | answer = 'y' 54 | #put this here to create loop 55 | if (answer.lower() in ["yes", "y"]) or not interactive: 56 | answer = "uninstall" 57 | 58 | # Check to see if we are root 59 | if is_posix(): 60 | try: # and delete folder 61 | if os.path.isdir("/var/artillery_check_root"): 62 | os.rmdir('/var/artillery_check_root') 63 | #if not thow error and quit 64 | except OSError as e: 65 | if (e.errno == errno.EACCES or e.errno == errno.EPERM): 66 | print ("You must be root to run this script!\r\n") 67 | sys.exit(1) 68 | print(''' 69 | Welcome to the Artillery installer. Artillery is a honeypot, file monitoring, and overall security tool used to protect your nix systems. 70 | 71 | Written by: Dave Kennedy (ReL1K) 72 | ''') 73 | #if we are root create loop for install/uninstall not perfect but works saves answer for next step 74 | if not os.path.isfile("/etc/init.d/artillery"): 75 | if interactive: 76 | answer = input("Do you want to install Artillery and have it automatically run when you restart [y/n]: ") 77 | else: 78 | answer = 'y' 79 | #if above is true it must be installed so ask to uninstall 80 | else: 81 | if os.path.isfile("/etc/init.d/artillery") and interactive: 82 | answer = input("[*] Artillery detected. Do you want to uninstall [y/n:] ") 83 | else: 84 | answer = 'y' 85 | #put this here to create loop 86 | if (answer.lower() in ["yes", "y"]) or not interactive: 87 | answer = "uninstall" 88 | 89 | if answer.lower() in ["yes", "y"]: 90 | init_globals() 91 | if is_posix(): 92 | #kill_artillery() 93 | 94 | print("[*] Beginning installation. This should only take a moment.") 95 | 96 | # if directories aren't there then create them 97 | #make root check folder here. Only root should 98 | #be able to create or delete this folder right? 99 | # leave folder for future installs/uninstall? 100 | if not os.path.isdir("/var/artillery_check_root"): 101 | os.makedirs("/var/artillery_check_root") 102 | if not os.path.isdir("/var/artillery/database"): 103 | os.makedirs("/var/artillery/database") 104 | if not os.path.isdir("/var/artillery/src/program_junk"): 105 | os.makedirs("/var/artillery/src/program_junk") 106 | 107 | # install to rc.local 108 | print("[*] Adding artillery into startup through init scripts..") 109 | if os.path.isdir("/etc/init.d"): 110 | if not os.path.isfile("/etc/init.d/artillery"): 111 | fileopen = open("src/startup_artillery", "r") 112 | config = fileopen.read() 113 | filewrite = open("/etc/init.d/artillery", "w") 114 | filewrite.write(config) 115 | filewrite.close() 116 | print("[*] Triggering update-rc.d on artillery to automatic start...") 117 | subprocess.Popen( 118 | "chmod +x /etc/init.d/artillery", shell=True).wait() 119 | subprocess.Popen( 120 | "update-rc.d artillery defaults", shell=True).wait() 121 | 122 | # remove old method if installed previously 123 | if os.path.isfile("/etc/init.d/rc.local"): 124 | fileopen = open("/etc/init.d/rc.local", "r") 125 | data = fileopen.read() 126 | data = data.replace( 127 | "sudo python /var/artillery/artillery.py &", "") 128 | filewrite = open("/etc/init.d/rc.local", "w") 129 | filewrite.write(data) 130 | filewrite.close() 131 | #Changed order of cmds. was giving error about file already exists. 132 | #also updated location to be the same accross all versions of Windows 133 | if is_windows(): 134 | program_files = os.environ["PROGRAMFILES(X86)"] 135 | install_path = os.getcwd() 136 | shutil.copytree(install_path, program_files + "\\Artillery\\") 137 | os.makedirs(program_files + "\\Artillery\\logs") 138 | os.makedirs(program_files + "\\Artillery\\database") 139 | os.makedirs(program_files + "\\Artillery\\src\\program_junk") 140 | 141 | 142 | if is_posix(): 143 | if interactive: 144 | choice = input("[*] Do you want to keep Artillery updated? (requires internet) [y/n]: ") 145 | else: 146 | choice = 'y' 147 | if choice in ["y", "yes"]: 148 | print("[*] Checking out Artillery through github to /var/artillery") 149 | # if old files are there 150 | if os.path.isdir("/var/artillery/"): 151 | shutil.rmtree('/var/artillery') 152 | subprocess.Popen( 153 | "git clone https://github.com/binarydefense/artillery /var/artillery/", shell=True).wait() 154 | print("[*] Finished. If you want to update Artillery go to /var/artillery and type 'git pull'") 155 | else: 156 | print("[*] Copying setup files over...") 157 | subprocess.Popen("cp -rf * /var/artillery/", shell=True).wait() 158 | 159 | # if os is Mac Os X than create a .plist daemon - changes added by 160 | # contributor - Giulio Bortot 161 | if os.path.isdir("/Library/LaunchDaemons"): 162 | # check if file is already in place 163 | if not os.path.isfile("/Library/LaunchDaemons/com.artillery.plist"): 164 | print("[*] Creating com.artillery.plist in your Daemons directory") 165 | filewrite = open( 166 | "/Library/LaunchDaemons/com.artillery.plist", "w") 167 | filewrite.write('\n\n\n\nDisabled\n\nProgramArguments\n\n/usr/bin/python\n/var/artillery/artillery.py\n\nKeepAlive\n\nRunAtLoad\n\nLabel\ncom.artillery\nDebug\n\n\n') 168 | print("[*] Adding right permissions") 169 | subprocess.Popen( 170 | "chown root:wheel /Library/LaunchDaemons/com.artillery.plist", shell=True).wait() 171 | 172 | check_config() 173 | if interactive: 174 | choice = input("[*] Would you like to start Artillery now? [y/n]: ") 175 | else: 176 | choice = 'y' 177 | if choice in ["yes", "y"]: 178 | if is_posix(): 179 | # this cmd is what they were refering to as "no longer supported"? from update-rc.d on install. 180 | # It looks like service starts but you have to manually launch artillery 181 | subprocess.Popen("/etc/init.d/artillery start", shell=True).wait() 182 | print("[*] Installation complete. Edit /var/artillery/config in order to config artillery to your liking") 183 | #added to start after install.launches in seperate window 184 | if is_windows(): 185 | os.chdir("src\windows") 186 | #copy over banlist 187 | os.system("start cmd /K banlist.bat") 188 | #Wait to make sure banlist is copied over 189 | time.sleep(2) 190 | #launch from install dir 191 | os.system("start cmd /K launch.bat") 192 | #cleanup cache folder 193 | time.sleep(2) 194 | os.system("start cmd /K del_cache.bat") 195 | 196 | 197 | #added root check to uninstall for linux 198 | if answer == "uninstall": 199 | if is_posix(): 200 | try: #check if the user is root 201 | if os.path.isdir("/var/artillery_check_root"): 202 | os.rmdir('/var/artillery_check_root') 203 | #if not throw an error and quit 204 | except OSError as e: 205 | if (e.errno == errno.EACCES or e.errno == errno.EPERM): 206 | print ("[*] You must be root to run this script!\r\n") 207 | sys.exit(1) 208 | else:# remove all of artillery 209 | os.remove("/etc/init.d/artillery") 210 | subprocess.Popen("rm -rf /var/artillery", shell=True) 211 | subprocess.Popen("rm -rf /etc/init.d/artillery", shell=True) 212 | #added to remove service files on kali2 213 | #subprocess.Popen("rm /lib/systemd/system/artillery.service", shell=True) 214 | #kill_artillery() 215 | print("[*] Artillery has been uninstalled. Manually kill the process if it is still running.") 216 | #Delete routine to remove artillery on windows.added uac check 217 | if is_windows(): 218 | if not isUserAdmin(): 219 | runAsAdmin() 220 | if isUserAdmin(): 221 | #remove program files 222 | subprocess.call(['cmd', '/C', 'rmdir', '/S', '/Q', 'C:\\Program Files (x86)\\Artillery']) 223 | #del uninstall cache 224 | os.chdir("src\windows") 225 | os.system("start cmd /K del_cache.bat") 226 | #just so they can see this message slleep a sec 227 | print("[*] Artillery has been uninstalled.\n[*] Manually kill the process if it is still running.") 228 | time.sleep(3) 229 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryDefense/artillery/805a5d858bf1cc3f6de5c3a5083b2d0d21136ebb/src/__init__.py -------------------------------------------------------------------------------- /src/anti_dos.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # basic for now, more to come 4 | # 5 | # 6 | import subprocess 7 | from src.core import * 8 | 9 | anti_dos_ports = read_config("ANTI_DOS_PORTS") 10 | anti_dos_throttle = read_config("ANTI_DOS_THROTTLE_CONNECTIONS") 11 | anti_dos_burst = read_config("ANTI_DOS_LIMIT_BURST") 12 | 13 | if is_config_enabled("ANTI_DOS"): 14 | # basic throttle for some ports 15 | anti_dos_ports = anti_dos_ports.split(",") 16 | for ports in anti_dos_ports: 17 | subprocess.Popen("iptables -A ARTILLERY -p tcp --dport %s -m limit --limit %s/minute --limit-burst %s -j ACCEPT" % 18 | (ports, anti_dos_throttle, anti_dos_burst), stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).wait() 19 | -------------------------------------------------------------------------------- /src/apache_monitor.py: -------------------------------------------------------------------------------- 1 | # 127.0.0.1 - - [10/Mar/2012:15:35:53 -0500] "GET /sdfsdfds.dsfds 2 | # HTTP/1.1" 404 501 "-" "Mozilla/5.0 (X11; Linux i686 on x86_64; 3 | # rv:10.0.2) Gecko/20100101 Firefox/10.0.2" 4 | 5 | 6 | def tail(some_file): 7 | this_file = open(some_file) 8 | # Go to the end of the file 9 | this_file.seek(0, 2) 10 | 11 | while True: 12 | line = this_file.readline() 13 | if line: 14 | yield line 15 | yield None 16 | 17 | # grab the access logs and tail them 18 | access = "/var/log/apache2/access.log" 19 | access_log = tail(access) 20 | 21 | # grab the error logs and tail them 22 | errors = "/var/log/apache2/error.log" 23 | error_log = tail(errors) 24 | -------------------------------------------------------------------------------- /src/artillery_service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Artillery Service 3 | 4 | [Service] 5 | ExecStart=/usr/bin/python /var/artillery/artillery.py start 6 | ExecStop=/usr/bin/pyton /var/artillery/artillery.py stop 7 | 8 | [Install] 9 | WantedBy=multi-user.target 10 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # config module for configuration reading/writing/translating 4 | # 5 | # module is disabled for now as this breaks config reading 6 | 7 | import os 8 | import platform 9 | from . import globals 10 | 11 | if platform.system() == "Windows": 12 | import ntpath 13 | 14 | import yaml 15 | 16 | from src.core import * 17 | 18 | 19 | def get_config_path(): 20 | path = "" 21 | # ToDo: Support for command line argument pointing to config file. 22 | if is_posix(): 23 | if os.path.isfile(globals.g_configfile): 24 | path = globals.g_configfile 25 | #if os.path.isfile("config"): 26 | # path = "config" 27 | if is_windows(): 28 | program_files = os.environ["PROGRAMFILES(X86)"] 29 | if os.path.isfile(globals.g_configfile): 30 | path = globals.g_configfile 31 | return path 32 | 33 | 34 | def read_config(param): 35 | path = get_config_path() 36 | if is_windows(): 37 | name = ntpath.basename(path) 38 | elif is_posix(): 39 | dirs, name = os.path.split(path) 40 | 41 | exten = name.split(".")[-1] 42 | 43 | if ((not exten) or (exten == 'ini')): 44 | return read_config_ini(path, param) 45 | elif (exten == 'yaml'): 46 | return read_config_yaml(path, param) 47 | 48 | return "" 49 | 50 | 51 | def read_config_ini(path, param): 52 | fileopen = file(path, "r") 53 | for line in fileopen: 54 | if not line.startswith("#"): 55 | match = re.search(param + "=", line) 56 | if match: 57 | line = line.rstrip() 58 | line = line.replace('"', "") 59 | line = line.split("=") 60 | return line[1] 61 | 62 | 63 | def read_config_yaml(path, param): 64 | fileopen = open(path, "r") 65 | configTree = yaml.safe_load(fileopen) 66 | fileopen.close() 67 | if (configTree): 68 | return configTree.get(param, None) 69 | 70 | 71 | def is_config_enabled(param): 72 | config = read_config(param).lower() 73 | return config in ("on", "yes", "true") 74 | -------------------------------------------------------------------------------- /src/core.py: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | # core module for reusable / central code 4 | # 5 | # 6 | #python2to3 7 | import string 8 | import random 9 | import smtplib 10 | try: 11 | from email.MIMEMultipart import MIMEMultipart 12 | from email.MIMEBase import MIMEBase 13 | from email.MIMEText import MIMEText 14 | from email import Encoders 15 | from email.utils import formatdate 16 | except ImportError: 17 | 18 | from email.mime.multipart import MIMEMultipart 19 | from email.utils import formatdate 20 | from email.mime.base import MIMEBase 21 | from email.mime.text import MIMEText 22 | from email import encoders 23 | 24 | import os 25 | import re 26 | import subprocess 27 | import urllib 28 | import socket 29 | import struct 30 | import sys 31 | 32 | # for python 2 vs 3 compatibility 33 | try: 34 | from urllib.request import urlopen 35 | from urllib.parse import urlparse 36 | except ImportError: 37 | from urlparse import urlparse 38 | from urllib import urlopen 39 | 40 | import os 41 | import sys 42 | import time 43 | import shutil 44 | import logging 45 | import logging.handlers 46 | import datetime 47 | import signal 48 | from string import * 49 | # from string import split, join 50 | import socket 51 | import traceback 52 | 53 | from . import globals 54 | 55 | 56 | # initialize global vars 57 | def init_globals(): 58 | if 'win32' in sys.platform: 59 | programfolder = os.environ["PROGRAMFILES(X86)"] 60 | globals.g_apppath = programfolder + "\\Artillery" 61 | globals.g_appfile = globals.g_apppath + "\\artillery.py" 62 | globals.g_configfile = globals.g_apppath + "\\config" 63 | globals.g_banlist = globals.g_apppath + "\\banlist.txt" 64 | globals.g_localbanlist = globals.g_apppath + "\\localbanlist.txt" 65 | 66 | # consolidated nix* variants 67 | if ('linux' or 'linux2' or 'darwin') in sys.platform: 68 | globals.g_apppath = "/var/artillery" 69 | globals.g_appfile = globals.g_apppath + "/artillery.py" 70 | globals.g_configfile = globals.g_apppath + "/config" 71 | globals.g_banlist = globals.g_apppath + "/banlist.txt" 72 | globals.g_localbanlist = globals.g_apppath + "/localbanlist.txt" 73 | 74 | 75 | # grab the current time 76 | def grab_time(): 77 | ts = time.time() 78 | return datetime.datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M:%S') 79 | 80 | # get hostname 81 | def gethostname(): 82 | return socket.gethostname() 83 | 84 | # create a brand new config file 85 | def create_config(configpath, configdefaults, keyorder): 86 | configpath = configpath 87 | configfile = open(configpath, "w") 88 | write_console("Creating/updating config file '%s'" % configpath) 89 | write_log("Creating config file %s" % (configpath)) 90 | banner = "#############################################################################################\n" 91 | banner += "#\n" 92 | banner += "# This is the Artillery configuration file. Change these variables and flags to change how\n" 93 | banner += "# this behaves.\n" 94 | banner += "#\n" 95 | banner += "# Artillery written by: Dave Kennedy (ReL1K)\n" 96 | banner += "# Website: https://www.binarydefense.com\n" 97 | banner += "# Email: info [at] binarydefense.com\n" 98 | banner += "# Download: git clone https://github.com/binarydefense/artillery artillery/\n" 99 | banner += "# Install: python setup.py\n" 100 | banner += "#\n" 101 | banner += "#############################################################################################\n" 102 | banner += "#\n" 103 | configfile.write(banner) 104 | for configkey in keyorder: 105 | newline_comment = "\n# %s\n" % configdefaults[configkey][1] 106 | newline_config = "%s=\"%s\"\n" % (configkey, configdefaults[configkey][0]) 107 | configfile.write(newline_comment) 108 | configfile.write(newline_config) 109 | configfile.close() 110 | return 111 | 112 | 113 | def check_config(): 114 | # populate defaults 115 | configdefaults = {} 116 | configdefaults["MONITOR"] = ["ON", "DETERMINE IF YOU WANT TO MONITOR OR NOT"] 117 | configdefaults["MONITOR_FOLDERS"] = ["\"/var/www\",\"/etc/\"", "THESE ARE THE FOLDERS TO MONITOR, TO ADD MORE, JUST DO \"/root\",\"/var/\", etc."] 118 | configdefaults["MONITOR_FREQUENCY"] = ["60", "BASED ON SECONDS, 2 = 2 seconds."] 119 | configdefaults["SYSTEM_HARDENING"] = ["ON", "PERFORM CERTAIN SYSTEM HARDENING CHECKS"] 120 | configdefaults["SSH_DEFAULT_PORT_CHECK"] = ["ON", "CHECK/WARN IF SSH IS RUNNING ON PORT 22"] 121 | configdefaults["EXCLUDE"] = ["","EXCLUDE CERTAIN DIRECTORIES OR FILES. USE FOR EXAMPLE: /etc/passwd,/etc/hosts.allow"] 122 | configdefaults["HONEYPOT_BAN"] = ["OFF", "DO YOU WANT TO AUTOMATICALLY BAN ON THE HONEYPOT"] 123 | configdefaults["HONEYPOT_BAN_CLASSC"] = ["OFF","WHEN BANNING, DO YOU WANT TO BAN ENTIRE CLASS C AT ONCE INSTEAD OF INDIVIDUAL IP ADDRESS"] 124 | configdefaults["HONEYPOT_BAN_LOG_PREFIX"] = ["","PUT A PREFIX ON ALL BANNED IP ADDRESSES. HELPFUL FOR WHEN TRYING TO PARSE OR SHOW DETECTIONS THAT YOU ARE PIPING OFF TO OTHER SYSTEMS. WHEN SET, PREFIX IPTABLES LOG ENTRIES WITH THE PROVIDED TEXT"] 125 | configdefaults["WHITELIST_IP"] = ["127.0.0.1,localhost", "WHITELIST IP ADDRESSES, SPECIFY BY COMMAS ON WHAT IP ADDRESSES YOU WANT TO WHITELIST"] 126 | configdefaults["TCPPORTS"] = ["22,1433,8080,21,5060,5061,5900,25,53,110,1723,1337,10000,5800,44443,16993","TCP PORTS TO SPAWN HONEYPOT FOR"] 127 | configdefaults["UDPPORTS"] = ["123,53,5060,5061,3478", "UDP PORTS TO SPAWN HONEYPOT FOR"] 128 | configdefaults["HONEYPOT_AUTOACCEPT"] = ["ON", "SHOULD THE HONEYPOT AUTOMATICALLY ADD ACCEPT RULES TO THE ARTILLERY CHAIN FOR ANY PORTS ITS LISTENING ON"] 129 | configdefaults["EMAIL_ALERTS"] = ["OFF","SHOULD EMAIL ALERTS BE SENT"] 130 | configdefaults["SMTP_USERNAME"] = ["","CURRENT SUPPORT IS FOR SMTP. ENTER YOUR USERNAME AND PASSWORD HERE FOR STARTTLS AUTHENTICATION. LEAVE BLANK FOR OPEN RELAY"] 131 | configdefaults["SMTP_PASSWORD"] = ["","ENTER SMTP PASSWORD HERE"] 132 | configdefaults["ALERT_USER_EMAIL"] = ["enter_your_email_address_here@localhost","THIS IS WHO TO SEND THE ALERTS TO - EMAILS WILL BE SENT FROM ARTILLERY TO THIS ADDRESS"] 133 | configdefaults["SMTP_FROM"] = ["Artillery_Incident@localhost","FOR SMTP ONLY HERE, THIS IS THE MAILTO"] 134 | configdefaults["SMTP_ADDRESS"] = ["smtp.gmail.com","SMTP ADDRESS FOR SENDING EMAIL, DEFAULT IS GMAIL"] 135 | configdefaults["SMTP_PORT"] = ["587","SMTP PORT FOR SENDING EMAILS DEFAULT IS GMAIL WITH STARTTLS"] 136 | configdefaults["EMAIL_TIMER"] = ["ON","THIS WILL SEND EMAILS OUT DURING A CERTAIN FREQUENCY. IF THIS IS SET TO OFF, ALERTS WILL BE SENT IMMEDIATELY (CAN LEAD TO A LOT OF SPAM)"] 137 | configdefaults["EMAIL_FREQUENCY"] = ["600", "HOW OFTEN DO YOU WANT TO SEND EMAIL ALERTS (DEFAULT 10 MINUTES) - IN SECONDS"] 138 | configdefaults["SSH_BRUTE_MONITOR"] = ["ON", "DO YOU WANT TO MONITOR SSH BRUTE FORCE ATTEMPTS"] 139 | configdefaults["SSH_BRUTE_ATTEMPTS"] = ["4", "HOW MANY ATTEMPTS BEFORE YOU BAN"] 140 | configdefaults["FTP_BRUTE_MONITOR"] = ["OFF", "DO YOU WANT TO MONITOR FTP BRUTE FORCE ATTEMPTS"] 141 | configdefaults["FTP_BRUTE_ATTEMPTS"] = ["4", "HOW MANY ATTEMPTS BEFORE YOU BAN"] 142 | configdefaults["AUTO_UPDATE"] = ["ON", "DO YOU WANT TO DO AUTOMATIC UPDATES - ON OR OFF"] 143 | configdefaults["ANTI_DOS"] = ["OFF", "ANTI DOS WILL CONFIGURE MACHINE TO THROTTLE CONNECTIONS, TURN THIS OFF IF YOU DO NOT WANT TO USE"] 144 | configdefaults["ANTI_DOS_PORTS"] = ["80,443", "THESE ARE THE PORTS THAT WILL PROVIDE ANTI_DOS PROTECTION"] 145 | configdefaults["ANTI_DOS_THROTTLE_CONNECTIONS"] = ["50", "THIS WILL THROTTLE HOW MANY CONNECTIONS PER MINUTE ARE ALLOWED HOWEVER THE BUST WILL ENFORCE THIS"] 146 | configdefaults["ANTI_DOS_LIMIT_BURST"] = ["200", "THIS WILL ONLY ALLOW A CERTAIN BURST PER MINUTE THEN WILL ENFORCE AND NOT ALLOW ANYMORE TO CONNECT"] 147 | configdefaults["ACCESS_LOG"] = ["/var/log/apache2/access.log", "THIS IS THE PATH FOR THE APACHE ACCESS LOG"] 148 | configdefaults["ERROR_LOG"] = ["/var/log/apache2/error.log", "THIS IS THE PATH FOR THE APACHE ERROR LOG"] 149 | configdefaults["BIND_INTERFACE"] = ["","THIS ALLOWS YOU TO SPECIFY AN IP ADDRESS. LEAVE THIS BLANK TO BIND TO ALL INTERFACES."] 150 | configdefaults["THREAT_INTELLIGENCE_FEED"] = ["ON", "TURN ON INTELLIGENCE FEED, CALL TO https://www.binarydefense.com/banlist.txt IN ORDER TO GET ALREADY KNOWN MALICIOUS IP ADDRESSES. WILL PULL EVERY 24 HOURS"] 151 | configdefaults["THREAT_FEED"] = ["https://www.binarydefense.com/banlist.txt","CONFIGURE THIS TO BE WHATEVER THREAT FEED YOU WANT BY DEFAULT IT WILL USE BINARY DEFENSE - NOTE YOU CAN SPECIFY MULTIPLE THREAT FEEDS BY DOING #http://urlthreatfeed1,http://urlthreadfeed2"] 152 | configdefaults["THREAT_SERVER"] = ["OFF", "A THREAT SERVER IS A SERVER THAT WILL COPY THE BANLIST.TXT TO A PUBLIC HTTP LOCATION TO BE PULLED BY OTHER ARTILLERY SERVER. THIS IS USED IF YOU DO NOT WANT TO USE THE STANDARD BINARY DEFENSE ONE."] 153 | configdefaults["THREAT_LOCATION"] = ["/var/www/","PUBLIC LOCATION TO PULL VIA HTTP ON THE THREAT SERVER. NOTE THAT THREAT SERVER MUST BE SET TO ON"] 154 | configdefaults["ROOT_CHECK"] = ["ON", "THIS CHECKS TO SEE WHAT PERMISSIONS ARE RUNNING AS ROOT IN A WEB SERVER DIRECTORY"] 155 | configdefaults["SYSLOG_TYPE"] = ["LOCAL", "Specify SYSLOG TYPE to be local, file or remote. LOCAL will pipe to syslog, REMOTE will pipe to remote SYSLOG, and file will send to alerts.log in local artillery directory"] 156 | configdefaults["LOG_MESSAGE_ALERT"] = ["Artillery has detected an attack from %ip% for a connection on a honeypot port %port%", "ALERT LOG MESSAGES (You can use the following variables: %time%, %ip%, %port%)"] 157 | configdefaults["LOG_MESSAGE_BAN"] = ["Artillery has blocked (and blacklisted) an attack from %ip% for a connection to a honeypot restricted port %port%", "BAN LOG MESSAGES (You can use the following variables: %time%, %ip%, %port%)"] 158 | configdefaults["SYSLOG_REMOTE_HOST"] = ["192.168.0.1","IF YOU SPECIFY SYSLOG TYPE TO REMOTE, SPECIFY A REMOTE SYSLOG SERVER TO SEND ALERTS TO"] 159 | configdefaults["SYSLOG_REMOTE_PORT"] = ["514", "IF YOU SPECIFY SYSLOG TYPE OF REMOTE, SEPCIFY A REMOTE SYSLOG PORT TO SEND ALERTS TO"] 160 | configdefaults["CONSOLE_LOGGING"] = ["ON", "TURN ON CONSOLE LOGGING"] 161 | configdefaults["RECYCLE_IPS"] = ["OFF", "RECYCLE LOGS AFTER A CERTAIN AMOUNT OF TIME - THIS WILL WIPE ALL IP ADDRESSES AND START FROM SCRATCH AFTER A CERTAIN INTERVAL"] 162 | configdefaults["ARTILLERY_REFRESH"] = ["604800", "RECYCLE INTERVAL AFTER A CERTAIN AMOUNT OF MINUTES IT WILL OVERWRITE THE LOG WITH A BLANK ONE AND ELIMINATE THE IPS - DEFAULT IS 7 DAYS"] 163 | configdefaults["SOURCE_FEEDS"] = ["OFF", "PULL ADDITIONAL SOURCE FEEDS FOR BANNED IP LISTS FROM MULTIPLE OTHER SOURCES OTHER THAN ARTILLERY"] 164 | configdefaults["LOCAL_BANLIST"] = ["OFF", "CREATE A SEPARATE LOCAL BANLIST FILE (USEFUL IF YOU'RE ALSO USING A THREAT FEED AND WANT TO HAVE A FILE THAT CONTAINS THE IPs THAT HAVE BEEN BANNED LOCALLY"] 165 | configdefaults["THREAT_FILE"] = ["banlist.txt", "FILE TO COPY TO THREAT_LOCATION, TO ACT AS A THREAT_SERVER. CHANGE TO \"localbanlist.txt\" IF YOU HAVE ENABLED \"LOCAL_BANLIST\" AND WISH TO HOST YOUR LOCAL BANLIST. IF YOU WISH TO COPY BOTH FILES, SEPARATE THE FILES WITH A COMMA - f.i. \"banlist.txt,localbanlist.txt\""] 166 | 167 | keyorder = [] 168 | keyorder.append("MONITOR") 169 | keyorder.append("MONITOR_FOLDERS") 170 | keyorder.append("MONITOR_FREQUENCY") 171 | keyorder.append("SYSTEM_HARDENING") 172 | keyorder.append("SSH_DEFAULT_PORT_CHECK") 173 | keyorder.append("EXCLUDE") 174 | keyorder.append("HONEYPOT_BAN") 175 | keyorder.append("HONEYPOT_BAN_CLASSC") 176 | keyorder.append("HONEYPOT_BAN_LOG_PREFIX") 177 | keyorder.append("WHITELIST_IP") 178 | keyorder.append("TCPPORTS") 179 | keyorder.append("UDPPORTS") 180 | keyorder.append("HONEYPOT_AUTOACCEPT") 181 | keyorder.append("EMAIL_ALERTS") 182 | keyorder.append("SMTP_USERNAME") 183 | keyorder.append("SMTP_PASSWORD") 184 | keyorder.append("ALERT_USER_EMAIL") 185 | keyorder.append("SMTP_FROM") 186 | keyorder.append("SMTP_ADDRESS") 187 | keyorder.append("SMTP_PORT") 188 | keyorder.append("EMAIL_TIMER") 189 | keyorder.append("EMAIL_FREQUENCY") 190 | keyorder.append("SSH_BRUTE_MONITOR") 191 | keyorder.append("SSH_BRUTE_ATTEMPTS") 192 | keyorder.append("FTP_BRUTE_MONITOR") 193 | keyorder.append("FTP_BRUTE_ATTEMPTS") 194 | keyorder.append("AUTO_UPDATE") 195 | keyorder.append("ANTI_DOS") 196 | keyorder.append("ANTI_DOS_PORTS") 197 | keyorder.append("ANTI_DOS_THROTTLE_CONNECTIONS") 198 | keyorder.append("ANTI_DOS_LIMIT_BURST") 199 | keyorder.append("ACCESS_LOG") 200 | keyorder.append("ERROR_LOG") 201 | keyorder.append("BIND_INTERFACE") 202 | keyorder.append("THREAT_INTELLIGENCE_FEED") 203 | keyorder.append("THREAT_FEED") 204 | keyorder.append("THREAT_SERVER") 205 | keyorder.append("THREAT_LOCATION") 206 | keyorder.append("THREAT_FILE") 207 | keyorder.append("LOCAL_BANLIST") 208 | keyorder.append("ROOT_CHECK") 209 | keyorder.append("SYSLOG_TYPE") 210 | keyorder.append("LOG_MESSAGE_ALERT") 211 | keyorder.append("LOG_MESSAGE_BAN") 212 | keyorder.append("SYSLOG_REMOTE_HOST") 213 | keyorder.append("SYSLOG_REMOTE_PORT") 214 | keyorder.append("CONSOLE_LOGGING") 215 | keyorder.append("RECYCLE_IPS") 216 | keyorder.append("ARTILLERY_REFRESH") 217 | keyorder.append("SOURCE_FEEDS") 218 | for key in configdefaults: 219 | if not key in keyorder: 220 | keyorder.append(key) 221 | 222 | 223 | # read config file 224 | createnew = False 225 | configpath = get_config_path() 226 | if os.path.exists(configpath): 227 | # read existing config file, update dict 228 | write_console("Checking existing config file '%s'" % configpath) 229 | for configkey in configdefaults: 230 | if config_exists(configkey): 231 | currentcomment = configdefaults[configkey][1] 232 | currentvalue = read_config(configkey) 233 | configdefaults[configkey] = [currentvalue, currentcomment] 234 | else: 235 | write_console(" Adding new config key '%s', default value '%s'" % (configkey,configdefaults[configkey][0])) 236 | else: 237 | createnew = True 238 | #config file does not exist, determine new path 239 | 240 | # write dict to file 241 | create_config(globals.g_configfile, configdefaults, keyorder) 242 | 243 | if createnew: 244 | msg = "A brand new config file '%s' was created. Please review the file, change as needed, and launch artillery (again)." % globals.g_configfile 245 | write_console(msg) 246 | write_log(msg,1) 247 | #sys.exit(1) 248 | 249 | return 250 | 251 | 252 | 253 | 254 | def get_config_path(): 255 | #path = "" 256 | #if is_posix(): 257 | # if os.path.isfile("/var/artillery/config"): 258 | # path = "/var/artillery/config" 259 | # if os.path.isfile("config"): 260 | # path = "config" 261 | #changed path to be more consistant across windows versions 262 | #if is_windows(): 263 | # program_files = os.environ["PROGRAMFILES(X86)"] 264 | # if os.path.isfile(program_files + "\\Artillery\\config"): 265 | # path = program_files + "\\Artillery\\config" 266 | path = globals.g_configfile 267 | return path 268 | 269 | 270 | 271 | # check if a certain config parameter exists in the current config file 272 | def config_exists(param): 273 | path = get_config_path() 274 | fileopen = open(path, "r") 275 | paramfound = False 276 | for line in fileopen: 277 | if not line.startswith("#"): 278 | match = re.search(param + "=", line) 279 | if match: 280 | paramfound = True 281 | return paramfound 282 | 283 | 284 | def read_config(param): 285 | path = get_config_path() 286 | fileopen = open(path, "r") 287 | for line in fileopen: 288 | if not line.startswith("#"): 289 | match = re.search(param + "=", line) 290 | if match: 291 | line = line.rstrip() 292 | line = line.replace('"', "") 293 | line = line.split("=") 294 | return line[1] 295 | return "" 296 | 297 | 298 | def convert_to_classc(param): 299 | ipparts = param.split('.') 300 | classc = "" 301 | if len(ipparts) == 4: 302 | classc = ipparts[0]+"."+ipparts[1]+"."+ipparts[2]+".0/24" 303 | return classc 304 | 305 | 306 | def is_config_enabled(param): 307 | try: 308 | config = read_config(param).lower() 309 | return config in ("on", "yes") 310 | 311 | except AttributeError: 312 | return "off" 313 | 314 | 315 | def ban(ip): 316 | # ip check routine to see if its a valid IP address 317 | ip = ip.rstrip() 318 | ban_check = read_config("HONEYPOT_BAN").lower() 319 | ban_classc = read_config("HONEYPOT_BAN_CLASSC").lower() 320 | test_ip = ip 321 | if "/" in test_ip: 322 | test_ip = test_ip.split("/")[0] 323 | if is_whitelisted_ip(test_ip): 324 | write_log("Not banning IP %s, whitelisted" % test_ip) 325 | return 326 | if ban_check == "on": 327 | if not ip.startswith("#"): 328 | if not ip.startswith("0."): 329 | if is_valid_ipv4(ip.strip()): 330 | # if we are running nix variant then trigger ban through 331 | # iptables 332 | if is_posix(): 333 | if not is_already_banned(ip): 334 | if ban_classc == "on": 335 | ip = convert_to_classc(ip) 336 | subprocess.Popen( 337 | "iptables -I ARTILLERY 1 -s %s -j DROP" % ip, shell=True).wait() 338 | iptables_logprefix = read_config("HONEYPOT_BAN_LOG_PREFIX") 339 | if iptables_logprefix != "": 340 | subprocess.Popen("iptables -I ARTILLERY 1 -s %s -j LOG --log-prefix \"%s\"" % (ip, iptables_logprefix), shell=True).wait() 341 | 342 | 343 | # if running windows then route attacker to some bs address. 344 | if is_windows(): 345 | #lets try and write an event log 346 | HoneyPotEvent() 347 | #now lets block em or mess with em route somewhere else? 348 | routecmd = "route ADD %s MASK 255.255.255.255 10.255.255.255" 349 | if ban_check == 'on': 350 | #this is dangerous you can inadvertantly 351 | #lock your self out of the network be careful with this 352 | #cool idea non the less 353 | if ban_classc == "on": 354 | ip = convert_to_classc(ip) 355 | ipparts = ip.split(".") 356 | routecmd = "route ADD %s.%s.%s.0 MASK 255.255.255.0 10.255.255.255" % (ipparts[0], ipparts[1], ipparts[2]) 357 | subprocess.Popen("%s" % (routecmd), 358 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 359 | else: 360 | # or use the old way and just ban the individual ip 361 | subprocess.Popen(routecmd % (ip), 362 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 363 | 364 | 365 | # add new IP to banlist 366 | fileopen = open(globals.g_banlist, "r") 367 | data = fileopen.read() 368 | if ip not in data: 369 | filewrite = open(globals.g_banlist, "a") 370 | filewrite.write(ip + "\n") 371 | #print("Added %s to file" % ip) 372 | filewrite.close() 373 | sort_banlist() 374 | 375 | if read_config("LOCAL_BANLIST").lower() == "on": 376 | fileopen = open(globals.g_localbanlist, "r") 377 | data = fileopen.read() 378 | if ip not in data: 379 | filewrite = open(globals.g_localbanlist, "a") 380 | filewrite.write(ip+"\n") 381 | filewrite.close() 382 | 383 | 384 | 385 | 386 | def update(): 387 | if is_posix(): 388 | write_log("Running auto update (git pull)") 389 | write_console("Running auto update (git pull)") 390 | 391 | if os.path.isdir(globals.g_apppath + "/.svn"): 392 | print( 393 | "[!] Old installation detected that uses subversion. Fixing and moving to github.") 394 | try: 395 | if len(globals.g_apppath) > 1: 396 | shutil.rmtree(globals.g_apppath) 397 | subprocess.Popen( 398 | "git clone https://github.com/binarydefense/artillery", shell=True).wait() 399 | except: 400 | print( 401 | "[!] Something failed. Please type 'git clone https://github.com/binarydefense/artillery %s' to fix!" % globals.g_apppath) 402 | 403 | #subprocess.Popen("cd %s;git pull" % globals.g_apppath, 404 | # stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 405 | update = execOScmd("cd %s; git pull" % globals.g_apppath) 406 | errorfound = False 407 | abortfound = False 408 | errormsg = "" 409 | for l in update: 410 | errormsg += "%s\n" % l 411 | if "error:" in l: 412 | errorfound = True 413 | if "Aborting" in l: 414 | abortfound = True 415 | if errorfound and abortfound: 416 | msg = "Error updating artillery, git pull was aborted. Error:\n%s" % errormsg 417 | write_log(msg,2) 418 | write_console(msg) 419 | msg = "I will make a cop of the config file, run git stash, and restore config file" 420 | write_log(msg,2) 421 | write_console(msg) 422 | saveconfig = "cp '%s' '%s.old'" % (globals.g_configfile, globals.g_configfile) 423 | execOScmd(saveconfig) 424 | gitstash = "git stash" 425 | execOScmd(gitstash) 426 | gitpull = "git pull" 427 | newpull = execOScmd(gitpull) 428 | restoreconfig = "cp '%s.old' '%s'" % (globals.g_configfile, globals.g_configfile) 429 | execOScmd(restoreconfig) 430 | pullmsg = "" 431 | for l in newpull: 432 | pullmsg += "%s\n" % l 433 | msg = "Tried to fix git pull issue. Git pull now says:" 434 | write_log(msg, 2) 435 | write_console(msg) 436 | write_log(pullmsg,2) 437 | write_console(pullmsg) 438 | 439 | else: 440 | msg = "Output 'git pull':\n%s" % errormsg 441 | write_log(msg) 442 | 443 | 444 | def addressInNetwork(ip, net): 445 | try: 446 | ipaddr = int(''.join([ '%02x' % int(x) for x in ip.split('.') ]), 16) 447 | netstr, bits = net.split('/') 448 | netaddr = int(''.join([ '%02x' % int(x) for x in netstr.split('.') ]), 16) 449 | mask = (0xffffffff << (32 - int(bits))) & 0xffffffff 450 | return (ipaddr & mask) == (netaddr & mask) 451 | except: 452 | return False 453 | 454 | 455 | def is_whitelisted_ip(ip): 456 | # grab ips 457 | ipaddr = str(ip) 458 | whitelist = read_config("WHITELIST_IP") 459 | whitelist = whitelist.split(',') 460 | for site in whitelist: 461 | if site.find("/") < 0: 462 | if site.find(ipaddr) >= 0: 463 | return True 464 | else: 465 | continue 466 | if addressInNetwork(ipaddr, site): 467 | return True 468 | return False 469 | 470 | # validate that its an actual ip address versus something else stupid 471 | 472 | 473 | def is_valid_ipv4(ip): 474 | # if IP is cidr, strip net 475 | if "/" in ip: 476 | ipparts = ip.split("/") 477 | ip = ipparts[0] 478 | if not ip.startswith("#"): 479 | pattern = re.compile(r""" 480 | ^ 481 | (?: 482 | # Dotted variants: 483 | (?: 484 | # Decimal 1-255 (no leading 0's) 485 | [3-9]\d?|2(?:5[0-5]|[0-4]?\d)?|1\d{0,2} 486 | | 487 | 0x0*[0-9a-f]{1,2} # Hexadecimal 0x0 - 0xFF (possible leading 0's) 488 | | 489 | 0+[1-3]?[0-7]{0,2} # Octal 0 - 0377 (possible leading 0's) 490 | ) 491 | (?: # Repeat 0-3 times, separated by a dot 492 | \. 493 | (?: 494 | [3-9]\d?|2(?:5[0-5]|[0-4]?\d)?|1\d{0,2} 495 | | 496 | 0x0*[0-9a-f]{1,2} 497 | | 498 | 0+[1-3]?[0-7]{0,2} 499 | ) 500 | ){0,3} 501 | | 502 | 0x0*[0-9a-f]{1,8} # Hexadecimal notation, 0x0 - 0xffffffff 503 | | 504 | 0+[0-3]?[0-7]{0,10} # Octal notation, 0 - 037777777777 505 | | 506 | # Decimal notation, 1-4294967295: 507 | 429496729[0-5]|42949672[0-8]\d|4294967[01]\d\d|429496[0-6]\d{3}| 508 | 42949[0-5]\d{4}|4294[0-8]\d{5}|429[0-3]\d{6}|42[0-8]\d{7}| 509 | 4[01]\d{8}|[1-3]\d{0,9}|[4-9]\d{0,8} 510 | ) 511 | $ 512 | """, re.VERBOSE | re.IGNORECASE) 513 | return pattern.match(ip) is not None 514 | 515 | 516 | def check_banlist_path(): 517 | path = "" 518 | if is_posix(): 519 | #if os.path.isfile("banlist.txt"): 520 | # path = "banlist.txt" 521 | 522 | if os.path.isfile(globals.g_banlist): 523 | path = globals.g_banlist 524 | 525 | # if path is blank then try making the file 526 | if path == "": 527 | if os.path.isdir(globals.g_apppath): 528 | filewrite = open(globals.g_banlist, "w") 529 | filewrite.write( 530 | "#\n#\n#\n# Binary Defense Systems Artillery Threat Intelligence Feed and Banlist Feed\n# https://www.binarydefense.com\n#\n# Note that this is for public use only.\n# The ATIF feed may not be used for commercial resale or in products that are charging fees for such services.\n# Use of these feeds for commerical (having others pay for a service) use is strictly prohibited.\n#\n#\n#\n") 531 | filewrite.close() 532 | path = globals.g_banlist 533 | #changed path to be more consistant across windows versions 534 | if is_windows(): 535 | #program_files = os.environ["PROGRAMFILES(X86)"] 536 | if os.path.isfile(globals.g_banlist): 537 | # grab the path 538 | path = globals.g_banlist 539 | if path == "": 540 | if os.path.isdir(globals.g_apppath): 541 | path = globals.g_apppath 542 | filewrite = open( 543 | globals.g_banlist, "w") 544 | filewrite.write( 545 | "#\n#\n#\n# Binary Defense Systems Artillery Threat Intelligence Feed and Banlist Feed\n# https://www.binarydefense.com\n#\n# Note that this is for public use only.\n# The ATIF feed may not be used for commercial resale or in products that are charging fees for such services.\n# Use of these feeds for commerical (having others pay for a service) use is strictly prohibited.\n#\n#\n#\n") 546 | filewrite.close() 547 | return path 548 | 549 | # this will write out a log file for us to be sent eventually 550 | 551 | 552 | def prep_email(alert): 553 | if is_posix(): 554 | # check if folder program_junk exists 555 | if not os.path.isdir("%s/src/program_junk" % globals.g_apppath): 556 | os.mkdir("%s/src/program_junk" % globals.g_apppath) 557 | # write the file out to program_junk 558 | filewrite = open( 559 | "%s/src/program_junk/email_alerts.log" % globals.g_apppath, "a") 560 | if is_windows(): 561 | program_files = os.environ["PROGRAMFILES(X86)"] 562 | filewrite = open( 563 | "%s\\src\\program_junk\\email_alerts.log" % globals.g_apppath, "a") 564 | filewrite.write(alert) 565 | filewrite.close() 566 | 567 | 568 | def is_posix(): 569 | return os.name == "posix" 570 | 571 | 572 | def is_windows(): 573 | return os.name == "nt" 574 | 575 | #moved for issue #39 BinaryDefense. changed to only import on windows but it is not defined until above 576 | if is_windows(): 577 | from .events import HoneyPotEvent #check events.py for reasoning. 578 | 579 | # execute OS command and to wait until it's finished 580 | def execOScmd(cmd, logmsg=""): 581 | if logmsg != "": 582 | write_log("execOSCmd: %s" % (logmsg)) 583 | p = subprocess.Popen('%s' % cmd, 584 | stdout=subprocess.PIPE, 585 | stderr=subprocess.PIPE, 586 | shell=True) 587 | outputobj = iter(p.stdout.readline, b'') 588 | outputlines = [] 589 | for l in outputobj: 590 | thisline = "" 591 | try: 592 | thisline = l.decode() 593 | except: 594 | try: 595 | thisline = l.decode('utf8') 596 | except: 597 | thisline = "" 598 | #print(thisline) 599 | outputlines.append(thisline.replace('\\n','').replace("'","")) 600 | return outputlines 601 | 602 | 603 | # execute OS commands Asynchronously 604 | # this one takes an array 605 | # first element is application, arguments are in additional array elements 606 | def execOScmdAsync(cmdarray): 607 | p = subprocess.Popen(cmdarray) 608 | #p.terminate() 609 | return 610 | 611 | def create_empty_file(filepath): 612 | filewrite = open(filepath, "w") 613 | filewrite.write("") 614 | filewrite.close() 615 | 616 | 617 | def write_banlist_banner(filepath): 618 | filewrite = open(filepath, "w") 619 | banner = """# 620 | # 621 | # 622 | # Binary Defense Systems Artillery Threat Intelligence Feed and Banlist Feed 623 | # https://www.binarydefense.com 624 | # 625 | # Note that this is for public use only. 626 | # The ATIF feed may not be used for commercial resale or in products that are charging fees for such services. 627 | # Use of these feeds for commerical (having others pay for a service) use is strictly prohibited. 628 | # 629 | # 630 | # 631 | """ 632 | filewrite.write(banner) 633 | filewrite.close() 634 | 635 | 636 | 637 | def create_iptables_subset(): 638 | if is_posix(): 639 | ban_check = read_config("HONEYPOT_BAN").lower() 640 | if ban_check == "on": 641 | # remove previous entry if it already exists 642 | execOScmd("iptables -D INPUT -j ARTILLERY", "Deleting ARTILLERY IPTables Chain") 643 | # create new chain 644 | write_log("Flushing iptables chain, creating a new one") 645 | execOScmd("iptables -N ARTILLERY -w 3") 646 | execOScmd("iptables -F ARTILLERY -w 3") 647 | execOScmd("iptables -I INPUT -j ARTILLERY -w 3") 648 | 649 | bannedips = [] 650 | 651 | if not os.path.isfile(globals.g_banlist): 652 | create_empty_file(globals.g_banlist) 653 | write_banlist_banner(globals.g_banlist) 654 | 655 | banfile = open(globals.g_banlist, "r").readlines() 656 | write_log("Read %d lines in '%s'" % (len(banfile), globals.g_banlist)) 657 | 658 | for ip in banfile: 659 | if not ip in bannedips: 660 | bannedips.append(ip) 661 | 662 | if read_config("LOCAL_BANLIST").lower() == "on": 663 | if not os.path.isfile(globals.g_localbanlist): 664 | create_empty_file(globals.g_localbanlist) 665 | write_banlist_banner(globals.g_localbanlist) 666 | localbanfile = open(globals.g_localbanlist,"r").readlines() 667 | write_log("Read %d lines in '%s'" % (len(localbanfile), globals.g_localbanlist)) 668 | for ip in localbanfile: 669 | if not ip in bannedips: 670 | bannedips.append(ip) 671 | 672 | # if we are banning 673 | banlist = [] 674 | if read_config("HONEYPOT_BAN").lower() == "on": 675 | # iterate through lines from ban file(s) and ban them if not already 676 | # banned 677 | for ip in bannedips: 678 | if not ip.startswith("#") and not ip.replace(" ","") == "": 679 | ip = ip.strip() 680 | if ip != "" and not ":" in ip: 681 | test_ip = ip 682 | if "/" in test_ip: 683 | test_ip = test_ip.split("/")[0] 684 | if not is_whitelisted_ip(test_ip): 685 | if is_posix(): 686 | if not ip.startswith("0."): 687 | if is_valid_ipv4(ip.strip()): 688 | if read_config("HONEYPOT_BAN_CLASSC").lower() == "on": 689 | if not ip.endswith("/24"): 690 | ip = convert_to_classc(ip) 691 | banlist.append(ip) 692 | if is_windows(): 693 | ban(ip) 694 | else: 695 | write_log("Not banning IP %s, whitelisted" % ip) 696 | if read_config("LOCAL_BANLIST").lower() == "on": 697 | localbanfile = open(globals.g_localbanlist,"r").readlines() 698 | 699 | if len(banlist) > 0: 700 | # convert banlist into unique list 701 | write_log("Filtering duplicate entries in banlist") 702 | set_banlist = set(banlist) 703 | unique_banlist = (list(set_banlist)) 704 | entries_at_once = 750 705 | total_nr = len(unique_banlist) 706 | write_log("Mass loading %d unique entries from banlist(s)" % total_nr) 707 | write_console(" Mass loading %d unique entries from banlist(s)" % total_nr) 708 | nr_of_lists = int(len(unique_banlist) / entries_at_once) + 1 709 | iplists = get_sublists(unique_banlist, nr_of_lists) 710 | listindex = 1 711 | logindex = 1 712 | logthreshold = 25 713 | if len(iplists) > 1000: 714 | logthreshold = 100 715 | total_added = 0 716 | for iplist in iplists: 717 | ips_to_block = ','.join(iplist) 718 | massloadcmd = "iptables -I ARTILLERY -s %s -j DROP -w 3" % ips_to_block 719 | subprocess.Popen(massloadcmd, shell=True).wait() 720 | iptables_logprefix = read_config("HONEYPOT_BAN_LOG_PREFIX") 721 | if iptables_logprefix != "": 722 | massloadcmd = "iptables -I ARTILLERY -s %s -j LOG --log-prefix \"%s\" -w 3" % (ips_to_block, iptables_logprefix) 723 | subprocess.Popen(massloadcmd, shell=True).wait() 724 | total_added += len(iplist) 725 | write_log("%d/%d - Added %d/%d IP entries to iptables chain." % (listindex, len(iplists), total_added, total_nr)) 726 | if logindex >= logthreshold: 727 | write_console(" %d/%d : Update: Added %d/%d entries to iptables chain" % (listindex, len(iplists), total_added, total_nr)) 728 | logindex = 0 729 | listindex +=1 730 | logindex += 1 731 | write_console(" %d/%d : Done: Added %d/%d entries to iptables chain, thank you for waiting." % (listindex-1, len(iplists), total_added, total_nr)) 732 | 733 | 734 | def get_sublists(original_list, number_of_sub_list_wanted): 735 | sublists = list() 736 | for sub_list_count in range(number_of_sub_list_wanted): 737 | sublists.append(original_list[sub_list_count::number_of_sub_list_wanted]) 738 | return sublists 739 | 740 | 741 | 742 | def is_already_banned(ip): 743 | ban_check = read_config("HONEYPOT_BAN").lower() 744 | if ban_check == "on": 745 | 746 | proc = subprocess.Popen("iptables -L ARTILLERY -n --line-numbers", 747 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 748 | iptablesbanlist = proc.stdout.readlines() 749 | ban_classc = read_config("HONEYPOT_BAN_CLASSC").lower() 750 | if ban_classc == "on": 751 | ip = convert_to_classc(ip) 752 | if ip in iptablesbanlist: 753 | return True 754 | else: 755 | return False 756 | else: 757 | return False 758 | 759 | # valid if IP address is legit 760 | 761 | 762 | def is_valid_ip(ip): 763 | return is_valid_ipv4(ip) 764 | 765 | # convert a binary string into an IP address 766 | 767 | 768 | def bin2ip(b): 769 | ip = "" 770 | for i in range(0, len(b), 8): 771 | ip += str(int(b[i:i + 8], 2)) + "." 772 | return ip[:-1] 773 | 774 | # convert an IP address from its dotted-quad format to its 32 binary digit 775 | # representation 776 | 777 | 778 | def ip2bin(ip): 779 | b = "" 780 | inQuads = ip.split(".") 781 | outQuads = 4 782 | for q in inQuads: 783 | if q != "": 784 | b += dec2bin(int(q), 8) 785 | outQuads -= 1 786 | while outQuads > 0: 787 | b += "00000000" 788 | outQuads -= 1 789 | return b 790 | 791 | # convert a decimal number to binary representation 792 | # if d is specified, left-pad the binary number with 0s to that length 793 | 794 | 795 | def dec2bin(n, d=None): 796 | s = "" 797 | while n > 0: 798 | if n & 1: 799 | s = "1" + s 800 | else: 801 | s = "0" + s 802 | n >>= 1 803 | 804 | if d is not None: 805 | while len(s) < d: 806 | s = "0" + s 807 | if s == "": 808 | s = "0" 809 | return s 810 | 811 | # print a list of IP addresses based on the CIDR block specified 812 | 813 | 814 | def printCIDR(attacker_ip): 815 | trigger = 0 816 | whitelist = read_config("WHITELIST_IP") 817 | whitelist = whitelist.split(",") 818 | for c in whitelist: 819 | match = re.search("/", c) 820 | if match: 821 | parts = c.split("/") 822 | baseIP = ip2bin(parts[0]) 823 | subnet = int(parts[1]) 824 | # Python string-slicing weirdness: 825 | # if a subnet of 32 was specified simply print the single IP 826 | if subnet == 32: 827 | ipaddr = bin2ip(baseIP) 828 | # for any other size subnet, print a list of IP addresses by concatenating 829 | # the prefix with each of the suffixes in the subnet 830 | else: 831 | ipPrefix = baseIP[:-(32 - subnet)] 832 | for i in range(2**(32 - subnet)): 833 | ipaddr = bin2ip(ipPrefix + dec2bin(i, (32 - subnet))) 834 | ip_check = is_valid_ip(ipaddr) 835 | # if the ip isnt messed up then do this 836 | if ip_check != False: 837 | # compare c (whitelisted IP) to subnet IP address 838 | # whitelist 839 | if c == ipaddr: 840 | # if we equal each other then trigger that we are 841 | # whitelisted 842 | trigger = 1 843 | 844 | # return the trigger - 1 = whitelisted 0 = not found in whitelist 845 | return trigger 846 | 847 | 848 | def threat_server(): 849 | public_http = read_config("THREAT_LOCATION") 850 | if os.path.isdir(public_http): 851 | banfiles = read_config("THREAT_FILE") 852 | if banfiles == "": 853 | banfiles = globals.g_banfile 854 | banfileparts = banfiles.split(",") 855 | while 1: 856 | for banfile in banfileparts: 857 | thisfile = globals.g_apppath + "/" + banfile 858 | subprocess.Popen("cp '%s' '%s'" % (thisfile, public_http), shell=True).wait() 859 | #write_log("ThreatServer: Copy '%s' to '%s'" % (thisfile, public_http)) 860 | time.sleep(300) 861 | 862 | # send the message then if its local or remote 863 | 864 | 865 | def syslog(message, alerttype): 866 | type = read_config("SYSLOG_TYPE").lower() 867 | alertindicator = "" 868 | if alerttype == -1: 869 | alertindicator = "" 870 | elif alerttype == 0: 871 | alertindicator = "[INFO]" 872 | elif alerttype == 1: 873 | alertindicator = "[WARN]" 874 | elif alerttype == 2: 875 | alertindicator = "[ERROR]" 876 | 877 | # if we are sending remote syslog 878 | if type == "remote": 879 | 880 | import socket 881 | FACILITY = { 882 | 'kern': 0, 'user': 1, 'mail': 2, 'daemon': 3, 883 | 'auth': 4, 'syslog': 5, 'lpr': 6, 'news': 7, 884 | 'uucp': 8, 'cron': 9, 'authpriv': 10, 'ftp': 11, 885 | 'local0': 16, 'local1': 17, 'local2': 18, 'local3': 19, 886 | 'local4': 20, 'local5': 21, 'local6': 22, 'local7': 23, 887 | } 888 | 889 | LEVEL = { 890 | 'emerg': 0, 'alert': 1, 'crit': 2, 'err': 3, 891 | 'warning': 4, 'notice': 5, 'info': 6, 'debug': 7 892 | } 893 | 894 | def syslog_send( 895 | message, level=LEVEL['notice'], facility=FACILITY['daemon'], 896 | host='localhost', port=514): 897 | 898 | # Send syslog UDP packet to given host and port. 899 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 900 | data = '<%d>%s' % (level + facility * 8, message + "\n") 901 | sock.sendto(data, (host, port)) 902 | sock.close() 903 | 904 | # send the syslog message 905 | remote_syslog = read_config("SYSLOG_REMOTE_HOST") 906 | remote_port = int(read_config("SYSLOG_REMOTE_PORT")) 907 | syslogmsg = message 908 | if alertindicator != "": 909 | syslogmsg = "Artillery%s: %s" % (alertindicator, message) 910 | #syslogmsg = "%s %s Artillery: %s" % (grab_time(), alertindicator, message) 911 | syslog_send(syslogmsg, host=remote_syslog, port=remote_port) 912 | 913 | # if we are sending local syslog messages 914 | if type == "local": 915 | my_logger = logging.getLogger('Artillery') 916 | my_logger.setLevel(logging.DEBUG) 917 | handler = logging.handlers.SysLogHandler(address='/dev/log') 918 | my_logger.addHandler(handler) 919 | for line in message.splitlines(): 920 | if alertindicator != "": 921 | my_logger.critical("Artillery%s: %s\n" % (alertindicator, line)) 922 | #my_logger.critical("%s %s Artillery: %s\n" % (grab_time(), alertindicator, line)) 923 | else: 924 | my_logger.critical("%s\n" % line) 925 | 926 | # if we don't want to use local syslog and just write to file in 927 | # logs/alerts.log 928 | if type == "file": 929 | if not os.path.isdir("%s/logs" % globals.g_apppath): 930 | os.makedirs("%s/logs" % globals.g_apppath) 931 | 932 | if not os.path.isfile("%s/logs/alerts.log" % globals.g_apppath): 933 | filewrite = open("%s/logs/alerts.log" % globals.g_apppath, "w") 934 | filewrite.write("***** Artillery Alerts Log *****\n") 935 | filewrite.close() 936 | 937 | filewrite = open("%s/logs/alerts.log" % globals.g_apppath, "a") 938 | filewrite.write("Artillery%s: %s\n" % (alertindicator, message)) 939 | filewrite.close() 940 | 941 | def write_console(alert): 942 | if is_config_enabled("CONSOLE_LOGGING"): 943 | alertlines = alert.split("\n") 944 | for alertline in alertlines: 945 | print("%s: %s" % (grab_time(), alertline)) 946 | return 947 | 948 | 949 | def write_log(alert, alerttype=0): 950 | # alerttype -1 = overrule 951 | # alerttype 0 = normal/information [+] 952 | # alerttype 1 = warning [-] 953 | # alerttype 2 = error [!!] 954 | if is_posix(): 955 | syslog(alert, alerttype) 956 | #changed path to be more consistant across windows versions 957 | if is_windows(): 958 | program_files = os.environ["PROGRAMFILES(X86)"] 959 | if not os.path.isdir("%s\\logs" % globals.g_apppath): 960 | os.makedirs("%s\\logs" % globals.g_apppath) 961 | if not os.path.isfile("%s\\logs\\alerts.log" % globals.g_apppath): 962 | filewrite = open( 963 | "%s\\logs\\alerts.log" % globals.g_apppath, "w") 964 | filewrite.write("***** Artillery Alerts Log *****\n") 965 | filewrite.close() 966 | filewrite = open("%s\\logs\\alerts.log" % globals.g_apppath, "a") 967 | filewrite.write(alert + "\n") 968 | filewrite.close() 969 | 970 | 971 | def warn_the_good_guys(subject, alert): 972 | email_alerts = is_config_enabled("EMAIL_ALERTS") 973 | email_timer = is_config_enabled("EMAIL_TIMER") 974 | subject = gethostname() + " | " + subject 975 | if email_alerts and not email_timer: 976 | send_mail(subject, alert) 977 | 978 | if email_alerts and email_timer: 979 | prep_email(alert + "\n") 980 | 981 | write_log(alert,1) 982 | write_console(alert) 983 | # send the actual email 984 | 985 | 986 | def send_mail(subject, text): 987 | mail(read_config("ALERT_USER_EMAIL"), subject, text) 988 | 989 | # mail function preping to send 990 | def id_generator(size=6, chars=string.ascii_uppercase + string.digits): 991 | return ''.join(random.choice(chars) for _ in range(size)) 992 | 993 | def mail(to, subject, text): 994 | enabled = read_config("EMAIL_ALERTS") 995 | if enabled == "ON": 996 | try: 997 | user = read_config("SMTP_USERNAME") 998 | pwd = read_config("SMTP_PASSWORD") 999 | smtp_address = read_config("SMTP_ADDRESS") 1000 | # port we use, default is 25 1001 | smtp_port = int(read_config("SMTP_PORT")) 1002 | smtp_from = read_config("SMTP_FROM") 1003 | msg = MIMEMultipart() 1004 | msg['From'] = smtp_from 1005 | msg['To'] = to 1006 | msg['Date'] = formatdate(localtime=True) 1007 | msg['Message-Id'] = "<" + id_generator(20) + "." + smtp_from + ">" 1008 | msg['Subject'] = subject 1009 | msg.attach(MIMEText(text)) 1010 | # prep the smtp server 1011 | mailServer = smtplib.SMTP("%s" % (smtp_address), smtp_port) 1012 | #if user == '': 1013 | # write_console("[!] Email username is blank. please provide address in config file") 1014 | 1015 | # send ehlo 1016 | mailServer.ehlo() 1017 | if not user == "": 1018 | # tls support? 1019 | mailServer.starttls() 1020 | # some servers require ehlo again 1021 | mailServer.ehlo() 1022 | mailServer.login(user, pwd) 1023 | # send the mail 1024 | write_log("Sending email to %s: %s" % (to, subject)) 1025 | mailServer.sendmail(smtp_from, to, msg.as_string()) 1026 | mailServer.close() 1027 | 1028 | except Exception as err: 1029 | write_log("Error, Artillery was unable to log into the mail server %s:%d" % ( 1030 | smtp_address, smtp_port),2) 1031 | emsg = traceback.format_exc() 1032 | write_log("Error: " + str(err),2) 1033 | write_log(" %s" % emsg,2) 1034 | write_console("[!] Artillery was unable to send email via %s:%d" % (smtp_address, smtp_port)) 1035 | write_console("[!] Error: %s" % emsg) 1036 | pass 1037 | else: 1038 | write_console("[*] Email alerts are not enabled. please look @ %s to enable." % (globals.g_configfile)) 1039 | # kill running instances of artillery 1040 | 1041 | 1042 | def kill_artillery(): 1043 | try: 1044 | proc = subprocess.Popen( 1045 | "ps -A x | grep artiller[y]", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 1046 | pid, err = proc.communicate() 1047 | pid = [int(x.strip()) for line in pid.split('\n') 1048 | for x in line.split(" ") if int(x.isdigit())] 1049 | # try: 1050 | # pid = int(pid[0]) 1051 | # except: 1052 | # depends on OS on integer 1053 | # pid = int(pid[2]) 1054 | for i in pid: 1055 | write_log("Killing the old Artillery process...") 1056 | print("[!] %s: Killing Old Artillery Process...." % (grab_time())) 1057 | os.kill(i, signal.SIGKILL) 1058 | 1059 | except Exception as e: 1060 | print(e) 1061 | pass 1062 | 1063 | 1064 | def cleanup_artillery(): 1065 | ban_check = read_config("HONEYPOT_BAN").lower() 1066 | if ban_check == "on": 1067 | subprocess.Popen("iptables -D INPUT -j ARTILLERY", 1068 | stdout=subprocess.PIP, stderr=subprocess.PIPE, shell=True) 1069 | subprocess.Popen("iptables -X ARTILLERY", 1070 | stdout=subprocess.PIP, stderr=subprocess.PIPE, shell=True) 1071 | 1072 | # overwrite artillery banlist after certain time interval 1073 | 1074 | 1075 | def refresh_log(): 1076 | while 1: 1077 | interval = read_config("ARTILLERY_REFRESH=") 1078 | try: 1079 | interval = int(interval) 1080 | except: 1081 | # if the interval was not an integer, then just pass and don't do 1082 | # it again 1083 | break 1084 | # sleep until interval is up 1085 | time.sleep(interval) 1086 | # overwrite the log with nothing 1087 | create_empty_file(globals.g_banlist) 1088 | write_banlist_banner(globals.g_banlist) 1089 | 1090 | 1091 | # format the ip addresses and check to ensure they aren't duplicates 1092 | def format_ips(url): 1093 | ips = "" 1094 | for urls in url: 1095 | try: 1096 | write_log("Grabbing feed from '%s'" % (str(urls))) 1097 | urls = str(urls) 1098 | f = [] 1099 | if urls.startswith("http"): 1100 | f = urlopen(urls).readlines() 1101 | else: 1102 | # try reading it as a file 1103 | try: 1104 | f = open(urls,"r").readlines() 1105 | except: 1106 | write_log("Unable to read '%s'" % urls,2) 1107 | pass 1108 | write_log("Retrieved %d lines from %s" % (len(f), str(urls))) 1109 | for line in f: 1110 | line = line.rstrip() 1111 | # stupid conversion from py2 to py3 smh 1112 | try: 1113 | ips = ips + line + "\n" 1114 | except: 1115 | line = line.decode("ascii") 1116 | ips = ips + line + "\n" 1117 | 1118 | except Exception as err: 1119 | if err == '404': 1120 | # Error 404, page not found! 1121 | write_log( 1122 | "HTTPError: Error 404, URL {} not found.".format(urls)) 1123 | else: 1124 | if is_windows(): 1125 | #had to convert all err to string on windows 1126 | #write_log() complaind and spit out a bunch 1127 | #of exceptions about the format specifiers 1128 | msg = format(err) 1129 | msg_to_string = "[!] Received URL Error trying to download feed from " + urls," Reason: " + msg 1130 | final_msg = str(msg_to_string) 1131 | write_log(final_msg) 1132 | if is_posix(): 1133 | write_log( 1134 | "Received URL Error trying to download feed from '%s', Reason: %s" (urls, format(err)),1) 1135 | 1136 | try: 1137 | if is_windows(): 1138 | #added this for the banlist windows 7 successfully pulls banlist with python 2.7. 1139 | #windows 8/10 with python 3.6 fail with 403 Forbidden error. has to do with format_ips 1140 | #function above. python 3.6 urlopen sends the wrong headers 1141 | fileopen = open(globals.g_banlist, "r").read() 1142 | # write the file 1143 | filewrite = open(globals.g_banlist, "a") 1144 | if is_posix(): 1145 | fileopen = open(globals.g_banlist, "r").read() 1146 | # write the file 1147 | filewrite = open(globals.g_banlist, "a") 1148 | # iterate through 1149 | uniquenewentries = 0 1150 | for line in ips.split("\n"): 1151 | line = line.rstrip() 1152 | # we are using OTX reputation here 1153 | if "ALL:" in line: 1154 | try: 1155 | line = line.split(" ")[1] 1156 | except: 1157 | pass 1158 | if not "#" in line: 1159 | if not "//" in line: 1160 | # if we don't have the IP yet 1161 | if read_config("HONEYPOT_BAN_CLASSC").lower() == "on": 1162 | line = convert_to_classc(line) 1163 | if not line in fileopen: 1164 | # make sure valid ipv4 1165 | if not line.startswith("0."): 1166 | if is_valid_ipv4(line.strip()): 1167 | filewrite.write(line + "\n") 1168 | uniquenewentries += 1 1169 | # close the file 1170 | filewrite.close() 1171 | write_log("Added %d new unique entries to banlist" % (uniquenewentries)) 1172 | except Exception as err: 1173 | print("Error identified as :" + str(err) + " with line: " + str(line)) 1174 | pass 1175 | 1176 | 1177 | # update threat intelligence feed with other sources - special thanks for 1178 | # the feed list from here: 1179 | # http://www.deepimpact.io/blog/splunkandfreeopen-sourcethreatintelligencefeeds 1180 | def pull_source_feeds(): 1181 | while 1: 1182 | # pull source feeds 1183 | url = [] 1184 | counter = 0 1185 | # if we are using source feeds 1186 | if read_config("SOURCE_FEEDS").lower() == "on": 1187 | if read_config("THREAT_INTELLIGENCE_FEED").lower() == "on": 1188 | url = [ 1189 | 'http://rules.emergingthreats.net/blockrules/compromised-ips.txt', 'https://zeustracker.abuse.ch/blocklist.php?download=badips', 1190 | 'https://palevotracker.abuse.ch/blocklists.php?download=ipblocklist', 'http://malc0de.com/bl/IP_Blacklist.txt', 'https://reputation.alienvault.com/reputation.unix'] 1191 | counter = 1 1192 | 1193 | # if we are using threat intelligence feeds 1194 | if read_config("THREAT_INTELLIGENCE_FEED").lower() == "on": 1195 | threat_feed = read_config("THREAT_FEED") 1196 | if threat_feed != "": 1197 | threat_feed = threat_feed.split(",") 1198 | for threats in threat_feed: 1199 | url.append(threats) 1200 | 1201 | counter = 1 1202 | 1203 | # if we used source feeds or ATIF 1204 | if counter == 1: 1205 | format_ips(url) 1206 | sort_banlist() 1207 | time.sleep(7200) # sleep for 2 hours 1208 | 1209 | #re ordered this section to included windows 1210 | def sort_banlist(): 1211 | if is_windows(): 1212 | ips = open(globals.g_banlist, "r").readlines() 1213 | if is_posix(): 1214 | ips = open(globals.g_banlist, "r").readlines() 1215 | 1216 | 1217 | banner = """# 1218 | # 1219 | # 1220 | # Binary Defense Systems Artillery Threat Intelligence Feed and Banlist Feed 1221 | # https://www.binarydefense.com 1222 | # 1223 | # Note that this is for public use only. 1224 | # The ATIF feed may not be used for commercial resale or in products that are charging fees for such services. 1225 | # Use of these feeds for commerical (having others pay for a service) use is strictly prohibited. 1226 | # 1227 | # 1228 | # 1229 | """ 1230 | ip_filter = "" 1231 | for ip in ips: 1232 | if is_valid_ipv4(ip.strip()): 1233 | if not ip.startswith("0.") and not ip == "": 1234 | ip_filter = ip_filter + ip.rstrip() + "\n" 1235 | ips = ip_filter 1236 | ips = ips.replace(banner, "") 1237 | ips = ips.replace(" ", "") 1238 | ips = ips.split("\n") 1239 | ips = [_f for _f in ips if _f] 1240 | ips = list(filter(str.strip, ips)) 1241 | tempips = [socket.inet_aton(ip.split("/")[0]) for ip in ips] 1242 | tempips.sort() 1243 | tempips.reverse() 1244 | if is_windows(): 1245 | filewrite = open(globals.g_banlist, "w") 1246 | if is_posix(): 1247 | filewrite = open(globals.g_banlist, "w") 1248 | ips2 = [socket.inet_ntoa(ip) for ip in tempips] 1249 | ips_parsed = "" 1250 | for ips in ips2: 1251 | if not ips.startswith("0."): 1252 | if ips.endswith(".0"): 1253 | ips += "/24" 1254 | ips_parsed = ips + "\n" + ips_parsed 1255 | filewrite.write(banner + "\n" + ips_parsed) 1256 | filewrite.close() 1257 | #removed turns out the issue was windows carriage returns in the init script i had. 1258 | #note to self never open linux service files on windows.doh 1259 | # this was just a place holder artillery.py code 1260 | #def writePidFile(): 1261 | # pid = str(os.getpid()) 1262 | # f = open('/var/run/artillery.pid', 'w') 1263 | # f.write(pid) 1264 | # f.close() 1265 | -------------------------------------------------------------------------------- /src/email_handler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # 4 | # Handles emails from the config. Delivers after X amount of time 5 | # 6 | # 7 | import shutil 8 | import time 9 | # needed for backwards compatibility of python2 vs 3 - need to convert to threading eventually 10 | try: import thread 11 | except ImportError: import _thread as thread 12 | from src.core import * 13 | 14 | from . import globals 15 | 16 | # check how long to send the email 17 | mail_time = read_config("EMAIL_FREQUENCY") 18 | 19 | # this is what handles the loop for checking email alert frequencies 20 | import socket 21 | 22 | def check_alert(): 23 | # loop forever 24 | while 1: 25 | mail_log_file = "" 26 | mail_old_log_file = "" 27 | if is_posix(): 28 | mail_log_file = "%s/src/program_junk/email_alerts.log" % globals.g_apppath 29 | mail_old_log_file = "%s/src/program_junk/email_alerts.old" % globals.g_apppath 30 | if is_windows(): 31 | mail_log_file = "%s\\src\\program_junk\\email_alerts.log" % globals.g_apppath 32 | mail_old_log_file = "%s\\src\\program_junk\\email_alerts.old" % globals.g_apppath 33 | # if the file is there, read it in then trigger email 34 | if os.path.isfile(mail_log_file): 35 | # read open the file to be sent 36 | fileopen = open(mail_log_file, "r") 37 | data = fileopen.read() 38 | if is_config_enabled("EMAIL_ALERTS"): 39 | send_mail("[!] " + socket.gethostname() + " | Artillery has new notifications for you. [!]", 40 | data) 41 | # save this for later just in case we need it 42 | shutil.move(mail_log_file, mail_old_log_file) 43 | time.sleep(int(mail_time)) 44 | 45 | # start a threat for checking email frequency 46 | thread.start_new_thread(check_alert, ()) 47 | -------------------------------------------------------------------------------- /src/events.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # 4 | # Events.py 5 | # 6 | # defined some events for use with Artillery 7 | # just basic events for now no dll. i dont know c. will try?. 8 | # I am aware of win32service.pyd and i do not want to blow up logs for 9 | # now. logs to application log on windows. would like to create custom events? 10 | # for now there are only 2 the names are self explanatory 11 | # Tweaked import for issue #39 from BinaryDefense 12 | from .core import is_windows, is_posix 13 | #dont know if needed but no issues for now 14 | if is_posix(): 15 | sys.exit(1) 16 | #changed to only import on windows 17 | if is_windows(): 18 | import win32api, win32con, win32evtlog, win32evtlogutil, win32security 19 | def HoneyPotEvent(): 20 | #lets try and write an event log 21 | process = win32api.GetCurrentProcess() 22 | token = win32security.OpenProcessToken(process, win32con.TOKEN_READ) 23 | my_sid = win32security.GetTokenInformation(token, win32security.TokenUser)[0] 24 | AppName = "Artillery" 25 | eventID = 1 26 | category =5 27 | myType = win32evtlog.EVENTLOG_WARNING_TYPE 28 | descr =["Artillery Detected access to a honeypot port", "The offending ip has been blocked and added to the local routing table",] 29 | data = "Application\0Data".encode("ascii") 30 | win32evtlogutil.ReportEvent(AppName, eventID, eventCategory=category, eventType=myType, strings=descr, data=data, sid=my_sid) 31 | 32 | 33 | def ArtilleryStartEvent(): 34 | process = win32api.GetCurrentProcess() 35 | token = win32security.OpenProcessToken(process, win32con.TOKEN_READ) 36 | my_sid = win32security.GetTokenInformation(token, win32security.TokenUser)[0] 37 | AppName = "Artillery" 38 | eventID = 1 39 | category =5 40 | myType = win32evtlog.EVENTLOG_INFORMATION_TYPE 41 | descr =["Artillery has started and begun monitoring the selected ports ",] 42 | data = "Application\0Data".encode("ascii") 43 | win32evtlogutil.ReportEvent(AppName, eventID, eventCategory=category, eventType=myType, strings=descr, data=data, sid=my_sid) 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/ftp_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ############################# 4 | # 5 | # monitor ftp and ban 6 | # added by e @ Nov 5th 7 | ############################# 8 | 9 | import time 10 | import re 11 | try: import thread 12 | except ImportError: import _thread as thread 13 | from src.core import * 14 | 15 | send_email = read_config("ALERT_USER_EMAIL") 16 | 17 | # how frequently we need to monitor 18 | monitor_time = read_config("MONITOR_FREQUENCY") 19 | monitor_time = int(monitor_time) 20 | ftp_attempts = read_config("FTP_BRUTE_ATTEMPTS") 21 | # check for whitelist 22 | 23 | from . import globals 24 | 25 | def ftp_monitor(monitor_time): 26 | while 1: 27 | # for debian base 28 | if os.path.isfile("/var/log/vsftpd.log"): 29 | fileopen1 = file("/var/log/auth.log", "r") 30 | else: 31 | print("Has not found configuration file for ftp. Ftp monitor now stops.") 32 | break 33 | 34 | if not os.path.isfile(globals.g_banlist): 35 | # create a blank file 36 | filewrite = file(globals.g_banlist, "w") 37 | filewrite.write("") 38 | filewrite.close() 39 | 40 | try: 41 | # base ftp counter to see how many attempts we've had 42 | ftp_counter = 0 43 | counter = 0 44 | for line in fileopen1: 45 | counter = 0 46 | fileopen2 = file(globals.g_banlist, "r") 47 | line = line.rstrip() 48 | # search for bad ftp 49 | match = re.search("CONNECT: Client", line) 50 | if match: 51 | ftp_counter = ftp_counter + 1 52 | # split based on spaces 53 | line = line.split('"') 54 | # pull ipaddress 55 | ipaddress = line[-2] 56 | ip_check = is_valid_ipv4(ipaddress) 57 | if ip_check != False: 58 | 59 | # if its not a duplicate then ban that ass 60 | if ftp_counter >= int(ftp_attempts): 61 | banlist = fileopen2.read() 62 | match = re.search(ipaddress, banlist) 63 | if match: 64 | counter = 1 65 | # reset FTP counter 66 | ftp_counter = 0 67 | 68 | # if counter is equal to 0 then we know that we 69 | # need to ban 70 | if counter == 0: 71 | whitelist_match = whitelist(ipaddress) 72 | if whitelist_match == 0: 73 | 74 | # if we have email alerting on we can send 75 | # email messages 76 | email_alerts = read_config( 77 | "EMAIL_ALERTS").lower() 78 | # check email frequency 79 | email_frequency = read_config( 80 | "EMAIL_FREQUENCY").lower() 81 | 82 | if email_alerts == "on" and email_frequency == "off": 83 | mail(send_email, 84 | "[!] Artillery has banned an FTP brute force. [!]", 85 | "The following IP has been blocked: " + ipaddress) 86 | 87 | # check frequency is allowed 88 | if email_alerts == "on" and email_frequency == "on": 89 | prep_email( 90 | "Artillery has blocked (blacklisted) the following IP for FTP brute forcing violations: " + ipaddress + "\n") 91 | 92 | # write out to log 93 | write_log( 94 | "Artillery has blocked (blacklisted) the following IP for FTP brute forcing violations: " + ipaddress) 95 | 96 | # do the actual ban, this is pulled from 97 | # src.core 98 | ban(ipaddress) 99 | ftp_counter = 0 100 | 101 | # wait one to make sure everything is 102 | # caught up 103 | time.sleep(1) 104 | # sleep for defined time 105 | time.sleep(monitor_time) 106 | 107 | except Exception as e: 108 | print("[*] An error in ftp monitor occured. Printing it out here: " + str(e)) 109 | 110 | if is_posix(): 111 | # start thread 112 | thread.start_new_thread(ftp_monitor, (monitor_time,)) 113 | -------------------------------------------------------------------------------- /src/globals.py: -------------------------------------------------------------------------------- 1 | # Artillery - globals 2 | 3 | global g_apppath 4 | global g_appfile 5 | global g_configfile 6 | global g_banlist 7 | global g_localbanlist 8 | -------------------------------------------------------------------------------- /src/harden.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # eventual home for checking some base files for security configurations 4 | # 5 | import re 6 | import os 7 | from src.core import * 8 | 9 | # flag warnings, base is nothing 10 | warning = "" 11 | 12 | if is_posix(): 13 | # 14 | # check ssh config 15 | # 16 | if os.path.isfile("/etc/ssh/sshd_config"): 17 | fileopen = open("/etc/ssh/sshd_config", "r") 18 | data = fileopen.read() 19 | if is_config_enabled("ROOT_CHECK"): 20 | match = re.search("RootLogin yes", data) 21 | # if we permit root logins trigger alert 22 | if match: 23 | # trigger warning if match 24 | warning = warning + \ 25 | "[!] Issue identified: /etc/ssh/sshd_config allows RootLogin. An attacker can gain root access to the system if password is guessed. Recommendation: Change RootLogin yes to RootLogin no\n\r\n\r" 26 | match = re.search(r"Port 22\b", data) 27 | if match: 28 | if is_config_enabled("SSH_DEFAULT_PORT_CHECK"): 29 | # trigger warning if match 30 | warning = warning + "[!] Issue identified: /etc/ssh/sshd_config. SSH is running on the default port 22. An attacker commonly scans for these type of ports. Recommendation: Change the port to something high that doesn't get picked up by typical port scanners.\n\r\n\r" 31 | 32 | # add SSH detection for password auth 33 | match = re.search("PasswordAuthentication yes", data) 34 | # if password authentication is used 35 | if match: 36 | warning = warning + \ 37 | "[!] Issue identified: Password authentication enabled. An attacker may be able to brute force weak passwords.\n\r\n\r" 38 | match = re.search("Protocol 1|Protocol 2,1", data) 39 | # 40 | if match: 41 | # triggered 42 | warning = warning + \ 43 | "[!] Issue identified: SSH Protocol 1 enabled which is potentially vulnerable to MiTM attacks. https://www.kb.cert.org/vuls/id/684820\n\r\n\r" 44 | 45 | # 46 | # check ftp config 47 | # 48 | if os.path.isfile("/etc/vsftpd.conf"): 49 | fileopen = open("/etc/vsftpd.conf", "r") 50 | data = fileopen.read() 51 | match = re.search("anonymous_enable=YES", data) 52 | if match: 53 | # trigger warning if match 54 | warning = warning + \ 55 | "[!] Issue identified: /etc/vsftpd.conf allows Anonymous login. An attacker can gain a foothold to the system with absolutel zero effort. Recommendation: Change anonymous_enable yes to anonymous_enable no\n\r\n\r" 56 | 57 | # 58 | # check /var/www permissions 59 | # 60 | if os.path.isdir("/var/www/"): 61 | for path, subdirs, files in os.walk("/var/www/"): 62 | for name in files: 63 | trigger_warning = 0 64 | filename = os.path.join(path, name) 65 | if os.path.isfile(filename): 66 | # check permission 67 | check_perm = os.stat(filename) 68 | check_perm = str(check_perm) 69 | match = re.search("st_uid=0", check_perm) 70 | if not match: 71 | trigger_warning = 1 72 | match = re.search("st_gid=0", check_perm) 73 | if not match: 74 | trigger_warning = 1 75 | # if we trigger on vuln 76 | if trigger_warning == 1: 77 | warning = warning + \ 78 | "Issue identified: %s permissions are not set to root. If an attacker compromises the system and is running under the Apache user account, could view these files. Recommendation: Change the permission of %s to root:root. Command: chown root:root %s\n\n" % ( 79 | filename, filename, filename) 80 | 81 | # 82 | # if we had warnings then trigger alert 83 | # 84 | if len(warning) > 1: 85 | subject = "[!] Insecure configuration detected on filesystem: " 86 | warn_the_good_guys(subject, subject + warning) 87 | -------------------------------------------------------------------------------- /src/honeypot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # this is the honeypot stuff 4 | # 5 | # 6 | # needed for backwards compatibility of python2 vs 3 - need to convert to threading eventually 7 | try: import thread 8 | except ImportError: import _thread as thread 9 | import socket 10 | import sys 11 | import re 12 | import subprocess 13 | import time 14 | try: import SocketServer 15 | except ImportError: import socketserver as SocketServer 16 | import os 17 | import random 18 | import datetime 19 | from src.core import * 20 | import traceback 21 | 22 | # port ranges to spawn pulled from config 23 | tcpports = read_config("TCPPORTS") 24 | udpports = read_config("UDPPORTS") 25 | # check to see what IP we need to bind to 26 | bind_interface = read_config("BIND_INTERFACE") 27 | honeypot_ban = is_config_enabled("HONEYPOT_BAN") 28 | honeypot_autoaccept = is_config_enabled("HONEYPOT_AUTOACCEPT") 29 | log_message_ban = read_config("LOG_MESSAGE_BAN") 30 | log_message_alert = read_config("LOG_MESSAGE_ALERT") 31 | 32 | # main socket server listener for responses 33 | 34 | 35 | class SocketListener((SocketServer.BaseRequestHandler)): 36 | 37 | def handle(self): 38 | pass 39 | 40 | def setup(self): 41 | # hehe send random length garbage to the attacker 42 | length = random.randint(5, 30000) 43 | 44 | # fake_string = random number between 5 and 30,000 then os.urandom the 45 | # command back 46 | fake_string = os.urandom(int(length)) 47 | 48 | # try the actual sending and banning 49 | try: 50 | ip = self.client_address[0] 51 | try: 52 | write_log("Honeypot detected incoming connection from %s to port %s" % (ip, self.server.server_address[1])) 53 | self.request.send(fake_string) 54 | except Exception as e: 55 | write_console("Unable to send data to %s:%s" % (ip, str(self.server.server_address[1]))) 56 | pass 57 | if is_valid_ipv4(ip): 58 | # ban the mofos 59 | if not is_whitelisted_ip(ip): 60 | now = str(datetime.datetime.today()) 61 | port = str(self.server.server_address[1]) 62 | subject = "%s [!] Artillery has detected an attack from the IP Address: %s" % ( 63 | now, ip) 64 | alert = "" 65 | message = log_message_alert 66 | if honeypot_ban: 67 | message = log_message_ban 68 | message = message.replace("%time%", now) 69 | message = message.replace("%ip%", ip) 70 | message = message.replace("%port%", str(port)) 71 | alert = message 72 | if "%" in message: 73 | nrvars = message.count("%") 74 | if nrvars == 1: 75 | alert = message % (now) 76 | elif nrvars == 2: 77 | alert = message % (now, ip) 78 | elif nrvars == 3: 79 | alert = message % (now, ip, str(port)) 80 | 81 | warn_the_good_guys(subject, alert) 82 | 83 | # close the socket 84 | try: 85 | self.request.close() 86 | except: 87 | pass 88 | 89 | # if it isn't whitelisted and we are set to ban 90 | ban(ip) 91 | else: 92 | write_log("Ignore connection from %s to port %s, whitelisted" % (ip, self.server.server_address[1])) 93 | 94 | except Exception as e: 95 | emsg = traceback.format_exc() 96 | print("[!] Error detected. Printing: " + str(e)) 97 | print(emsg) 98 | write_log(emsg,2) 99 | print("") 100 | pass 101 | 102 | 103 | def open_sesame(porttype, port): 104 | if honeypot_autoaccept: 105 | if is_posix(): 106 | cmd = "iptables -D ARTILLERY -p %s --dport %s -j ACCEPT -w 3" % (porttype, port) 107 | execOScmd(cmd) 108 | cmd = "iptables -A ARTILLERY -p %s --dport %s -j ACCEPT -w 3" % (porttype, port) 109 | execOScmd(cmd) 110 | write_log("Created iptables rule to accept incoming connection to %s %s" % (porttype, port)) 111 | if is_windows(): 112 | pass 113 | 114 | # here we define a basic server 115 | 116 | def listentcp_server(tcpport, bind_interface): 117 | if not tcpport == "": 118 | port = int(tcpport) 119 | bindsuccess = False 120 | errormsg = "" 121 | nrattempts = 0 122 | while nrattempts < 5 and not bindsuccess: 123 | nrattempts += 1 124 | bindsuccess = True 125 | try: 126 | nrattempts += 1 127 | if bind_interface == "": 128 | server = SocketServer.ThreadingTCPServer( 129 | ('', port), SocketListener) 130 | else: 131 | server = SocketServer.ThreadingTCPServer( 132 | ('%s' % bind_interface, port), SocketListener) 133 | open_sesame("tcp", port) 134 | server.serve_forever() 135 | # if theres already something listening on this port 136 | except Exception as err: 137 | errormsg += socket.gethostname() + " | %s | Artillery error - unable to bind to TCP port %s\n" % (grab_time(), port) 138 | errormsg += str(err) 139 | errormsg += "\n" 140 | bindsuccess = False 141 | time.sleep(2) 142 | continue 143 | 144 | if not bindsuccess: 145 | binderror = "Artillery was unable to bind to TCP port %s. This could be due to an active port in use.\n" % (port) 146 | subject = socket.gethostname() + " | Artillery error - unable to bind to TCP port %s" % port 147 | binderror += errormsg 148 | write_log(binderror, 2) 149 | send_mail(subject, binderror) 150 | 151 | 152 | def listenudp_server(udpport, bind_interface): 153 | if not udpport == "": 154 | port = int(udpport) 155 | bindsuccess = False 156 | errormsg = "" 157 | nrattempts = 0 158 | while nrattempts < 5 and not bindsuccess: 159 | nrattempts += 1 160 | bindsuccess = True 161 | try: 162 | if bind_interface == "": 163 | server = SocketServer.ThreadingUDPServer( 164 | ('', port), SocketListener) 165 | else: 166 | server = SocketServer.ThreadingUDPServer( 167 | ('%s' % bind_interface, port), SocketListener) 168 | open_sesame("udp", port) 169 | server.serve_forever() 170 | # if theres already something listening on this port 171 | except Exception as err: 172 | errormsg += socket.gethostname() + " | %s | Artillery error - unable to bind to UDP port %s\n" % (grab_time(), port) 173 | errormsg += str(err) 174 | errormsg += "\n" 175 | bindsuccess = False 176 | time.sleep(2) 177 | continue 178 | 179 | if not bindsuccess: 180 | binderror = '' 181 | bind_error = "Artillery was unable to bind to UDP port %s. This could be due to an active port in use.\n" % (port) 182 | subject = socket.gethostname() + " | Artillery error - unable to bind to UDP port %s" % port 183 | binderror += errormsg 184 | write_log(binderror, 2) 185 | send_mail(subject, binderror) 186 | 187 | 188 | def main(tcpports, udpports, bind_interface): 189 | 190 | # split into tuple 191 | tports = tcpports.split(",") 192 | for tport in tports: 193 | tport = tport.replace(" ","") 194 | if tport != "": 195 | write_log("Set up listener for tcp port %s" % tport) 196 | thread.start_new_thread(listentcp_server, (tport, bind_interface,)) 197 | 198 | # split into tuple 199 | uports = udpports.split(",") 200 | for uport in uports: 201 | uport = uport.replace(" ","") 202 | if uport != "": 203 | write_log("Set up listener for udp port %s" % uport) 204 | thread.start_new_thread(listenudp_server, (uport, bind_interface,)) 205 | 206 | # launch the application 207 | main(tcpports, udpports, bind_interface) 208 | -------------------------------------------------------------------------------- /src/monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # This one monitors file system integrity 4 | # 5 | import os 6 | import re 7 | import hashlib 8 | import time 9 | import subprocess 10 | # needed for backwards compatibility of python2 vs 3 - need to convert to threading eventually 11 | try: import thread 12 | except ImportError: import _thread as thread 13 | import datetime 14 | import shutil 15 | from src.core import * 16 | 17 | 18 | def monitor_system(time_wait): 19 | # total_compare is a tally of all sha512 hashes 20 | total_compare = "" 21 | # what files we need to monitor 22 | check_folders = read_config("MONITOR_FOLDERS") 23 | # split lines 24 | check_folders = check_folders.replace('"', "") 25 | check_folders = check_folders.replace("MONITOR_FOLDERS=", "") 26 | check_folders = check_folders.rstrip() 27 | check_folders = check_folders.split(",") 28 | # cycle through tuple 29 | for directory in check_folders: 30 | time.sleep(0.1) 31 | # we need to check to see if the directory is there first, you never 32 | # know 33 | if os.path.isdir(directory): 34 | # check to see if theres an include 35 | exclude_check = read_config("EXCLUDE") 36 | match = re.search(exclude_check, directory) 37 | # if we hit a match then we need to exclude 38 | if not directory in exclude_check: 39 | # this will pull a list of files and associated folders 40 | for path, subdirs, files in os.walk(directory): 41 | for name in files: 42 | filename = os.path.join(path, name) 43 | # check for sub directory exclude paths 44 | if not filename in exclude_check: 45 | # some system protected files may not show up, so 46 | # we check here 47 | if os.path.isfile(filename): 48 | try: 49 | fileopen = open(filename, "rb") 50 | data = fileopen.read() 51 | 52 | except: 53 | pass 54 | hash = hashlib.sha512() 55 | try: 56 | hash.update(data) 57 | except: 58 | pass 59 | # here we split into : with filename : 60 | # hexdigest 61 | compare = filename + ":" + hash.hexdigest() + "\n" 62 | # this will be all of our hashes 63 | total_compare = total_compare + compare 64 | 65 | # write out temp database 66 | temp_database_file = open("/var/artillery/database/temp.database", "w") 67 | temp_database_file.write(total_compare) 68 | temp_database_file.close() 69 | 70 | # once we are done write out the database, if this is the first time, 71 | # create a database then compare 72 | if not os.path.isfile("/var/artillery/database/integrity.database"): 73 | # prep the integrity database to be written for first time 74 | database_file = open("/var/artillery/database/integrity.database", "w") 75 | database_file.write(total_compare) 76 | database_file.close() 77 | 78 | # hash the original database 79 | if os.path.isfile("/var/artillery/database/integrity.database"): 80 | database_file = open("/var/artillery/database/integrity.database", "r") 81 | try: database_content = database_file.read().encode('utf-8') 82 | except: database_content = database_file.read() 83 | if os.path.isfile("/var/artillery/database/temp.database"): 84 | temp_database_file = open( 85 | "/var/artillery/database/temp.database", "r") 86 | try: temp_hash = temp_database_file.read().encode('utf-8') 87 | except: temp_hash = temp_database_file.read() 88 | 89 | # hash the databases then compare 90 | database_hash = hashlib.sha512() 91 | database_hash.update(database_content) 92 | database_hash = database_hash.hexdigest() 93 | 94 | # this is the temp integrity database 95 | temp_database_hash = hashlib.sha512() 96 | temp_database_hash.update(temp_hash) 97 | temp_database_hash = temp_database_hash.hexdigest() 98 | # if we don't match then there was something that was changed 99 | if database_hash != temp_database_hash: 100 | # using diff for now, this will be rewritten properly at a 101 | # later time 102 | compare_files = subprocess.Popen( 103 | "diff /var/artillery/database/integrity.database /var/artillery/database/temp.database", shell=True, stdout=subprocess.PIPE) 104 | output_file = compare_files.communicate()[0] 105 | if output_file == "": 106 | # no changes 107 | pass 108 | 109 | else: 110 | subject = "[!] Artillery has detected a change. [!]" 111 | output_file = "********************************** The following changes were detected at %s **********************************\n" % ( 112 | str(datetime.datetime.now())) + str(output_file) + "\n********************************** End of changes. **********************************\n\n" 113 | warn_the_good_guys(subject, output_file) 114 | 115 | # put the new database as old 116 | if os.path.isfile("/var/artillery/database/temp.database"): 117 | shutil.move("/var/artillery/database/temp.database", 118 | "/var/artillery/database/integrity.database") 119 | 120 | 121 | def start_monitor(): 122 | # check if we want to monitor files 123 | if is_config_enabled("MONITOR"): 124 | # start the monitoring 125 | time_wait = read_config("MONITOR_FREQUENCY") 126 | 127 | # loop forever 128 | while 1: 129 | thread.start_new_thread(monitor_system, (time_wait,)) 130 | time_wait = int(time_wait) 131 | time.sleep(time_wait) 132 | 133 | # start the thread only if its running posix will rewrite this module to 134 | # use difflib and some others butfor now its reliant on linux 135 | if is_posix(): 136 | thread.start_new_thread(start_monitor, ()) 137 | -------------------------------------------------------------------------------- /src/pyuac.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- 3 | # vim: fileencoding=utf-8 tabstop=4 expandtab shiftwidth=4 4 | 5 | """User Access Control for Microsoft Windows Vista and higher. This is 6 | only for the Windows platform. 7 | 8 | This will relaunch either the current script - with all the same command 9 | line parameters - or else you can provide a different script/program to 10 | run. If the current user doesn't normally have admin rights, he'll be 11 | prompted for an admin password. Otherwise he just gets the UAC prompt. 12 | 13 | Note that the prompt may simply shows a generic python.exe with "Publisher: 14 | Unknown" if the python.exe is not signed. 15 | 16 | This is meant to be used something like this:: 17 | 18 | if not pyuac.isUserAdmin(): 19 | return pyuac.runAsAdmin() 20 | 21 | # otherwise carry on doing whatever... 22 | 23 | See L{runAsAdmin} for the main interface. 24 | 25 | """ 26 | 27 | import sys, os, traceback, types 28 | 29 | def isUserAdmin(): 30 | """@return: True if the current user is an 'Admin' whatever that 31 | means (root on Unix), otherwise False. 32 | 33 | Warning: The inner function fails unless you have Windows XP SP2 or 34 | higher. The failure causes a traceback to be printed and this 35 | function to return False. 36 | """ 37 | 38 | if os.name == 'nt': 39 | import ctypes 40 | # WARNING: requires Windows XP SP2 or higher! 41 | try: 42 | return ctypes.windll.shell32.IsUserAnAdmin() 43 | except: 44 | traceback.print_exc() 45 | print ("Admin check failed, assuming not an admin.") 46 | return False 47 | else: 48 | # Check for root on Posix 49 | return os.getuid() == 0 50 | 51 | def runAsAdmin(cmdLine=None, wait=True): 52 | """Attempt to relaunch the current script as an admin using the same 53 | command line parameters. Pass cmdLine in to override and set a new 54 | command. It must be a list of [command, arg1, arg2...] format. 55 | 56 | Set wait to False to avoid waiting for the sub-process to finish. You 57 | will not be able to fetch the exit code of the process if wait is 58 | False. 59 | 60 | Returns the sub-process return code, unless wait is False in which 61 | case it returns None. 62 | 63 | @WARNING: this function only works on Windows. 64 | """ 65 | 66 | if os.name != 'nt': 67 | #raise RuntimeError, ("This function is only implemented on Windows.") 68 | #print("This function is only implemented on Windows.") 69 | raise RuntimeError("This function is only implemented on Windows.") 70 | import win32api, win32con, win32event, win32process 71 | from win32com.shell.shell import ShellExecuteEx 72 | from win32com.shell import shellcon 73 | 74 | python_exe = sys.executable 75 | 76 | if cmdLine is None: 77 | cmdLine = [python_exe] + sys.argv 78 | elif type(cmdLine) not in (types.TupleType,types.ListType): 79 | raise ValueError("cmdLine is not a sequence.") 80 | cmd = '"%s"' % (cmdLine[0],) 81 | # XXX TODO: isn't there a function or something we can call to massage command line params? 82 | params = " ".join(['"%s"' % (x,) for x in cmdLine[1:]]) 83 | cmdDir = '' 84 | showCmd = win32con.SW_SHOWNORMAL 85 | lpVerb = 'runas' # causes UAC elevation prompt. 86 | 87 | # print "Running", cmd, params 88 | 89 | # ShellExecute() doesn't seem to allow us to fetch the PID or handle 90 | # of the process, so we can't get anything useful from it. Therefore 91 | # the more complex ShellExecuteEx() must be used. 92 | 93 | # procHandle = win32api.ShellExecute(0, lpVerb, cmd, params, cmdDir, showCmd) 94 | 95 | procInfo = ShellExecuteEx(nShow=showCmd, 96 | fMask=shellcon.SEE_MASK_NOCLOSEPROCESS, 97 | lpVerb=lpVerb, 98 | lpFile=cmd, 99 | lpParameters=params) 100 | 101 | if wait: 102 | procHandle = procInfo['hProcess'] 103 | obj = win32event.WaitForSingleObject(procHandle, win32event.INFINITE) 104 | rc = win32process.GetExitCodeProcess(procHandle) 105 | #print "Process handle %s returned code %s" % (procHandle, rc) 106 | else: 107 | rc = None 108 | 109 | return rc 110 | sys.exit() 111 | 112 | def test(): 113 | """A simple test function; check if we're admin, and if not relaunch 114 | the script as admin.""", 115 | rc = 0 116 | if not isUserAdmin(): 117 | print("You're not an admin."), os.getpid(), "params: ", sys.argv 118 | #rc = runAsAdmin(["c:\\Windows\\notepad.exe"]) 119 | rc = runAsAdmin() 120 | else: 121 | print("You are an admin!"), os.getpid(), "params: ", sys.argv 122 | rc = 0 123 | x = raw_input('Press Enter to exit.') 124 | return rc 125 | 126 | 127 | if __name__ == "__main__": 128 | res = test() 129 | sys.exit(res) 130 | -------------------------------------------------------------------------------- /src/ssh_monitor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # monitor ssh and ban 4 | # 5 | import time 6 | import re 7 | # needed for backwards compatibility of python2 vs 3 - need to convert to threading eventually 8 | try: import thread 9 | except ImportError: import _thread as thread 10 | from src.core import * 11 | 12 | from . import globals 13 | 14 | monitor_frequency = int(read_config("MONITOR_FREQUENCY")) 15 | ssh_attempts = read_config("SSH_BRUTE_ATTEMPTS") 16 | 17 | 18 | def ssh_monitor(monitor_frequency): 19 | counter = 0 20 | while 1: 21 | # for debian base 22 | if os.path.isfile("/var/log/auth.log"): 23 | fileopen1 = open("/var/log/auth.log", "r") 24 | counter = 1 25 | 26 | # for OS X 27 | if os.path.isfile("/var/log/secure.log"): 28 | if counter == 0: 29 | fileopen1 = open("/var/log/secure.log", "r") 30 | counter = 1 31 | 32 | # for centOS 33 | if os.path.isfile("/var/log/secure"): 34 | if counter == 0: 35 | fileopen1 = open("/var/log/secure", "r") 36 | counter = 1 37 | 38 | # for Debian 39 | if os.path.isfile("/var/log/faillog"): 40 | if counter == 0: 41 | fileopen1 = open("/var/log/faillog", "r") 42 | counter = 1 43 | 44 | if not os.path.isfile(globals.g_banlist): 45 | # create a blank file 46 | filewrite = open(globals.g_banlist, "w") 47 | filewrite.write("") 48 | filewrite.close() 49 | 50 | try: 51 | # base ssh counter to see how many attempts we've had 52 | ssh_counter = 0 53 | counter = 0 54 | for line in fileopen1: 55 | counter = 0 56 | fileopen2 = open(globals.g_banlist, "r") 57 | line = line.rstrip() 58 | # search for bad ssh 59 | match = re.search("Failed password for", line) 60 | if match: 61 | ssh_counter = ssh_counter + 1 62 | line = line.split(" ") 63 | # pull ipaddress 64 | ipaddress = line[-4] 65 | if is_valid_ipv4(ipaddress): 66 | 67 | # if its not a duplicate then ban that ass 68 | if ssh_counter >= int(ssh_attempts): 69 | banlist = fileopen2.read() 70 | match = re.search(ipaddress, banlist) 71 | if match: 72 | counter = 1 73 | # reset SSH counter 74 | ssh_counter = 0 75 | 76 | # if counter is equal to 0 then we know that we 77 | # need to ban 78 | if counter == 0: 79 | whitelist_match = is_whitelisted_ip(ipaddress) 80 | if whitelist_match == 0: 81 | subject = "[!] Artillery has banned an SSH brute force. [!]" 82 | alert = "Artillery has blocked (blacklisted) the following IP for SSH brute forcing violations: " + ipaddress 83 | warn_the_good_guys(subject, alert) 84 | 85 | # do the actual ban, this is pulled from 86 | # src.core 87 | ban(ipaddress) 88 | ssh_counter = 0 89 | 90 | # wait one to make sure everything is 91 | # caught up 92 | time.sleep(1) 93 | # sleep for defined time 94 | time.sleep(monitor_frequency) 95 | 96 | except Exception as e: 97 | print("[*] An error in ssh monitor occured. Printing it out here: " + str(e)) 98 | 99 | if is_posix(): 100 | thread.start_new_thread(ssh_monitor, (monitor_frequency,)) 101 | -------------------------------------------------------------------------------- /src/startup_artillery: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: artillery 5 | # Required-Start: $remote_fs $syslog 6 | # Required-Stop: $remote_fs $syslog 7 | # Default-Start: 2 3 4 5 8 | # Default-Stop: 0 1 6 9 | # Short-Description: Artillery - Advanced threat intelligence 10 | # Description: This file should be used to construct scripts to be 11 | # placed in /etc/init.d. 12 | ### END INIT INFO 13 | # artillery 14 | # description: Artillery - ATIF 15 | # processname: artillery 16 | 17 | DAEMON_PATH="/var/artillery" 18 | 19 | DAEMON=/var/artillery/artillery.py 20 | DAEMONOPTS="" 21 | 22 | NAME=artillery 23 | DESC="Artillery - Advanced threat intelligence" 24 | PIDFILE=/var/run/artillery.pid 25 | SCRIPTNAME=/etc/init.d/artillery 26 | 27 | case "$1" in 28 | start) 29 | printf "%-50s" "Starting Artillery..." 30 | cd $DAEMON_PATH 31 | PID=`$DAEMON $DAEMONOPTS > /dev/null 2>&1 & echo $!` 32 | #echo "Saving PID" $PID " to " $PIDFILE 33 | if [ -z $PID ]; then 34 | printf "%s\n" "Fail" 35 | else 36 | echo $PID > $PIDFILE 37 | printf "%s\n" "Ok" 38 | fi 39 | ;; 40 | status) 41 | printf "%-50s" "Checking Artillery..." 42 | if [ -f $PIDFILE ]; then 43 | PID=`cat $PIDFILE` 44 | if [ -z "`ps axf | grep ${PID} | grep -v grep`" ]; then 45 | printf "%s\n" "Process dead but pidfile exists" 46 | else 47 | echo "Running" 48 | fi 49 | else 50 | printf "%s\n" "Service not running" 51 | fi 52 | ;; 53 | stop) 54 | printf "%-50s" "Stopping Artillery..." 55 | PID=`cat $PIDFILE` 56 | cd $DAEMON_PATH 57 | if [ -f $PIDFILE ]; then 58 | kill -HUP $PID 59 | printf "%s\n" "Ok" 60 | rm -f $PIDFILE 61 | else 62 | printf "%s\n" "pidfile not found" 63 | fi 64 | ;; 65 | 66 | restart) 67 | $0 stop 68 | $0 start 69 | ;; 70 | 71 | *) 72 | echo "Usage: $0 {status|start|stop|restart}" 73 | exit 1 74 | esac 75 | -------------------------------------------------------------------------------- /src/win_func.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import re 3 | import os 4 | import sys 5 | import threading 6 | import datetime 7 | import time 8 | from win32evtlogutil import ReportEvent, AddSourceToRegistry, RemoveSourceFromRegistry 9 | from win32api import GetCurrentProcess 10 | from win32security import GetTokenInformation, TokenUser, OpenProcessToken 11 | from win32con import TOKEN_READ 12 | import win32evtlog 13 | 14 | # 15 | 16 | #py2 to 3 17 | if is_windows(): 18 | try: 19 | from _winreg import * 20 | except ImportError: 21 | from winreg import * 22 | 23 | if is_posix(): 24 | print("[!] Linux detected!!!!!!!!!.This script wil only run on windows. please try again") 25 | sys.exit() 26 | #################################################################################### 27 | #Function to return lists for most functions in this file 28 | #that way all there is to change is this function. this will insert all info 29 | #into list to use for referencing different things throught file 30 | # 31 | def get_config(cfg): 32 | '''get various pre-set config options used throughout script''' 33 | #Current artillery version 34 | current = ['2.1.1'] 35 | #Known Os versions 36 | oslst = ['Windows 7 Pro', 'Windows Server 2008 R2 Standard', 'Windows 8.1 Pro', 'Windows 10 Pro', 'Windows Small Business Server 2011 Essentials', 37 | 'Windows Server 2012 R2 Essentials', 'Hyper-V Server 2012 R2'] 38 | #Known Build numbers 39 | builds = ['7601', '9600', '1709', '17134'] 40 | regkeys = [r'SOFTWARE\Microsoft\Windows NT\CurrentVersion', r'SYSTEM\CurrentControlSet\Services\LanmanServer', r'SYSTEM\CurrentControlSet\Services\LanmanWorkstation', 41 | r'SYSTEM\CurrentControlSet\Services\WinHttpAutoProxySvc', r'SOFTWARE\Policies\Microsoft\Windows NT\DNSClient'] 42 | #switches for New-NetFirewallRule & Set-NetFirewallRule & Remove-NetFirewallRule functions in powershellused to initially create group and then add to it/remove from 43 | firew = ['New-NetFirewallRule ', 'Set-NetFirewallrule ', 'Remove-NetFirewallRule', '-Action ', '-DisplayName ', '-Direction ', '-Description ', '-Enabled ', '-RemoteAddress'] 44 | pshell = ['powershell.exe ', '-ExecutionPolicy ', 'Bypass '] 45 | #list to hold variables of host system tried to grab most important ones 46 | path_vars = ['SYSTEMDRIVE','PROGRAMFILES','COMPUTERNAME', 'PROCESSOR_ARCHITECTURE','PSMODULEPATH','NUMBER_OF_PROCESSORS','WINDIR'] 47 | #temp list 48 | temp = [] 49 | if cfg == 'CurrentBuild': 50 | return current 51 | elif cfg == 'OsList': 52 | return oslst 53 | elif cfg == 'Builds': 54 | return builds 55 | elif cfg == 'Reg': 56 | return regkeys 57 | elif cfg == 'Temp': 58 | return temp 59 | elif cfg == 'Firewall': 60 | firew.sort(reverse=True) 61 | return firew 62 | elif cfg == 'PShell': 63 | pshell.sort(reverse=True) 64 | return pshell 65 | elif cfg == 'Path': 66 | return path_vars 67 | else: 68 | pass 69 | # 70 | ############################################################################## 71 | def get_win_os(): 72 | '''This function uses pre-compiled lists to try and determine host os by comparing values to host entries 73 | if a match is found reports version''' 74 | if is_posix: 75 | pass 76 | if is_windows: 77 | OsName = "" 78 | OsBuild = "" 79 | #reg key list 80 | reg = get_config('Reg') 81 | #known os list 82 | kvl = get_config('OsList') 83 | #known builds 84 | b1 = get_config('Builds') 85 | #final client cfg list 86 | ccfg = [] 87 | try: 88 | oskey = reg[0] 89 | oskeyctr = 0 90 | oskeyval = OpenKey(HKEY_LOCAL_MACHINE, oskey) 91 | while True: 92 | ossubkey = EnumValue(oskeyval, oskeyctr) 93 | #dumps all results to txt file to parse for needed strings below 94 | osresults = open("version_check.txt", "a") 95 | osresults.write(str(ossubkey)+'\n') 96 | oskeyctr += 1 97 | #catch the error when it hits end of the key 98 | except WindowsError: 99 | osresults.close() 100 | #open up file and read what we got 101 | data = open('version_check.txt', 'r') 102 | # keywords from registry key in file 103 | keywords = ['ProductName', 'CurrentVersion', 'CurrentBuildNumber'] 104 | exp = re.compile("|".join(keywords), re.I) 105 | for line in data: 106 | #write out final info wanted to list 107 | if re.findall(exp, line): 108 | line = line.strip() 109 | ccfg.append(line) 110 | data.close() 111 | #delete the version info file. we dont need it any more 112 | subprocess.call(['cmd', '/C', 'del', 'version_check.txt']) 113 | # now compare 3 lists from get_config function and client_config.txt to use for id 114 | #sort clientconfig list to have items in same spot accross platforms 115 | ccfg.sort(reverse=True) 116 | osresults = ccfg[0] 117 | buildresults = ccfg[2] 118 | for name in kvl: 119 | if name in osresults: 120 | OsName = name 121 | for build in b1: 122 | if build in buildresults: 123 | OsBuild = build 124 | #when were done comparing print what was found 125 | print("[*] Detected OS: " + OsName, "Build: " + OsBuild) 126 | # -------------------------------------------------------------------------------- /src/windows/WindowsTODO.txt: -------------------------------------------------------------------------------- 1 | Just a note to upstream devs. I am in the process of building an event dll for windows. Still in testing phase. just trying to figure out how to process the alerts and info recieved. when complete i will publish to my repo as well as all src. 2 | any questions please contact me through my git page. 3 | 4 | RH 5 | 6 | i am introducing a new file to assist with windows related functions. i have been doing testing with powershell and it has been good. i need to gather more info on host to enable most functions. some functions wont run or arent on certain systems. also this relates to other cmds as well 7 | this file contains predetermined values to look for and responds accordingly based on what it finds. for now i am just intruducing partial lists for operation to limit issues and introduce things gradually 8 | -------------------------------------------------------------------------------- /src/windows/banlist.bat: -------------------------------------------------------------------------------- 1 | ::the pause cmd is only here for win7 if not the window closes 2 | 3 | ::right away.you can safely remove for other versions@echo OFF 4 | ::echo %cd% 5 | @echo OFF 6 | echo Copying banlist over...... 7 | copy banlist.txt "C:\Program Files (x86)\Artillery" 8 | exit 9 | exit 10 | exit 11 | 12 | -------------------------------------------------------------------------------- /src/windows/del_Cache.bat: -------------------------------------------------------------------------------- 1 | :: Deletes cache files from folder in setup and uninstall 2 | :: 3 | ::clean cache from main dir 4 | cd.. 5 | cd __pycache__ 6 | :: there is no pycache folder in py 2.7 .changed to pyc. to prevent from deleting all files in dir 7 | echo y|del *.pyc 8 | exit 9 | exit 10 | exit 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/windows/launch.bat: -------------------------------------------------------------------------------- 1 | ::Launches artillery after setup 2 | cd \ 3 | cd "C:\Program Files (x86)\Artillery" 4 | start "" python artillery.py 5 | exit 6 | exit 7 | exit 8 | --------------------------------------------------------------------------------