An IRC bot which monitors for compromised embedded devices being used as proxies

antissh.py 4.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. #!/usr/bin/env python3
  2. # dependencies: asyncssh, asyncio-irc
  3. import asyncio
  4. import asyncssh
  5. import sys
  6. import re
  7. import aiohttp
  8. import json
  9. from asyncirc import irc
  10. from configparser import ConfigParser
  11. import logging
  12. config = ConfigParser()
  13. config.read(sys.argv[1])
  14. TARGET_IP = config.get('target', 'ip', fallback='162.220.112.99')
  15. TARGET_PORT = config.getint('target', 'port', fallback=6667)
  16. QUICK_MODE = config.getboolean('target', 'quick_mode', fallback=False)
  17. HOST = config.get('host', 'hostname', fallback='irc.dereferenced.org')
  18. PORT = config.getint('host', 'port', fallback=6667)
  19. USE_SSL = config.getboolean('host', 'ssl', fallback=False)
  20. OPER = config.get('host', 'oper', fallback='x x')
  21. NICKNAME = config.get('host', 'nickname', fallback='antissh')
  22. MODES = config.get('host', 'modes', fallback='')
  23. KLINE_CMD_TEMPLATE = config.get('host', 'kline_cmd', fallback='KLINE 86400 *@{ip} :Vulnerable SSH daemon found on this host. Please fix your SSH daemon and try again later.\r\n')
  24. # advanced users only
  25. # charybdis uses:
  26. # *** Notice -- Client connecting: kaniini_ (~kaniini@127.0.0.1) [127.0.0.1] {users} [William Pitcock]
  27. # re.findall(r'\[[0-9a-f\.:]+\]', message)
  28. IP_REGEX = re.compile(r'Client connecting\:.*\[([0-9a-f\.:]+)\]')
  29. POSITIVE_HIT_STRING = b'Looking up your hostname'
  30. DEFAULT_CREDENTIALS = [
  31. ('ADMIN', 'ADMIN'),
  32. ('admin', '123456'),
  33. ('admin', ''),
  34. ('root', '')
  35. ]
  36. # dnsbl settings
  37. dronebl_key = config.get('dnsbl', 'dronebl_key', fallback=None)
  38. dnsbl_im_key = config.get('dnsbl', 'dnsbl_im_key', fallback=None)
  39. dnsbl_active = (dronebl_key is not None or dnsbl_im_key is not None)
  40. async def submit_dronebl(ip):
  41. add_stanza = '<add ip="{ip}" type="15" port="22" comment="{comment}" />'.format(
  42. ip=ip, comment='A vulnerable SSH server on an IOT gateway, detected by antissh.')
  43. envelope = '<?xml version="1.0"?><request key="{key}">{stanza}</request>'.format(
  44. key=dronebl_key, stanza=add_stanza)
  45. headers = {
  46. 'Content-Type': 'text/xml'
  47. }
  48. await aiohttp.post('https://dronebl.org/rpc2', headers=headers, data=envelope)
  49. async def submit_dnsbl_im(ip):
  50. envelope = {
  51. 'key': dnsbl_im_key,
  52. 'addresses': [{
  53. 'ip': ip,
  54. 'type': '4',
  55. 'reason': 'A vulnerable SSH server on an IOT gateway, detected by antissh.'
  56. }]
  57. }
  58. headers = {
  59. 'Content-Type': 'application/json'
  60. }
  61. await aiohttp.post('https://api.dnsbl.im/import', headers=headers, data=json.dumps(envelope))
  62. async def check_with_credentials(ip, target_ip, target_port, username, password):
  63. """Checks whether a given username or password works to open a direct TCP session."""
  64. try:
  65. async with asyncssh.connect(ip, username=username, password=password, known_hosts=None) as conn:
  66. if QUICK_MODE:
  67. return True
  68. try:
  69. reader, writer = await conn.open_connection(target_ip, target_port)
  70. except asyncssh.Error:
  71. return False
  72. writer.write(b'\r\n')
  73. writer.write_eof()
  74. response = await reader.read()
  75. return POSITIVE_HIT_STRING in response
  76. except (asyncssh.Error, OSError):
  77. return False
  78. async def check_with_credentials_group(ip, target_ip, target_port, credentials_group=DEFAULT_CREDENTIALS):
  79. futures = [check_with_credentials(ip, target_ip, target_port, c[0], c[1]) for c in credentials_group]
  80. results = await asyncio.gather(*futures)
  81. return True in results
  82. async def check_connecting_client(bot, ip):
  83. result = await check_with_credentials_group(ip, TARGET_IP, TARGET_PORT)
  84. if result:
  85. print('found vulnerable SSH daemon at', ip)
  86. bot.writeln(KLINE_CMD_TEMPLATE.format(ip=ip))
  87. if dnsbl_active:
  88. tasks = []
  89. if dronebl_key: tasks += [submit_dronebl(ip)]
  90. if dnsbl_im_key: tasks += [submit_dnsbl_im(ip)]
  91. await asyncio.wait(tasks)
  92. def main():
  93. logging.basicConfig(level=logging.DEBUG)
  94. bot = irc.connect(HOST, PORT, use_ssl=USE_SSL)
  95. bot.register(NICKNAME, "antissh", "antissh proxy checking bot")
  96. @bot.on('irc-001')
  97. def handle_connection_start(message):
  98. bot.writeln("OPER {}\r\n".format(OPER))
  99. if MODES:
  100. bot.writeln("MODE {0} {1}\r\n".format(NICKNAME, MODES))
  101. @bot.on('notice')
  102. def handle_connection_notice(message, user, target, text):
  103. if 'connecting' not in text:
  104. return
  105. match = IP_REGEX.search(text)
  106. if match:
  107. ip = match.group(1)
  108. asyncio.ensure_future(check_connecting_client(bot, ip))
  109. asyncio.get_event_loop().run_forever()
  110. if __name__ == '__main__':
  111. main()