PageRenderTime 52ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/pentest/grabber/crystal.py

https://github.com/sullivanmatt/Raspberry-Pwn
Python | 532 lines | 500 code | 7 blank | 25 comment | 4 complexity | 1bd205dd4517e651ab43b89cca341ccf MD5 | raw file
Possible License(s): BSD-3-Clause, AGPL-1.0, MPL-2.0-no-copyleft-exception, GPL-2.0, GPL-3.0
  1. #!/usr/bin/env python
  2. """
  3. Crystal Module for Grabber v0.1
  4. Copyright (C) 2006 - Romain Gaucher - http://rgaucher.info
  5. """
  6. import sys,os,re, string, shutil
  7. from xml.sax import * # Need PyXML [http://pyxml.sourceforge.net/]
  8. from grabber import getContent_POST, getContentDirectURL_POST
  9. from grabber import getContent_GET , getContentDirectURL_GET
  10. from grabber import single_urlencode, partially_in, unescape
  11. from grabber import investigate, setDatabase
  12. from spider import flatten, htmlencode, dict_add
  13. from spider import database
  14. vulnToDescritiveNames = {
  15. 'xss' : 'Cross-Site Scripting',
  16. 'sql' : 'SQL Injection',
  17. 'bsql': 'Specific Blind Injection...',
  18. 'include' : 'PHP Include Vulnerability'
  19. }
  20. """
  21. Crystal Module Cooking Book
  22. ---------------------------
  23. Make-ahead Tip: Prepare lots of coffee before starting...
  24. Preparation: 24 hours
  25. Ingredients:
  26. A: PHP-Sat
  27. B: Grabber Modules lambda
  28. Tools:
  29. A: Context editor
  30. B: Python 2.4
  31. C: Nice music (Opera is not needed but you should listen this)
  32. Directions:
  33. 0) Read the configuration file (with boolean operator in patterns)
  34. 1) Scan the PHP sources with PHP-Sat handler (which copy everything in the
  35. '/local/crystal/' directory).
  36. 2) Make a kind of diff then:
  37. If the diff results, check for the patterns (given in the configuration file)
  38. Parse the PHP line under the end of the pattern
  39. Try to get a variable value
  40. <after>
  41. If no direct variable... backtrack sequentially or in the AST
  42. </after>
  43. 3) Generate the XML report of "the crystal-static-analysis" module
  44. 4) Build a database of:
  45. >>> transformed_into_URL(hypothetical flawed files) : {list of "flawed" params}
  46. 5) Run the classical tools
  47. """
  48. # Crystal Configuration variables
  49. crystalFiles = None
  50. crystalUrl = None
  51. crystalExtension = None
  52. crystalAnalyzerBin= None
  53. crystalAnalyzerInputParam = None
  54. crystalAnalyzerOutputParam = None
  55. crystalCheckStart = None
  56. crystalCheckEnd = None
  57. # example: {'xss' : ['pattern_1 __AND__ pattern_3','pattern_2'], 'sql' : ['pattern_3'], 'bsql' : ['pattern_3']}
  58. crystalPatterns = {}
  59. # example: {'xss' : [{'var-position' : reg1}], 'sql' : [{'var-position' : reg2}]}
  60. crystalRegExpPatterns = {}
  61. crystalStorage = []
  62. crystalDatabase = {}
  63. crystalFinalStorage = {}
  64. def normalize_whitespace(text):
  65. return ' '.join(text.split())
  66. def clear_whitespace(text):
  67. return text.replace(' ','')
  68. # Handle the XML file with a SAX Parser
  69. class CrystalConfHandler(ContentHandler):
  70. def __init__(self):
  71. self.inAnalyzer = False
  72. self.inPatterns = False
  73. self.inPattern = False
  74. self.isRegExp = False
  75. self.curretVarPos= None
  76. self.currentKeys = []
  77. self.string = ""
  78. def startElement(self, name, attrs):
  79. global crystalAnalyzerInputParam, crystalAnalyzerOutputParam, crystalPatterns, crystalCheckStart, crystalCheckEnd
  80. self.string = ""
  81. self.currentKeys = []
  82. if name == 'analyzer':
  83. self.inAnalyzer = True
  84. elif name == 'path' and self.inAnalyzer:
  85. # store the attributes input and output
  86. if 'input' in attrs.keys() and 'output' in attrs.keys():
  87. crystalAnalyzerInputParam = attrs.getValue('input')
  88. crystalAnalyzerOutputParam = attrs.getValue('output')
  89. else:
  90. raise KeyError("CrystalXMLConf: <path> needs 'input' and 'output' attributes")
  91. elif name == 'patterns' and self.inAnalyzer:
  92. self.inPatterns = True
  93. if 'start' in attrs.keys() and 'end' in attrs.keys():
  94. crystalCheckStart = attrs.getValue('start')
  95. crystalCheckEnd = attrs.getValue('end')
  96. else:
  97. raise KeyError("CrystalXMLConf: <patterns> needs 'start' and 'end' attributes")
  98. if 'name' in attrs.keys():
  99. if attrs.getValue('name').lower() == 'regexp':
  100. self.isRegExp = True
  101. elif self.inPatterns and name == 'pattern':
  102. self.inPattern = True
  103. if 'module' in attrs.keys():
  104. modules = attrs.getValue('module')
  105. modules.replace(' ','')
  106. self.currentKeys = modules.split(',')
  107. if self.isRegExp:
  108. if 'varposition' in attrs.keys():
  109. curretVarPos = attrs.getValue('varposition')
  110. else:
  111. raise KeyError("CrystalXMLConf: <pattern > needs 'varposition' attribute")
  112. def characters(self, ch):
  113. self.string = self.string + ch
  114. def endElement(self, name):
  115. global crystalFiles, crystalUrl, crystalExtension, crystalAnalyzerBin, crystalPatterns, crystalRegExpPatterns
  116. if name == 'files':
  117. crystalFiles = normalize_whitespace(self.string)
  118. elif name == 'url':
  119. crystalUrl = normalize_whitespace(self.string)
  120. elif name == 'extension' and self.inAnalyzer:
  121. crystalExtension = normalize_whitespace(self.string)
  122. elif name == 'path' and self.inAnalyzer:
  123. crystalAnalyzerBin = normalize_whitespace(self.string)
  124. elif not self.isRegExp and name == 'pattern' and self.inPattern:
  125. tempList = self.string.split('__OR__')
  126. for a in self.currentKeys:
  127. if a not in crystalPatterns:
  128. crystalPatterns[a] = []
  129. l = crystalPatterns[a]
  130. for t in tempList:
  131. l.append(normalize_whitespace(t))
  132. elif self.isRegExp and name == 'pattern' and self.inPattern:
  133. """
  134. tempList = self.string.split('__OR__')
  135. for a in self.currentKeys:
  136. if a not in crystalPatterns:
  137. crystalRegExpPatterns[a] = []
  138. l = crystalRegExpPatterns[a]
  139. # build the compiled regexp
  140. plop = normalize_whitespace(l)
  141. plop = re.compile(plop, re.I)
  142. l.append({currentVarPos : plop})
  143. """
  144. elif name == "patterns" and self.inPatterns:
  145. self.inPatterns = False
  146. if self.isRegExp:
  147. self.isRegExp = False
  148. elif name == "analyzer" and self.inAnalyzer:
  149. self.inAnalyzer = False
  150. def copySubTree(src, dst, regFilter):
  151. global crystalStorage
  152. names = os.listdir(src)
  153. try:
  154. os.mkdir(dst)
  155. except OSError:
  156. a = 0
  157. try:
  158. os.mkdir(dst.replace('crystal/current', 'crystal/analyzed'))
  159. except OSError:
  160. a = 0
  161. for name in names:
  162. srcname = os.path.join(src, name)
  163. dstname = os.path.join(dst, name)
  164. try:
  165. if os.path.islink(srcname):
  166. linkto = os.readlink(srcname)
  167. os.symlink(linkto, dstname)
  168. elif os.path.isdir(srcname):
  169. copySubTree(srcname, dstname, regFilter)
  170. elif regFilter.match(srcname):
  171. shutil.copy2(srcname, dstname)
  172. crystalStorage.append(dstname)
  173. except (IOError, os.error), why:
  174. continue
  175. def execCmd(program, args):
  176. p = os.popen(program + " " + args)
  177. p.close()
  178. def generateListOfFiles():
  179. """
  180. Create a ghost in ./local/crystal/current and /local/crystal/analyzed
  181. And run the SwA tool
  182. """
  183. regScripts = re.compile(r'(.*).' + crystalExtension + '$', re.I)
  184. copySubTree(crystalFiles, 'local/crystal/current', regScripts)
  185. print "Running the static analysis tool..."
  186. for file in crystalStorage:
  187. fileIn = os.path.abspath(os.path.join('./', file))
  188. fileOut = os.path.abspath(os.path.join('./', file.replace('current', 'analyzed')))
  189. cmdLine = crystalAnalyzerInputParam + " " + fileIn + " " + crystalAnalyzerOutputParam + " " + fileOut
  190. # execCmd(crystalAnalyzerBin, cmdLine)
  191. print crystalAnalyzerBin,cmdLine
  192. os.system(crystalAnalyzerBin +" "+ cmdLine)
  193. def stripNoneASCII(output):
  194. # should be somepthing to do that.. :/
  195. newOutput = ""
  196. for s in output:
  197. try:
  198. s = s.encode()
  199. newOutput += s
  200. except UnicodeDecodeError:
  201. continue
  202. return newOutput
  203. def isPatternInFile(fileName):
  204. global crystalDatabase
  205. file = None
  206. try:
  207. file = open(fileName, 'r')
  208. except IOError:
  209. print "Crystal: Cannot open the file [%s]" % fileName
  210. return False
  211. inZone, inLined = False, False
  212. detectPattern = False
  213. lineNumber = 0
  214. shortName = fileName[fileName.rfind('analyzed') + 9 : ]
  215. vulnName = ""
  216. for l in file.readlines():
  217. lineNumber += 1
  218. l = l.replace('\n','')
  219. try:
  220. """
  221. Check for the regular expression patterns
  222. if len(crystalRegExpPatterns) > 0:
  223. for modules in crystalRegExpPatterns:
  224. for regexp in crystalRegExpPatterns[modules]:
  225. if regexp.match()
  226. """
  227. if len(vulnName) > 0 and (detectPattern and not inZone or inLined):
  228. # creating the nice structure
  229. # { 'index.php' : {'xss' : {'12', 'echo $_GET["plop"]'}}}
  230. if shortName not in crystalDatabase:
  231. crystalDatabase[shortName] = {}
  232. if vulnName not in crystalDatabase[shortName]:
  233. crystalDatabase[shortName][vulnName] = {}
  234. if str(lineNumber) not in crystalDatabase[shortName][vulnName]:
  235. crystalDatabase[shortName][vulnName][str(lineNumber)] = l
  236. detectPattern = False
  237. inLined = False
  238. vulnName = ""
  239. if l.count(crystalCheckStart) > 0 and not inZone:
  240. b1 = l.find(crystalCheckStart)
  241. inZone = True
  242. # same line for start and end ?
  243. if l.count(crystalCheckEnd) > 0:
  244. b2 = l.find(crystalCheckStart)
  245. if b1 < b2:
  246. inZone = False
  247. position = l.lower().find(pattern.lower())
  248. if b1 < position and position < b2:
  249. detectPattern = True
  250. inLined = True
  251. elif inZone:
  252. # is there any pattern around the corner ?
  253. for modules in crystalPatterns:
  254. for p in crystalPatterns[modules]:
  255. p = p.lower()
  256. l = l.lower()
  257. # The folowing code is stupid!
  258. # I have to change the algorithm for the __AND__ parsing...
  259. if '__AND__' in p:
  260. listPatterns = p.split('__AND__')
  261. isIn = True
  262. for patton in listPatterns:
  263. if patton not in l:
  264. isIn = isIn and False
  265. if isIn:
  266. detectPattern = True
  267. vulnName = modules
  268. a=0
  269. else:
  270. # test if the simple pattern is in the line
  271. if p in l:
  272. detectPattern = True
  273. vulnName = modules
  274. if l.count(crystalCheckEnd) > 0:
  275. inZone = False
  276. except UnicodeDecodeError:
  277. continue
  278. return True
  279. def buildDatabase():
  280. """
  281. Read the analzed files (indirectly with the crystalStorage.replace('current','analyzed') )
  282. And look for the patterns
  283. """
  284. listOut = []
  285. for file in crystalStorage:
  286. fileOut = os.path.abspath(os.path.join('./', file.replace('current', 'analyzed')))
  287. if not isPatternInFile(fileOut):
  288. print "Error with the file [%s]" % file
  289. def createStructure():
  290. """
  291. Create the structure in the ./local directory
  292. """
  293. try:
  294. os.mkdir("local/crystal/")
  295. except OSError,e :
  296. a=0
  297. try:
  298. os.mkdir("local/crystal/current")
  299. except OSError,e :
  300. a=0
  301. try:
  302. os.mkdir("local/crystal/analyzed")
  303. except OSError,e :
  304. a=0
  305. """
  306. def realLineNumberReverse(fileName, codeStr):
  307. print fileName, codeStr
  308. try:
  309. fN = os.path.abspath(os.path.join('./local/crystal/current/', fileName))
  310. file = open(fN, 'r')
  311. lineNumber = 0
  312. for a in file.readlines():
  313. lineNumber += 1
  314. if codeStr in a:
  315. print a
  316. file.close()
  317. return lineNumber
  318. file.close()
  319. except IOError,e:
  320. print e
  321. return 0
  322. return 0
  323. """
  324. def generateReport_1():
  325. """
  326. Create a first report like:
  327. * Developer report:
  328. # using XSLT...
  329. <site>
  330. <file name="index.php">
  331. <vulnerability line="9">xss</vulnerability>
  332. <vulnerability line="25">sql</vulnerability>
  333. </file>
  334. ...
  335. </site>
  336. * Security report:
  337. <site>
  338. <vulnerability name="xss">
  339. <file name="index.php" line="9" />
  340. ...
  341. </vulnerabilty>
  342. <vulnerability name="sql">
  343. <file name="index.php" line="25" />
  344. </vulnerabilty>
  345. </site>
  346. """
  347. plop = open('results/crystal_SecurityReport_Grabber.xml','w')
  348. plop.write("<crystal>\n")
  349. plop.write("<site>\n")
  350. plop.write("<!-- The line numbers are from the files in the 'analyzed' directory -->\n")
  351. for file in crystalDatabase:
  352. plop.write("\t<file name='%s'>\n" % file)
  353. for vuln in crystalDatabase[file]:
  354. for line in crystalDatabase[file][vuln]:
  355. # lineNumber = realLineNumberReverse(file,crystalDatabase[file][vuln][line])
  356. localVuln = vuln
  357. if localVuln in vulnToDescritiveNames:
  358. localVuln = vulnToDescritiveNames[localVuln]
  359. plop.write("\t\t<vulnerability name='%s' line='%s' >%s</vulnerability>\n" % (localVuln, line, htmlencode(crystalDatabase[file][vuln][line])))
  360. plop.write("\t</file>\n")
  361. plop.write("</site>\n")
  362. plop.write("</crystal>\n")
  363. plop.close()
  364. def buildUrlKey(file):
  365. fileName = file.replace('\\','/') # on windows...
  366. keyUrl = crystalUrl
  367. if keyUrl[len(keyUrl)-1] != '/' and fileName[0] != '/':
  368. keyUrl += '/'
  369. keyUrl += fileName
  370. return keyUrl
  371. reParamPOST = re.compile(r'(.*)\$_POST\[(.+)\](.*)',re.I)
  372. reParamGET = re.compile(r'(.*)\$_GET\[(.+)\](.*)' ,re.I)
  373. def getSimpleParamFromCode_GET(code):
  374. """
  375. Using the regular expression above, try to get some parameters name
  376. """
  377. params = [] # we can have multiple params...
  378. code = code.replace("'",'');
  379. code = code.replace('"','');
  380. if code.lower().count('get') > 0:
  381. # try to match the $_GET
  382. if reParamGET.match(code):
  383. out = reParamGET.search(code)
  384. params.append(out.group(2))
  385. params.append(getSimpleParamFromCode_GET(out.group(3)))
  386. params = flatten(params)
  387. return params
  388. def getSimpleParamFromCode_POST(code):
  389. """
  390. Using the regular expression above, try to get some parameters name
  391. """
  392. params = [] # we can have multiple params...
  393. code = code.replace("'",'');
  394. code = code.replace('"','');
  395. if code.lower().count('post') > 0:
  396. # try to match the $_GET
  397. if reParamPOST.match(code):
  398. out = reParamPOST.search(code)
  399. params.append(out.group(2))
  400. params.append(getSimpleParamFromCode_POST(out.group(3)))
  401. params = flatten(params)
  402. return params
  403. def createClassicalDatabase(vulnsType, localCrystalDB):
  404. """
  405. From the crystalDatabase, generate the same database as in Spider
  406. This is generated for calling the differents modules
  407. ClassicalDB = { url : { 'GET' : { param : value } } }
  408. """
  409. classicalDB = {}
  410. for file in localCrystalDB:
  411. # build the URL
  412. keyUrl = buildUrlKey(file)
  413. if keyUrl not in classicalDB:
  414. classicalDB[keyUrl] = {'GET' : {}, 'POST' : {}}
  415. for vuln in localCrystalDB[file]:
  416. # only get the kind of vulnerability we want
  417. if vuln != vulnsType:
  418. continue
  419. for line in localCrystalDB[file][vuln]:
  420. code = localCrystalDB[file][vuln][line]
  421. # try to extract some data...
  422. params_GET = getSimpleParamFromCode_GET (code)
  423. params_POST = getSimpleParamFromCode_POST(code)
  424. if len(params_GET) > 0:
  425. for p in params_GET:
  426. lG = classicalDB[keyUrl]['GET']
  427. if p not in classicalDB[keyUrl]['GET']:
  428. lG = dict_add(lG,{p:''})
  429. classicalDB[keyUrl]['GET'] = lG
  430. if len(params_POST) > 0:
  431. for p in params_POST:
  432. lP = classicalDB[keyUrl]['POST']
  433. if p not in classicalDB[keyUrl]['POST']:
  434. lP = dict_add(lP,{p:''})
  435. classicalDB[keyUrl]['POST'] = lP
  436. return classicalDB
  437. def retrieveVulnList():
  438. vulnList = []
  439. for file in crystalDatabase:
  440. for vuln in crystalDatabase[file]:
  441. if vuln not in vulnList:
  442. vulnList.append(vuln)
  443. return vulnList
  444. def process(urlGlobal, localDB, attack_list):
  445. """
  446. Crystal Module entry point
  447. """
  448. print "Crystal Module Start"
  449. try:
  450. f = open("crystal.conf.xml", 'r')
  451. f.close()
  452. except IOError:
  453. print "The crystal module needs the 'crystal.conf.xml' configuration file."
  454. sys.exit(1)
  455. parser = make_parser()
  456. crystal_handler = CrystalConfHandler()
  457. # Tell the parser to use our handler
  458. parser.setContentHandler(crystal_handler)
  459. try:
  460. parser.parse("crystal.conf.xml")
  461. except KeyError, e:
  462. print e
  463. sys.exit(1)
  464. #---------- White box testing
  465. createStructure()
  466. generateListOfFiles()
  467. buildDatabase()
  468. print "Build first report: List of vulneratilities and places in the code"
  469. generateReport_1()
  470. #---------- Start the Black Box testing
  471. # need to create a classical database like, so losing information
  472. # but for a type of vulnerability
  473. listVulns = retrieveVulnList()
  474. for vulns in listVulns:
  475. localDatabase = createClassicalDatabase(vulns, crystalDatabase)
  476. setDatabase(localDatabase)
  477. print "inProcess Crystal DB = ", localDatabase
  478. # print vulns, database
  479. # Call the Black Box Module
  480. print "Scan for ", vulns
  481. investigate(crystalUrl, vulns)
  482. print "Crystal Module Stop"