├── LICENSE.md ├── README.md ├── lazyScanner.py ├── linuxScanner.py └── scanModules ├── __init__.py ├── centosDetect.py ├── debianDetect.py ├── linuxDetect.py ├── nixDetect.py └── osDetect.py /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 videns 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vulners-scanner 2 | # Description 3 | PoC of a host-based vulnerability scanner, which uses vulners.com API. Detects operating system, collects installed packages and checks vulnerabilities in it. 4 | # Supported OS 5 | Currently support collecting packages for these operating systems: 6 | * Debian-based (debian, kali, ubuntu) 7 | * Rhel-based (redhat, centos, fedora) 8 | 9 | # Python version 10 | Lazy and Advanced versions were tested on a python2.6, python2.7, python3.5. If you found any bugs, don't hesitate to open issue 11 | 12 | # Docker support 13 | Experimental support of detecting vulnerabilities in running docker containers (only advanced script). Need to activate it changing `checkDocker=False` to `checkDocker=True` in linuxScanner.py 14 | 15 | # How to use 16 | 17 | * Lazy scanner 18 | The simplest script to show vulners.com API capabilities. Just run script and it will return all found vulnerabilities: 19 | ``` 20 | # git clone https://github.com/videns/vulners-scanner 21 | # cd vulners-scanner 22 | # ./lazyScanner.py 23 | OS Name - debian, OS Version - 8 24 | Total provided packages: 315 25 | { 26 | "data": { 27 | "vulnerabilities": [ 28 | "DSA-3644", 29 | "DSA-3626" 30 | ], 31 | "packages": { 32 | "openssh-client 1:6.7p1-5+deb8u2 amd64": { 33 | "DSA-3626": [ 34 | { 35 | "bulletinVersion": "1:6.7p1-5+deb8u3", 36 | "providedVersion": "1:6.7p1-5+deb8u2", 37 | "bulletinPackage": "openssh-client_1:6.7p1-5+deb8u3_all.deb", 38 | "result": true, 39 | "operator": "lt", 40 | "OSVersion": "8", 41 | "providedPackage": "openssh-client 1:6.7p1-5+deb8u2 amd64" 42 | } 43 | ] 44 | } 45 | "fontconfig-config 2.11.0-6.3 all": { 46 | "DSA-3644": [ 47 | { 48 | "bulletinVersion": "2.11.0-6.3+deb8u1", 49 | "providedVersion": "2.11.0-6.3", 50 | "bulletinPackage": "fontconfig-config_2.11.0-6.3+deb8u1_all.deb", 51 | "result": true, 52 | "operator": "lt", 53 | "OSVersion": "8", 54 | "providedPackage": "fontconfig-config 2.11.0-6.3 all" 55 | } 56 | ] 57 | }, 58 | "libfontconfig1 2.11.0-6.3 amd64": { 59 | "DSA-3644": [ 60 | { 61 | "bulletinVersion": "2.11.0-6.3+deb8u1", 62 | "providedVersion": "2.11.0-6.3", 63 | "bulletinPackage": "libfontconfig1_2.11.0-6.3+deb8u1_all.deb", 64 | "result": true, 65 | "operator": "lt", 66 | "OSVersion": "8", 67 | "providedPackage": "libfontconfig1 2.11.0-6.3 amd64" 68 | } 69 | ] 70 | } 71 | } 72 | }, 73 | "result": "OK" 74 | } 75 | Vulnerabilities: 76 | DSA-3644 77 | DSA-3626 78 | ``` 79 | 80 | * Advanced scanner. 81 | Detect OS in a several ways. Supports running docker containers scan (need to activate manually in a file) 82 | ``` 83 | # git clone https://github.com/videns/vulners-scanner 84 | # cd vulners-scanner 85 | # ./linuxScanner.py 86 | 87 | _ 88 | __ ___ _| |_ __ ___ _ __ ___ 89 | \ \ / / | | | | '_ \ / _ \ '__/ __| 90 | \ V /| |_| | | | | | __/ | \__ \ 91 | \_/ \__,_|_|_| |_|\___|_| |___/ 92 | 93 | ========================================== 94 | Host info - Host machine 95 | OS Name - Darwin, OS Version - 15.6.0 96 | Total found packages: 0 97 | ========================================== 98 | Host info - docker container "java:8-jre" 99 | OS Name - debian, OS Version - 8 100 | Total found packages: 166 101 | Vulnerable packages: 102 | libgcrypt20 1.6.3-2+deb8u1 amd64 103 | DSA-3650 - 'libgcrypt20 -- security update', cvss.score - 0.0 104 | libexpat1 2.1.0-6+deb8u2 amd64 105 | DSA-3597 - 'expat -- security update', cvss.score - 7.8 106 | perl-base 5.20.2-3+deb8u4 amd64 107 | DSA-3628 - 'perl -- security update', cvss.score - 0.0 108 | gnupg 1.4.18-7+deb8u1 amd64 109 | DSA-3649 - 'gnupg -- security update', cvss.score - 0.0 110 | gpgv 1.4.18-7+deb8u1 amd64 111 | DSA-3649 - 'gnupg -- security update', cvss.score - 0.0 112 | ``` 113 | 114 | -------------------------------------------------------------------------------- /lazyScanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = 'videns' 5 | try: 6 | import urllib.request as urllib2 7 | except ImportError: 8 | import urllib2 9 | try: 10 | from subprocess import DEVNULL # py3k 11 | except ImportError: 12 | import os 13 | DEVNULL = open(os.devnull, 'wb') 14 | import json 15 | import re 16 | import subprocess 17 | 18 | VULNERS_LINKS = {'pkgChecker':'https://vulners.com/api/v3/audit/audit/', 19 | 'bulletin':'https://vulners.com/api/v3/search/id/?id=%s'} 20 | 21 | 22 | class LazyScanner(): 23 | def __init__(self): 24 | pass 25 | 26 | def sshCommand(self,cmd): 27 | cmdResult = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=DEVNULL, shell=True).communicate()[0] 28 | if isinstance(cmdResult, bytes): 29 | cmdResult = cmdResult.decode('utf8') 30 | return cmdResult 31 | 32 | 33 | 34 | def getOSInfo(self): 35 | version = self.sshCommand("cat /etc/os-release") 36 | if version: 37 | reFamily = re.search("^ID=\"?(\w+)\"?",version,re.MULTILINE) 38 | if reFamily: 39 | osFamily = reFamily.group(1).lower() 40 | else: 41 | return 42 | 43 | reVersion = re.search("^VERSION_ID=\"?(\w+)(.\w+)?\"?",version,re.MULTILINE) 44 | if reVersion: 45 | osVersion = ''.join([s.lower() for s in reVersion.groups()]) 46 | else: 47 | return 48 | return (osFamily, osVersion) 49 | 50 | def getPackages(self, osName): 51 | if osName in ('debian','ubuntu', 'kali', 'linuxmint'): 52 | cmd = "dpkg-query -W -f='${Package} ${Version} ${Architecture}\n'" 53 | elif osName in ('rhel', 'centos', 'oraclelinux', 'suse', 'fedora'): 54 | cmd = "rpm -qa" 55 | else: 56 | cmd = None 57 | return self.sshCommand(cmd).splitlines() if cmd else None 58 | 59 | 60 | def auditSystem(self): 61 | osInfo = self.getOSInfo() 62 | if not osInfo: 63 | print("Can't detect OS, try linuxScanner.py instead") 64 | return 65 | print("OS Name - %s, OS Version - %s" % (osInfo[0], osInfo[1])) 66 | 67 | installedPackages = self.getPackages(osInfo[0]) 68 | if not installedPackages: 69 | print("Couldn't find packages") 70 | return 71 | 72 | print("Total provided packages: %s" % len(installedPackages)) 73 | # Get vulnerability information 74 | payload = {'os':osInfo[0], 75 | 'version':osInfo[1], 76 | 'package':installedPackages} 77 | req = urllib2.Request(VULNERS_LINKS.get('pkgChecker')) 78 | req.add_header('Content-Type', 'application/json') 79 | response = urllib2.urlopen(req, json.dumps(payload).encode('utf-8')) 80 | responseData = response.read() 81 | if isinstance(responseData, bytes): 82 | responseData = responseData.decode('utf8') 83 | responseData = json.loads(responseData) 84 | resultCode = responseData.get("result") 85 | if resultCode == "OK": 86 | print(json.dumps(responseData, indent=4)) 87 | print("Vulnerabilities:\n%s" % "\n".join(responseData.get('data').get('vulnerabilities'))) 88 | else: 89 | print("Error - %s" % responseData.get('data').get('error')) 90 | return 91 | 92 | if __name__ == "__main__": 93 | scanner = LazyScanner() 94 | scanner.auditSystem() 95 | -------------------------------------------------------------------------------- /linuxScanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __author__ = 'videns' 4 | import inspect 5 | import pkgutil 6 | import json 7 | import os 8 | try: 9 | import urllib.request as urllib2 10 | except ImportError: 11 | import urllib2 12 | import scanModules 13 | 14 | 15 | VULNERS_LINKS = {'pkgChecker':'https://vulners.com/api/v3/audit/audit/', 16 | 'bulletin':'https://vulners.com/api/v3/search/id/'} 17 | 18 | VULNERS_ASCII = r""" 19 | _ 20 | __ ___ _| |_ __ ___ _ __ ___ 21 | \ \ / / | | | | '_ \ / _ \ '__/ __| 22 | \ V /| |_| | | | | | __/ | \__ \ 23 | \_/ \__,_|_|_| |_|\___|_| |___/ 24 | 25 | """ 26 | 27 | 28 | class scannerEngine(): 29 | def __init__(self): 30 | self.osInstanceClasses = self.getInstanceClasses() 31 | 32 | def getInstanceClasses(self): 33 | self.detectors = None 34 | members = set() 35 | for modPath, modName, isPkg in pkgutil.iter_modules(scanModules.__path__): 36 | #find all classed inherited from scanner.osDetect.ScannerInterface in all files 37 | members = members.union(inspect.getmembers(__import__('%s.%s' % ('scanModules',modName), fromlist=['scanModules']), 38 | lambda member:inspect.isclass(member) 39 | and issubclass(member, scanModules.osDetect.ScannerInterface) 40 | and member.__module__ == '%s.%s' % ('scanModules',modName) 41 | and member != scanModules.osDetect.ScannerInterface)) 42 | return members 43 | 44 | def getInstance(self,sshPrefix): 45 | inited = [instance[1](sshPrefix) for instance in self.osInstanceClasses] 46 | if not inited: 47 | raise Exception("No OS Detection classes found") 48 | osInstance = max(inited, key=lambda x:x.osDetectionWeight) 49 | if osInstance.osDetectionWeight: 50 | return osInstance 51 | 52 | def sendVulnRequest(self, url, payload): 53 | req = urllib2.Request(url) 54 | req.add_header('Content-Type', 'application/json') 55 | response = urllib2.urlopen(req, json.dumps(payload).encode('utf-8')) 56 | responseData = response.read() 57 | if isinstance(responseData, bytes): 58 | responseData = responseData.decode('utf8') 59 | responseData = json.loads(responseData) 60 | return responseData 61 | 62 | def auditSystem(self, sshPrefix, systemInfo=None): 63 | instance = self.getInstance(sshPrefix) 64 | installedPackages = instance.getPkg() 65 | print("="*42) 66 | if systemInfo: 67 | print("Host info - %s" % systemInfo) 68 | print("OS Name - %s, OS Version - %s" % (instance.osFamily, instance.osVersion)) 69 | print("Total found packages: %s" % len(installedPackages)) 70 | if not installedPackages: 71 | return instance 72 | # Get vulnerability information 73 | payload = {'os':instance.osFamily, 74 | 'version':instance.osVersion, 75 | 'package':installedPackages} 76 | url = VULNERS_LINKS.get('pkgChecker') 77 | response = self.sendVulnRequest(url, payload) 78 | resultCode = response.get("result") 79 | if resultCode != "OK": 80 | print("Error - %s" % response.get('data').get('error')) 81 | else: 82 | vulnsFound = response.get('data').get('vulnerabilities') 83 | if not vulnsFound: 84 | print("No vulnerabilities found") 85 | else: 86 | payload = {'id':vulnsFound} 87 | allVulnsInfo = self.sendVulnRequest(VULNERS_LINKS['bulletin'], payload) 88 | vulnInfoFound = allVulnsInfo['result'] == 'OK' 89 | print("Vulnerable packages:") 90 | for package in response['data']['packages']: 91 | print(" "*4 + package) 92 | packageVulns = [] 93 | for vulns in response['data']['packages'][package]: 94 | if vulnInfoFound: 95 | vulnInfo = "{id} - '{title}', cvss.score - {score}".format(id=vulns, 96 | title=allVulnsInfo['data']['documents'][vulns]['title'], 97 | score=allVulnsInfo['data']['documents'][vulns]['cvss']['score']) 98 | packageVulns.append((vulnInfo,allVulnsInfo['data']['documents'][vulns]['cvss']['score'])) 99 | else: 100 | packageVulns.append((vulns,0)) 101 | packageVulns = sorted(packageVulns, key=lambda x:x[1]) 102 | packageVulns = [" "*8 + x[0] for x in packageVulns] 103 | print("\n".join(packageVulns)) 104 | 105 | return instance 106 | 107 | def scan(self, checkDocker = False): 108 | #scan host machine 109 | hostInstance = self.auditSystem(sshPrefix=None,systemInfo="Host machine") 110 | #scan dockers 111 | if checkDocker: 112 | containers = hostInstance.sshCommand("docker ps") 113 | if containers: 114 | containers = containers.splitlines()[1:] 115 | dockers = [(line.split()[0], line.split()[1]) for line in containers] 116 | for (dockerID, dockerImage) in dockers: 117 | sshPrefix = "docker exec %s" % dockerID 118 | self.auditSystem(sshPrefix, "docker container \"%s\"" % dockerImage) 119 | 120 | 121 | if __name__ == "__main__": 122 | print('\n'.join(VULNERS_ASCII.splitlines())) 123 | scannerInstance = scannerEngine() 124 | scannerInstance.scan(checkDocker=False) 125 | -------------------------------------------------------------------------------- /scanModules/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'videns' 3 | from os.path import dirname, basename, isfile 4 | import glob 5 | modules = glob.glob(dirname(__file__)+"/*.py") 6 | __all__ = [ basename(f)[:-3] for f in modules if isfile(f)] -------------------------------------------------------------------------------- /scanModules/centosDetect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'videns' 3 | import re 4 | 5 | from scanModules.linuxDetect import linuxDetect 6 | 7 | 8 | class rpmBasedDetect(linuxDetect): 9 | def __init__(self,sshPrefix): 10 | self.supportedFamilies = ('redhat', 'centos', 'oraclelinux', 'suse', 'fedora') 11 | super(rpmBasedDetect, self).__init__(sshPrefix) 12 | 13 | def osDetect(self): 14 | osDetection = super(rpmBasedDetect, self).osDetect() 15 | if osDetection: 16 | (osVersion, osFamily, osDetectionWeight) = osDetection 17 | 18 | if osFamily in self.supportedFamilies: 19 | osDetectionWeight = 60 20 | return (osVersion, osFamily, osDetectionWeight) 21 | 22 | version = self.sshCommand("cat /etc/centos-release") 23 | if version: 24 | osVersion = re.search("\s+(\d+)\.",version).group(1) 25 | osFamily = "centos" 26 | osDetectionWeight = 70 27 | return (osVersion, osFamily, osDetectionWeight) 28 | 29 | version = self.sshCommand("cat /etc/redhat-release") 30 | if version: 31 | osVersion = re.search("\s+(\d+)\.",version).group(1) 32 | osFamily = "rhel" 33 | osDetectionWeight = 60 34 | return (osVersion, osFamily, osDetectionWeight) 35 | 36 | 37 | def getPkg(self): 38 | pkgList = self.sshCommand("rpm -qa") 39 | return pkgList.splitlines() 40 | 41 | -------------------------------------------------------------------------------- /scanModules/debianDetect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'videns' 3 | import re 4 | 5 | from scanModules.linuxDetect import linuxDetect 6 | 7 | 8 | class debBasedDetect(linuxDetect): 9 | def __init__(self,sshPrefix): 10 | self.supportedFamilies = ('debian','ubuntu', 'kali') 11 | self.debCodenames = {'stretch':'9', 12 | 'jessie':'8', 13 | 'wheezy':'7', 14 | 'squeeze':'6', 15 | 'lenny':'5', 16 | 'etch':'4', 17 | 'sarge':'3.1', 18 | 'woody':'3.0', 19 | 'potato':'2.2', 20 | 'slink':'2.1', 21 | 'hamm':'2.0'} 22 | super(debBasedDetect, self).__init__(sshPrefix) 23 | 24 | def osDetect(self): 25 | osDetection = super(debBasedDetect, self).osDetect() 26 | if osDetection: 27 | (osVersion, osFamily, osDetectionWeight) = osDetection 28 | 29 | if osFamily in self.supportedFamilies: 30 | osDetectionWeight = 60 31 | return (osVersion, osFamily, osDetectionWeight) 32 | 33 | version = self.sshCommand("cat /etc/debian_version") 34 | if version and re.match(r"^[\d\.]+$",version): 35 | osVersion = version 36 | osFamily = "debian" 37 | osDetectionWeight = 60 38 | return (osVersion, osFamily, osDetectionWeight) 39 | elif version and re.match(r"^\w+/\w+",version): 40 | osCodename = re.search(r"^(\w+)/",version).group(1).lower() 41 | if osCodename in self.debCodenames: 42 | osVersion = self.debCodenames[osCodename] 43 | osFamily = "debian" 44 | osDetectionWeight = 60 45 | return (osVersion, osFamily, osDetectionWeight) 46 | 47 | version = self.sshCommand("cat /etc/lsb-release") 48 | if version: 49 | mID = re.search("^DISTRIB_ID=\"?(.*?)\"?",version,re.MULTILINE) 50 | mVer = re.search("^DISTRIB_RELEASE=\"?(.*?)\"?", version, re.MULTILINE) 51 | if mID and mVer: 52 | osFamily = mID.group(1).lower() 53 | osVersion = mVer.group(1).lower() 54 | osDetectionWeight = 60 55 | return (osVersion, osFamily, osDetectionWeight) 56 | 57 | 58 | 59 | def getPkg(self): 60 | pkgList = self.sshCommand("dpkg-query -W -f='${Package} ${Version} ${Architecture}\n'") 61 | return pkgList.splitlines() 62 | 63 | 64 | -------------------------------------------------------------------------------- /scanModules/linuxDetect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'videns' 3 | import re 4 | 5 | from scanModules.osDetect import ScannerInterface 6 | 7 | 8 | class linuxDetect(ScannerInterface): 9 | def osDetect(self): 10 | version = self.sshCommand("cat /etc/os-release") 11 | if version: 12 | reFamily = re.search(r"^ID=(.*)", version, re.MULTILINE) 13 | if reFamily: 14 | osFamily = reFamily.group(1).lower().strip('"') 15 | else: 16 | return 17 | 18 | reVersion = re.search("^VERSION_ID=(.*)", version, re.MULTILINE) 19 | if reVersion: 20 | osVersion = reVersion.group(1).lower().strip('"') 21 | else: 22 | return 23 | 24 | osDetectionWeight = 50 25 | return (osVersion, osFamily, osDetectionWeight) 26 | 27 | -------------------------------------------------------------------------------- /scanModules/nixDetect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'videns' 3 | from scanModules.osDetect import ScannerInterface 4 | 5 | 6 | class nixDetect(ScannerInterface): 7 | def osDetect(self): 8 | osFamily = self.sshCommand("uname -s") 9 | osVersion = self.sshCommand("uname -r") 10 | if osFamily and osVersion: 11 | osDetectionWeight = 10 12 | return (osVersion, osFamily, osDetectionWeight) 13 | 14 | -------------------------------------------------------------------------------- /scanModules/osDetect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __author__ = 'videns' 3 | import subprocess 4 | import uuid 5 | import re 6 | try: 7 | from subprocess import DEVNULL # py3k 8 | except ImportError: 9 | import os 10 | DEVNULL = open(os.devnull, 'wb') 11 | 12 | class ScannerInterface(object): 13 | def __init__(self, sshPrefix): 14 | self.osVersion = None 15 | self.osFamily = None 16 | self.osDetectionWeight = 0 17 | self.sshPrefix = sshPrefix 18 | osDetection = self.osDetect() 19 | if osDetection is not None: 20 | (self.osVersion, self.osFamily, self.osDetectionWeight) = osDetection 21 | 22 | def sshCommand(self, command): 23 | if self.sshPrefix: 24 | command = "%s %s" % (self.sshPrefix, command) 25 | randPre = str(uuid.uuid4()).split('-')[0] 26 | randAfter = str(uuid.uuid4()).split('-')[0] 27 | randFail = str(uuid.uuid4()).split('-')[0] 28 | command = "echo %s; %s; echo %s || echo %s" % (randPre, command, randAfter, randFail) 29 | cmdResult = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=DEVNULL, shell=True).communicate()[0] 30 | if isinstance(cmdResult, bytes): 31 | cmdResult = cmdResult.decode('utf8') 32 | if randFail in cmdResult: 33 | return None 34 | else: 35 | resMatch = re.search(r"%s\n(.*)\n%s" % (randPre, randAfter), cmdResult, re.DOTALL) 36 | if resMatch: 37 | return resMatch.group(1) 38 | else: 39 | return None 40 | 41 | def osDetect(self): 42 | return None 43 | 44 | def getPkg(self): 45 | return [] --------------------------------------------------------------------------------