#!/usr/bin/env python """ Crystal Module for Grabber v0.1 Copyright (C) 2006 - Romain Gaucher - http://rgaucher.info """ import sys,os,re, string, shutil from xml.sax import * # Need PyXML [http://pyxml.sourceforge.net/] from grabber import getContent_POST, getContentDirectURL_POST from grabber import getContent_GET , getContentDirectURL_GET from grabber import single_urlencode, partially_in, unescape from grabber import investigate, setDatabase from spider import flatten, htmlencode, dict_add from spider import database vulnToDescritiveNames = { 'xss' : 'Cross-Site Scripting', 'sql' : 'SQL Injection', 'bsql': 'Specific Blind Injection...', 'include' : 'PHP Include Vulnerability' } """ Crystal Module Cooking Book --------------------------- Make-ahead Tip: Prepare lots of coffee before starting... Preparation: 24 hours Ingredients: A: PHP-Sat B: Grabber Modules lambda Tools: A: Context editor B: Python 2.4 C: Nice music (Opera is not needed but you should listen this) Directions: 0) Read the configuration file (with boolean operator in patterns) 1) Scan the PHP sources with PHP-Sat handler (which copy everything in the '/local/crystal/' directory). 2) Make a kind of diff then: If the diff results, check for the patterns (given in the configuration file) Parse the PHP line under the end of the pattern Try to get a variable value If no direct variable... backtrack sequentially or in the AST 3) Generate the XML report of "the crystal-static-analysis" module 4) Build a database of: >>> transformed_into_URL(hypothetical flawed files) : {list of "flawed" params} 5) Run the classical tools """ # Crystal Configuration variables crystalFiles = None crystalUrl = None crystalExtension = None crystalAnalyzerBin= None crystalAnalyzerInputParam = None crystalAnalyzerOutputParam = None crystalCheckStart = None crystalCheckEnd = None # example: {'xss' : ['pattern_1 __AND__ pattern_3','pattern_2'], 'sql' : ['pattern_3'], 'bsql' : ['pattern_3']} crystalPatterns = {} # example: {'xss' : [{'var-position' : reg1}], 'sql' : [{'var-position' : reg2}]} crystalRegExpPatterns = {} crystalStorage = [] crystalDatabase = {} crystalFinalStorage = {} def normalize_whitespace(text): return ' '.join(text.split()) def clear_whitespace(text): return text.replace(' ','') # Handle the XML file with a SAX Parser class CrystalConfHandler(ContentHandler): def __init__(self): self.inAnalyzer = False self.inPatterns = False self.inPattern = False self.isRegExp = False self.curretVarPos= None self.currentKeys = [] self.string = "" def startElement(self, name, attrs): global crystalAnalyzerInputParam, crystalAnalyzerOutputParam, crystalPatterns, crystalCheckStart, crystalCheckEnd self.string = "" self.currentKeys = [] if name == 'analyzer': self.inAnalyzer = True elif name == 'path' and self.inAnalyzer: # store the attributes input and output if 'input' in attrs.keys() and 'output' in attrs.keys(): crystalAnalyzerInputParam = attrs.getValue('input') crystalAnalyzerOutputParam = attrs.getValue('output') else: raise KeyError("CrystalXMLConf: needs 'input' and 'output' attributes") elif name == 'patterns' and self.inAnalyzer: self.inPatterns = True if 'start' in attrs.keys() and 'end' in attrs.keys(): crystalCheckStart = attrs.getValue('start') crystalCheckEnd = attrs.getValue('end') else: raise KeyError("CrystalXMLConf: needs 'start' and 'end' attributes") if 'name' in attrs.keys(): if attrs.getValue('name').lower() == 'regexp': self.isRegExp = True elif self.inPatterns and name == 'pattern': self.inPattern = True if 'module' in attrs.keys(): modules = attrs.getValue('module') modules.replace(' ','') self.currentKeys = modules.split(',') if self.isRegExp: if 'varposition' in attrs.keys(): curretVarPos = attrs.getValue('varposition') else: raise KeyError("CrystalXMLConf: needs 'varposition' attribute") def characters(self, ch): self.string = self.string + ch def endElement(self, name): global crystalFiles, crystalUrl, crystalExtension, crystalAnalyzerBin, crystalPatterns, crystalRegExpPatterns if name == 'files': crystalFiles = normalize_whitespace(self.string) elif name == 'url': crystalUrl = normalize_whitespace(self.string) elif name == 'extension' and self.inAnalyzer: crystalExtension = normalize_whitespace(self.string) elif name == 'path' and self.inAnalyzer: crystalAnalyzerBin = normalize_whitespace(self.string) elif not self.isRegExp and name == 'pattern' and self.inPattern: tempList = self.string.split('__OR__') for a in self.currentKeys: if a not in crystalPatterns: crystalPatterns[a] = [] l = crystalPatterns[a] for t in tempList: l.append(normalize_whitespace(t)) elif self.isRegExp and name == 'pattern' and self.inPattern: """ tempList = self.string.split('__OR__') for a in self.currentKeys: if a not in crystalPatterns: crystalRegExpPatterns[a] = [] l = crystalRegExpPatterns[a] # build the compiled regexp plop = normalize_whitespace(l) plop = re.compile(plop, re.I) l.append({currentVarPos : plop}) """ elif name == "patterns" and self.inPatterns: self.inPatterns = False if self.isRegExp: self.isRegExp = False elif name == "analyzer" and self.inAnalyzer: self.inAnalyzer = False def copySubTree(src, dst, regFilter): global crystalStorage names = os.listdir(src) try: os.mkdir(dst) except OSError: a = 0 try: os.mkdir(dst.replace('crystal/current', 'crystal/analyzed')) except OSError: a = 0 for name in names: srcname = os.path.join(src, name) dstname = os.path.join(dst, name) try: if os.path.islink(srcname): linkto = os.readlink(srcname) os.symlink(linkto, dstname) elif os.path.isdir(srcname): copySubTree(srcname, dstname, regFilter) elif regFilter.match(srcname): shutil.copy2(srcname, dstname) crystalStorage.append(dstname) except (IOError, os.error), why: continue def execCmd(program, args): p = os.popen(program + " " + args) p.close() def generateListOfFiles(): """ Create a ghost in ./local/crystal/current and /local/crystal/analyzed And run the SwA tool """ regScripts = re.compile(r'(.*).' + crystalExtension + '$', re.I) copySubTree(crystalFiles, 'local/crystal/current', regScripts) print "Running the static analysis tool..." for file in crystalStorage: fileIn = os.path.abspath(os.path.join('./', file)) fileOut = os.path.abspath(os.path.join('./', file.replace('current', 'analyzed'))) cmdLine = crystalAnalyzerInputParam + " " + fileIn + " " + crystalAnalyzerOutputParam + " " + fileOut # execCmd(crystalAnalyzerBin, cmdLine) print crystalAnalyzerBin,cmdLine os.system(crystalAnalyzerBin +" "+ cmdLine) def stripNoneASCII(output): # should be somepthing to do that.. :/ newOutput = "" for s in output: try: s = s.encode() newOutput += s except UnicodeDecodeError: continue return newOutput def isPatternInFile(fileName): global crystalDatabase file = None try: file = open(fileName, 'r') except IOError: print "Crystal: Cannot open the file [%s]" % fileName return False inZone, inLined = False, False detectPattern = False lineNumber = 0 shortName = fileName[fileName.rfind('analyzed') + 9 : ] vulnName = "" for l in file.readlines(): lineNumber += 1 l = l.replace('\n','') try: """ Check for the regular expression patterns if len(crystalRegExpPatterns) > 0: for modules in crystalRegExpPatterns: for regexp in crystalRegExpPatterns[modules]: if regexp.match() """ if len(vulnName) > 0 and (detectPattern and not inZone or inLined): # creating the nice structure # { 'index.php' : {'xss' : {'12', 'echo $_GET["plop"]'}}} if shortName not in crystalDatabase: crystalDatabase[shortName] = {} if vulnName not in crystalDatabase[shortName]: crystalDatabase[shortName][vulnName] = {} if str(lineNumber) not in crystalDatabase[shortName][vulnName]: crystalDatabase[shortName][vulnName][str(lineNumber)] = l detectPattern = False inLined = False vulnName = "" if l.count(crystalCheckStart) > 0 and not inZone: b1 = l.find(crystalCheckStart) inZone = True # same line for start and end ? if l.count(crystalCheckEnd) > 0: b2 = l.find(crystalCheckStart) if b1 < b2: inZone = False position = l.lower().find(pattern.lower()) if b1 < position and position < b2: detectPattern = True inLined = True elif inZone: # is there any pattern around the corner ? for modules in crystalPatterns: for p in crystalPatterns[modules]: p = p.lower() l = l.lower() # The folowing code is stupid! # I have to change the algorithm for the __AND__ parsing... if '__AND__' in p: listPatterns = p.split('__AND__') isIn = True for patton in listPatterns: if patton not in l: isIn = isIn and False if isIn: detectPattern = True vulnName = modules a=0 else: # test if the simple pattern is in the line if p in l: detectPattern = True vulnName = modules if l.count(crystalCheckEnd) > 0: inZone = False except UnicodeDecodeError: continue return True def buildDatabase(): """ Read the analzed files (indirectly with the crystalStorage.replace('current','analyzed') ) And look for the patterns """ listOut = [] for file in crystalStorage: fileOut = os.path.abspath(os.path.join('./', file.replace('current', 'analyzed'))) if not isPatternInFile(fileOut): print "Error with the file [%s]" % file def createStructure(): """ Create the structure in the ./local directory """ try: os.mkdir("local/crystal/") except OSError,e : a=0 try: os.mkdir("local/crystal/current") except OSError,e : a=0 try: os.mkdir("local/crystal/analyzed") except OSError,e : a=0 """ def realLineNumberReverse(fileName, codeStr): print fileName, codeStr try: fN = os.path.abspath(os.path.join('./local/crystal/current/', fileName)) file = open(fN, 'r') lineNumber = 0 for a in file.readlines(): lineNumber += 1 if codeStr in a: print a file.close() return lineNumber file.close() except IOError,e: print e return 0 return 0 """ def generateReport_1(): """ Create a first report like: * Developer report: # using XSLT... xss sql ... * Security report: ... """ plop = open('results/crystal_SecurityReport_Grabber.xml','w') plop.write("\n") plop.write("\n") plop.write("\n") for file in crystalDatabase: plop.write("\t\n" % file) for vuln in crystalDatabase[file]: for line in crystalDatabase[file][vuln]: # lineNumber = realLineNumberReverse(file,crystalDatabase[file][vuln][line]) localVuln = vuln if localVuln in vulnToDescritiveNames: localVuln = vulnToDescritiveNames[localVuln] plop.write("\t\t%s\n" % (localVuln, line, htmlencode(crystalDatabase[file][vuln][line]))) plop.write("\t\n") plop.write("\n") plop.write("\n") plop.close() def buildUrlKey(file): fileName = file.replace('\\','/') # on windows... keyUrl = crystalUrl if keyUrl[len(keyUrl)-1] != '/' and fileName[0] != '/': keyUrl += '/' keyUrl += fileName return keyUrl reParamPOST = re.compile(r'(.*)\$_POST\[(.+)\](.*)',re.I) reParamGET = re.compile(r'(.*)\$_GET\[(.+)\](.*)' ,re.I) def getSimpleParamFromCode_GET(code): """ Using the regular expression above, try to get some parameters name """ params = [] # we can have multiple params... code = code.replace("'",''); code = code.replace('"',''); if code.lower().count('get') > 0: # try to match the $_GET if reParamGET.match(code): out = reParamGET.search(code) params.append(out.group(2)) params.append(getSimpleParamFromCode_GET(out.group(3))) params = flatten(params) return params def getSimpleParamFromCode_POST(code): """ Using the regular expression above, try to get some parameters name """ params = [] # we can have multiple params... code = code.replace("'",''); code = code.replace('"',''); if code.lower().count('post') > 0: # try to match the $_GET if reParamPOST.match(code): out = reParamPOST.search(code) params.append(out.group(2)) params.append(getSimpleParamFromCode_POST(out.group(3))) params = flatten(params) return params def createClassicalDatabase(vulnsType, localCrystalDB): """ From the crystalDatabase, generate the same database as in Spider This is generated for calling the differents modules ClassicalDB = { url : { 'GET' : { param : value } } } """ classicalDB = {} for file in localCrystalDB: # build the URL keyUrl = buildUrlKey(file) if keyUrl not in classicalDB: classicalDB[keyUrl] = {'GET' : {}, 'POST' : {}} for vuln in localCrystalDB[file]: # only get the kind of vulnerability we want if vuln != vulnsType: continue for line in localCrystalDB[file][vuln]: code = localCrystalDB[file][vuln][line] # try to extract some data... params_GET = getSimpleParamFromCode_GET (code) params_POST = getSimpleParamFromCode_POST(code) if len(params_GET) > 0: for p in params_GET: lG = classicalDB[keyUrl]['GET'] if p not in classicalDB[keyUrl]['GET']: lG = dict_add(lG,{p:''}) classicalDB[keyUrl]['GET'] = lG if len(params_POST) > 0: for p in params_POST: lP = classicalDB[keyUrl]['POST'] if p not in classicalDB[keyUrl]['POST']: lP = dict_add(lP,{p:''}) classicalDB[keyUrl]['POST'] = lP return classicalDB def retrieveVulnList(): vulnList = [] for file in crystalDatabase: for vuln in crystalDatabase[file]: if vuln not in vulnList: vulnList.append(vuln) return vulnList def process(urlGlobal, localDB, attack_list): """ Crystal Module entry point """ print "Crystal Module Start" try: f = open("crystal.conf.xml", 'r') f.close() except IOError: print "The crystal module needs the 'crystal.conf.xml' configuration file." sys.exit(1) parser = make_parser() crystal_handler = CrystalConfHandler() # Tell the parser to use our handler parser.setContentHandler(crystal_handler) try: parser.parse("crystal.conf.xml") except KeyError, e: print e sys.exit(1) #---------- White box testing createStructure() generateListOfFiles() buildDatabase() print "Build first report: List of vulneratilities and places in the code" generateReport_1() #---------- Start the Black Box testing # need to create a classical database like, so losing information # but for a type of vulnerability listVulns = retrieveVulnList() for vulns in listVulns: localDatabase = createClassicalDatabase(vulns, crystalDatabase) setDatabase(localDatabase) print "inProcess Crystal DB = ", localDatabase # print vulns, database # Call the Black Box Module print "Scan for ", vulns investigate(crystalUrl, vulns) print "Crystal Module Stop"