Skip to content

Commit

Permalink
Remote Desktop Protocol (RDP) matcher: allow regexp matching
Browse files Browse the repository at this point in the history
  • Loading branch information
vnxme committed Jul 22, 2024
1 parent cf0b618 commit ed36f3f
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 7 deletions.
70 changes: 63 additions & 7 deletions modules/l4rdp/matcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"net"
"net/netip"
"regexp"
"strconv"
"strings"

Expand All @@ -35,12 +36,17 @@ func init() {

// MatchRDP is able to match RDP connections.
type MatchRDP struct {
CookieHash string `json:"cookie_hash,omitempty"`
CookieIPs []string `json:"cookie_ips,omitempty"`
CookiePorts []uint16 `json:"cookie_ports,omitempty"`
CustomInfo string `json:"custom_info,omitempty"`
CookieHash string `json:"cookie_hash,omitempty"`
CookieHashRegexp string `json:"cookie_hash_regexp,omitempty"`
CookieIPs []string `json:"cookie_ips,omitempty"`
CookiePorts []uint16 `json:"cookie_ports,omitempty"`
CustomInfo string `json:"custom_info,omitempty"`
CustomInfoRegexp string `json:"custom_info_regexp,omitempty"`

cookieIPs []netip.Prefix

cookieHashRegexp *regexp.Regexp
customInfoRegexp *regexp.Regexp
}

// CaddyModule returns the Caddy module information.
Expand Down Expand Up @@ -142,16 +148,22 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) {
repl := cx.Context.Value(layer4.ReplacerCtxKey).(*caddy.Replacer)
repl.Set("l4.rdp.cookie_hash", hash)

// Full match
if len(m.CookieHash) > 0 && m.CookieHash != hash {
break
}

// Regexp match
if len(m.CookieHashRegexp) > 0 && !m.cookieHashRegexp.MatchString(hash) {
break
}

hasValidCookie = true
break
}

// NOTE: we can stop validation because hash hasn't matched
if !hasValidCookie && len(m.CookieHash) > 0 {
if !hasValidCookie && (len(m.CookieHash) > 0 || len(m.CookieHashRegexp) > 0) {
return false, nil
}

Expand Down Expand Up @@ -296,16 +308,22 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) {
repl := cx.Context.Value(layer4.ReplacerCtxKey).(*caddy.Replacer)
repl.Set("l4.rdp.custom_info", info)

// Full match
if len(m.CustomInfo) > 0 && m.CustomInfo != info {
break
}

// Regexp match
if len(m.CustomInfoRegexp) > 0 && !m.customInfoRegexp.MatchString(info) {
break
}

hasValidCustom = true
break
}

// NOTE: we can stop validation because info hasn't matched
if !hasValidCustom && len(m.CustomInfo) > 0 {
if !hasValidCustom && (len(m.CustomInfo) > 0 || len(m.CustomInfoRegexp) > 0) {
return false, nil
}

Expand Down Expand Up @@ -398,12 +416,20 @@ func (m *MatchRDP) Match(cx *layer4.Connection) (bool, error) {
return true, nil
}

// Provision parses m's IP ranges, either from IP or CIDR expressions.
// Provision parses m's IP ranges, either from IP or CIDR expressions, and regular expressions.
func (m *MatchRDP) Provision(_ caddy.Context) (err error) {
m.cookieIPs, err = layer4.ParseNetworks(m.CookieIPs)
if err != nil {
return err
}
m.cookieHashRegexp, err = regexp.Compile(m.CookieHashRegexp)
if err != nil {
return err
}
m.customInfoRegexp, err = regexp.Compile(m.CustomInfoRegexp)
if err != nil {
return err
}
return nil
}

Expand All @@ -413,12 +439,18 @@ func (m *MatchRDP) Provision(_ caddy.Context) (err error) {
// cookie_hash <value>
// }
// rdp {
// cookie_hash_regexp <value>
// }
// rdp {
// cookie_ip <ranges...>
// cookie_port <ports...>
// }
// rdp {
// custom_info <value>
// }
// rdp {
// custom_info_regexp <value>
// }
// rdp
//
// Note: according to the protocol documentation, RDP cookies and tokens are optional, i.e. it depends on the client
Expand Down Expand Up @@ -456,6 +488,18 @@ func (m *MatchRDP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
_, val := d.NextArg(), d.Val()
m.CookieHash, hasCookieHash = val[:min(RDPCookieHashBytesMax, uint16(len(val)))], true
case "cookie_hash_regexp":
if hasCookieIPOrPort || hasCustomInfo {
return d.Errf("%s option '%s' can't be combined with other options", wrapper, optionName)
}
if hasCookieHash {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
_, val := d.NextArg(), d.Val()
m.CookieHashRegexp, hasCookieHash = val[:min(RDPCookieHashBytesMax, uint16(len(val)))], true
case "cookie_ip":
if hasCookieHash || hasCustomInfo {
return d.Errf("%s option '%s' can only be combined with 'cookie_port' option", wrapper, optionName)
Expand Down Expand Up @@ -499,6 +543,18 @@ func (m *MatchRDP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
_, val := d.NextArg(), d.Val()
m.CustomInfo, hasCustomInfo = val[:min(RDPCustomInfoBytesMax, uint16(len(val)))], true
case "custom_info_regexp":
if hasCookieHash || hasCookieIPOrPort {
return d.Errf("%s option '%s' can't be combined with other options", wrapper, optionName)
}
if hasCustomInfo {
return d.Errf("duplicate %s option '%s'", wrapper, optionName)
}
if d.CountRemainingArgs() != 1 {
return d.ArgErr()
}
_, val := d.NextArg(), d.Val()
m.CustomInfoRegexp, hasCustomInfo = val[:min(RDPCustomInfoBytesMax, uint16(len(val)))], true
default:
return d.ArgErr()
}
Expand Down
6 changes: 6 additions & 0 deletions modules/l4rdp/matcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ func Test_MatchRDP_Match(t *testing.T) {
{matcher: &MatchRDP{CookieHash: ""}, data: packetValid3, shouldMatch: true},
{matcher: &MatchRDP{CookieHash: "a0123"}, data: packetValid3, shouldMatch: true},
{matcher: &MatchRDP{CookieHash: "admin"}, data: packetValid3, shouldMatch: false},
{matcher: &MatchRDP{CookieHashRegexp: ""}, data: packetValid3, shouldMatch: true},
{matcher: &MatchRDP{CookieHashRegexp: "^[a-z]\\d+$"}, data: packetValid3, shouldMatch: true},
{matcher: &MatchRDP{CookieHashRegexp: "^[A-Z]\\d+$"}, data: packetValid3, shouldMatch: false},
// with filtered port
{matcher: &MatchRDP{CookiePorts: []uint16{}}, data: packetValid5, shouldMatch: true},
{matcher: &MatchRDP{CookiePorts: []uint16{3389}}, data: packetValid5, shouldMatch: true},
Expand All @@ -186,6 +189,9 @@ func Test_MatchRDP_Match(t *testing.T) {
{matcher: &MatchRDP{CustomInfo: ""}, data: packetValid9, shouldMatch: true},
{matcher: &MatchRDP{CustomInfo: "anything could be here"}, data: packetValid9, shouldMatch: true},
{matcher: &MatchRDP{CustomInfo: "arbitrary text"}, data: packetValid9, shouldMatch: false},
{matcher: &MatchRDP{CustomInfoRegexp: ""}, data: packetValid9, shouldMatch: true},
{matcher: &MatchRDP{CustomInfoRegexp: "^([A-Za-z0-9 ]+)$"}, data: packetValid9, shouldMatch: true},
{matcher: &MatchRDP{CustomInfoRegexp: "^\\x00\\x01\\x02\\x03\\x04$"}, data: packetValid9, shouldMatch: false},
}

ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
Expand Down

0 comments on commit ed36f3f

Please sign in to comment.