|
| 1 | +""" |
| 2 | +Copyright(C) 2018 Stamus Networks |
| 3 | +Written by Eric Leblond <[email protected]> |
| 4 | +
|
| 5 | +This file is part of Scirius. |
| 6 | +
|
| 7 | +Scirius is free software: you can redistribute it and/or modify |
| 8 | +it under the terms of the GNU General Public License as published by |
| 9 | +the Free Software Foundation, either version 3 of the License, or |
| 10 | +(at your option) any later version. |
| 11 | +
|
| 12 | +Scirius is distributed in the hope that it will be useful, |
| 13 | +but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | +GNU General Public License for more details. |
| 16 | +
|
| 17 | +You should have received a copy of the GNU General Public License |
| 18 | +along with Scirius. If not, see <http://www.gnu.org/licenses/>. |
| 19 | +""" |
| 20 | + |
| 21 | +import psutil |
| 22 | +import subprocess |
| 23 | +import tempfile |
| 24 | +import shutil |
| 25 | +import os |
| 26 | +import json |
| 27 | +import StringIO |
| 28 | +import re |
| 29 | + |
| 30 | +from django.utils.html import escape |
| 31 | + |
| 32 | +class TestRules(): |
| 33 | + VARIABLE_ERROR = 101 |
| 34 | + RULEFILE_ERRNO = [ 39, 42 ] |
| 35 | + USELESS_ERRNO = [ 40, 43, 44 ] |
| 36 | + CONFIG_FILE = """ |
| 37 | +%YAML 1.1 |
| 38 | +--- |
| 39 | +logging: |
| 40 | + default-log-level: error |
| 41 | + outputs: |
| 42 | + - console: |
| 43 | + enabled: yes |
| 44 | + type: json |
| 45 | +vars: |
| 46 | + address-groups: |
| 47 | + HOME_NET: "[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12]" |
| 48 | + EXTERNAL_NET: "!$HOME_NET" |
| 49 | + HTTP_SERVERS: "$HOME_NET" |
| 50 | + SMTP_SERVERS: "$HOME_NET" |
| 51 | + SQL_SERVERS: "$HOME_NET" |
| 52 | + DNS_SERVERS: "$HOME_NET" |
| 53 | + TELNET_SERVERS: "$HOME_NET" |
| 54 | + AIM_SERVERS: "$EXTERNAL_NET" |
| 55 | + DNP3_SERVER: "$HOME_NET" |
| 56 | + DNP3_CLIENT: "$HOME_NET" |
| 57 | + MODBUS_CLIENT: "$HOME_NET" |
| 58 | + MODBUS_SERVER: "$HOME_NET" |
| 59 | + ENIP_CLIENT: "$HOME_NET" |
| 60 | + ENIP_SERVER: "$HOME_NET" |
| 61 | + port-groups: |
| 62 | + HTTP_PORTS: "80" |
| 63 | + SHELLCODE_PORTS: "!80" |
| 64 | + ORACLE_PORTS: 1521 |
| 65 | + SSH_PORTS: 22 |
| 66 | + DNP3_PORTS: 20000 |
| 67 | + MODBUS_PORTS: 502 |
| 68 | +""" |
| 69 | + |
| 70 | + REFERENCE_CONFIG = """ |
| 71 | +# config reference: system URL |
| 72 | +
|
| 73 | +config reference: bugtraq http://www.securityfocus.com/bid/ |
| 74 | +config reference: bid http://www.securityfocus.com/bid/ |
| 75 | +config reference: cve http://cve.mitre.org/cgi-bin/cvename.cgi?name= |
| 76 | +#config reference: cve http://cvedetails.com/cve/ |
| 77 | +config reference: secunia http://www.secunia.com/advisories/ |
| 78 | +
|
| 79 | +#whitehats is unfortunately gone |
| 80 | +config reference: arachNIDS http://www.whitehats.com/info/IDS |
| 81 | +
|
| 82 | +config reference: McAfee http://vil.nai.com/vil/content/v_ |
| 83 | +config reference: nessus http://cgi.nessus.org/plugins/dump.php3?id= |
| 84 | +config reference: url http:// |
| 85 | +config reference: et http://doc.emergingthreats.net/ |
| 86 | +config reference: etpro http://doc.emergingthreatspro.com/ |
| 87 | +config reference: telus http:// |
| 88 | +config reference: osvdb http://osvdb.org/show/osvdb/ |
| 89 | +config reference: threatexpert http://www.threatexpert.com/report.aspx?md5= |
| 90 | +config reference: md5 http://www.threatexpert.com/report.aspx?md5= |
| 91 | +config reference: exploitdb http://www.exploit-db.com/exploits/ |
| 92 | +config reference: openpacket https://www.openpacket.org/capture/grab/ |
| 93 | +config reference: securitytracker http://securitytracker.com/id? |
| 94 | +config reference: secunia http://secunia.com/advisories/ |
| 95 | +config reference: xforce http://xforce.iss.net/xforce/xfdb/ |
| 96 | +config reference: msft http://technet.microsoft.com/security/bulletin/ |
| 97 | +""" |
| 98 | + |
| 99 | + CLASSIFICATION_CONFIG = """ |
| 100 | +config classification: not-suspicious,Not Suspicious Traffic,3 |
| 101 | +config classification: unknown,Unknown Traffic,3 |
| 102 | +config classification: bad-unknown,Potentially Bad Traffic, 2 |
| 103 | +config classification: attempted-recon,Attempted Information Leak,2 |
| 104 | +config classification: successful-recon-limited,Information Leak,2 |
| 105 | +config classification: successful-recon-largescale,Large Scale Information Leak,2 |
| 106 | +config classification: attempted-dos,Attempted Denial of Service,2 |
| 107 | +config classification: successful-dos,Denial of Service,2 |
| 108 | +config classification: attempted-user,Attempted User Privilege Gain,1 |
| 109 | +config classification: unsuccessful-user,Unsuccessful User Privilege Gain,1 |
| 110 | +config classification: successful-user,Successful User Privilege Gain,1 |
| 111 | +config classification: attempted-admin,Attempted Administrator Privilege Gain,1 |
| 112 | +config classification: successful-admin,Successful Administrator Privilege Gain,1 |
| 113 | +
|
| 114 | +
|
| 115 | +# NEW CLASSIFICATIONS |
| 116 | +config classification: rpc-portmap-decode,Decode of an RPC Query,2 |
| 117 | +config classification: shellcode-detect,Executable code was detected,1 |
| 118 | +config classification: string-detect,A suspicious string was detected,3 |
| 119 | +config classification: suspicious-filename-detect,A suspicious filename was detected,2 |
| 120 | +config classification: suspicious-login,An attempted login using a suspicious username was detected,2 |
| 121 | +config classification: system-call-detect,A system call was detected,2 |
| 122 | +config classification: tcp-connection,A TCP connection was detected,4 |
| 123 | +config classification: trojan-activity,A Network Trojan was detected, 1 |
| 124 | +config classification: unusual-client-port-connection,A client was using an unusual port,2 |
| 125 | +config classification: network-scan,Detection of a Network Scan,3 |
| 126 | +config classification: denial-of-service,Detection of a Denial of Service Attack,2 |
| 127 | +config classification: non-standard-protocol,Detection of a non-standard protocol or event,2 |
| 128 | +config classification: protocol-command-decode,Generic Protocol Command Decode,3 |
| 129 | +config classification: web-application-activity,access to a potentially vulnerable web application,2 |
| 130 | +config classification: web-application-attack,Web Application Attack,1 |
| 131 | +config classification: misc-activity,Misc activity,3 |
| 132 | +config classification: misc-attack,Misc Attack,2 |
| 133 | +config classification: icmp-event,Generic ICMP event,3 |
| 134 | +config classification: kickass-porn,SCORE! Get the lotion!,1 |
| 135 | +config classification: policy-violation,Potential Corporate Privacy Violation,1 |
| 136 | +config classification: default-login-attempt,Attempt to login by a default username and password,2 |
| 137 | +""" |
| 138 | + |
| 139 | + def parse_suricata_error(self, error, single = False): |
| 140 | + ret = { |
| 141 | + 'errors': [], |
| 142 | + 'warnings': [], |
| 143 | + } |
| 144 | + error_list = [] |
| 145 | + warning_list = [] |
| 146 | + variable_list = [] |
| 147 | + error_stream = StringIO.StringIO(error) |
| 148 | + for line in error_stream: |
| 149 | + try: |
| 150 | + s_err = json.loads(line) |
| 151 | + except: |
| 152 | + ret['errors'].append({'message': error, 'format': 'raw'}) |
| 153 | + return ret |
| 154 | + errno = s_err['engine']['error_code'] |
| 155 | + if not single or errno not in self.RULEFILE_ERRNO: |
| 156 | + if errno == self.VARIABLE_ERROR: |
| 157 | + variable = s_err['engine']['message'].split("\"")[1] |
| 158 | + if not "$" + variable in variable_list: |
| 159 | + variable_list.append("$" + variable) |
| 160 | + s_err['engine']['message'] = "Custom address variable \"$%s\" is used and need to be defined in probes configuration" % (variable) |
| 161 | + ret['warnings'].append(s_err['engine']) |
| 162 | + continue |
| 163 | + if not errno in self.USELESS_ERRNO: |
| 164 | + # clean error message |
| 165 | + if errno == 39: |
| 166 | + # exclude error on varible |
| 167 | + found = False |
| 168 | + for variable in variable_list: |
| 169 | + if variable in s_err['engine']['message']: |
| 170 | + found = True |
| 171 | + break |
| 172 | + if found: |
| 173 | + continue |
| 174 | + s_err['engine']['message'] = s_err['engine']['message'].split(' from file')[0] |
| 175 | + getsid = re.compile("sid *:(\d+)") |
| 176 | + match = getsid.search(line) |
| 177 | + if match: |
| 178 | + s_err['engine']['sid'] = int(match.groups()[0]) |
| 179 | + if errno == 42: |
| 180 | + s_err['engine']['message'] = s_err['engine']['message'].split(' from')[0] |
| 181 | + ret['errors'].append(s_err['engine']) |
| 182 | + return ret |
| 183 | + |
| 184 | + def rule_buffer(self, rule_buffer, config_buffer = None, related_files = None, reference_config = None, classification_config = None): |
| 185 | + # create temp directory |
| 186 | + tmpdir = tempfile.mkdtemp() |
| 187 | + # write the rule file in temp dir |
| 188 | + rule_file = os.path.join(tmpdir, "file.rules") |
| 189 | + rf = open(rule_file, 'w') |
| 190 | + try: |
| 191 | + rf.write(rule_buffer) |
| 192 | + except UnicodeEncodeError: |
| 193 | + rf.write(rule_buffer.encode('utf-8')) |
| 194 | + rf.close() |
| 195 | + |
| 196 | + if not reference_config: |
| 197 | + refence_config = self.REFERENCE_CONFIG |
| 198 | + reference_file = os.path.join(tmpdir, "reference.config") |
| 199 | + rf = open(reference_file, 'w') |
| 200 | + rf.write(refence_config) |
| 201 | + rf.close() |
| 202 | + |
| 203 | + if not classification_config: |
| 204 | + classification_config = self.CLASSIFICATION_CONFIG |
| 205 | + classification_file = os.path.join(tmpdir, "classification.config") |
| 206 | + cf = open(classification_file, 'w') |
| 207 | + cf.write(classification_config) |
| 208 | + cf.close() |
| 209 | + |
| 210 | + if not config_buffer: |
| 211 | + config_buffer = self.CONFIG_FILE |
| 212 | + config_file = os.path.join(tmpdir, "suricata.yaml") |
| 213 | + cf = open(config_file, 'w') |
| 214 | + # write the config file in temp dir |
| 215 | + cf.write(config_buffer) |
| 216 | + cf.write("default-rule-path: " + tmpdir + "\n") |
| 217 | + cf.write("default-reputation-path: " + tmpdir + "\n") |
| 218 | + cf.write("reference-config-file: " + tmpdir + "/reference.config\n") |
| 219 | + cf.write("classification-file: " + tmpdir + "/classification.config\n") |
| 220 | + cf.close() |
| 221 | + related_files = related_files or {} |
| 222 | + for rfile in related_files: |
| 223 | + related_file = os.path.join(tmpdir, rfile) |
| 224 | + rf = open(related_file, 'w') |
| 225 | + rf.write(related_files[rfile]) |
| 226 | + rf.close() |
| 227 | + |
| 228 | + suri_cmd = ['suricata', '-T', '-l', tmpdir, '-S', rule_file, '-c', config_file] |
| 229 | + # start suricata in test mode |
| 230 | + suriprocess = subprocess.Popen(suri_cmd , stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| 231 | + (outdata, errdata) = suriprocess.communicate() |
| 232 | + shutil.rmtree(tmpdir) |
| 233 | + # if success ok |
| 234 | + if suriprocess.returncode == 0: |
| 235 | + return {'status': True, 'errors': ''} |
| 236 | + # if not return error |
| 237 | + return {'status': False, 'errors': errdata} |
| 238 | + |
| 239 | + def _escape_result(self, res): |
| 240 | + for key in ('warnings', 'errors'): |
| 241 | + lst = res.get(key, []) |
| 242 | + for msg in lst: |
| 243 | + msg['message'] = escape(msg['message']) |
| 244 | + return res |
| 245 | + |
| 246 | + def check_rule_buffer(self, rule_buffer, config_buffer = None, related_files = None, single = False): |
| 247 | + related_files = related_files or {} |
| 248 | + prov_result = self.rule_buffer(rule_buffer, config_buffer = config_buffer, related_files = related_files) |
| 249 | + if prov_result['status'] and not prov_result.has_key('warnings'): |
| 250 | + return self._escape_result(prov_result) |
| 251 | + res = self.parse_suricata_error(prov_result['errors'], single = single) |
| 252 | + prov_result['errors'] = res['errors'] |
| 253 | + prov_result['warnings'] = res['warnings'] |
| 254 | + i = 6 # support only 6 unknown variables per rule |
| 255 | + prov_result['iter'] = 0; |
| 256 | + while len(res['warnings']) and i > 0: |
| 257 | + modified = False |
| 258 | + for warning in res['warnings']: |
| 259 | + if warning['error_code'] == self.VARIABLE_ERROR: |
| 260 | + var = warning['message'].split("\"")[1] |
| 261 | + # transform rule_buffer to remove the faulty variable |
| 262 | + rule_buffer = rule_buffer.replace("!" + var, "any"); |
| 263 | + rule_buffer = rule_buffer.replace(var, "any"); |
| 264 | + modified = True |
| 265 | + if modified == False: |
| 266 | + break |
| 267 | + result = self.rule_buffer(rule_buffer, config_buffer = config_buffer, related_files = related_files) |
| 268 | + res = self.parse_suricata_error(result['errors'], single = single) |
| 269 | + prov_result['errors'] = res['errors'] |
| 270 | + if len(res['warnings']): |
| 271 | + prov_result['warnings'] = prov_result['warnings'] + res['warnings'] |
| 272 | + i = i - 1 |
| 273 | + prov_result['iter'] = prov_result['iter'] + 1 |
| 274 | + if len(prov_result['errors']) == 0: |
| 275 | + prov_result['status'] = True |
| 276 | + return self._escape_result(prov_result) |
| 277 | + |
| 278 | + def rule(self, rule_buffer, config_buffer = None, related_files = None): |
| 279 | + related_files = related_files or {} |
| 280 | + return self.check_rule_buffer(rule_buffer, config_buffer = config_buffer, related_files = related_files, single = True) |
| 281 | + |
| 282 | + def rules(self, rule_buffer, config_buffer = None, related_files = None): |
| 283 | + related_files = related_files or {} |
| 284 | + return self.check_rule_buffer(rule_buffer, config_buffer = config_buffer, related_files = related_files, single = False) |
| 285 | + |
0 commit comments