apply rewrite_urls rules when redirecting to proxy pages

Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2026-04-16 21:08:43 +02:00
parent 06c73977b7
commit fda18732f8
2 changed files with 160 additions and 36 deletions
+79 -27
View File
@@ -15,6 +15,7 @@ import (
"net/url"
"os"
"path/filepath"
"regexp"
"runtime/debug"
"strings"
textTmpl "text/template"
@@ -1240,10 +1241,9 @@ func (s *Server) checkAndServePhishingPage(
return true, fmt.Errorf("Proxy page has no configuration: %s", err)
}
// extract the phishing domain from Proxy configuration
var rawConfig map[string]interface{}
err = yaml.Unmarshal([]byte(proxyConfig.String()), &rawConfig)
if err != nil {
// parse proxy config as typed struct so rewrite_urls rules are accessible
var parsedConfig service.ProxyServiceConfigYAML
if err := yaml.Unmarshal([]byte(proxyConfig.String()), &parsedConfig); err != nil {
return true, fmt.Errorf("invalid Proxy configuration YAML: %s", err)
}
@@ -1256,19 +1256,13 @@ func (s *Server) checkAndServePhishingPage(
// find the phishing domain mapping for the start URL domain
phishingDomain := ""
for originalHost, domainData := range rawConfig {
if originalHost == "proxy" || originalHost == "global" {
for originalHost, hostCfg := range parsedConfig.Hosts {
if hostCfg == nil {
continue
}
if originalHost == startDomain {
if domainMap, ok := domainData.(map[string]interface{}); ok {
if to, exists := domainMap["to"]; exists {
if toStr, ok := to.(string); ok {
phishingDomain = toStr
break
}
}
}
phishingDomain = hostCfg.To
break
}
}
@@ -1485,30 +1479,88 @@ func (s *Server) checkAndServePhishingPage(
"phishingDomainType", phishingDomainRecord.Type,
)
// build the redirect URL to the phishing domain with campaign recipient ID
// build the redirect URL to the phishing domain with campaign recipient ID.
// apply rewrite_urls rules so the victim sees the friendly path, not the real
// start url path. query params from the start url are carried through but
// remapped according to rule.Query so they match what checkAndApplyURLRewrite
// expects when the request arrives at the proxy.
urlParam := cTemplate.URLIdentifier.Name.MustGet()
// construct the redirect URL properly
// collect rewrite_urls rules for the start domain — host-specific first, then global
var rewriteRules []service.ProxyServiceURLRewriteRule
if hostCfg, ok := parsedConfig.Hosts[startDomain]; ok && hostCfg != nil {
rewriteRules = append(rewriteRules, hostCfg.RewriteURLs...)
}
if parsedConfig.Global != nil {
rewriteRules = append(rewriteRules, parsedConfig.Global.RewriteURLs...)
}
// start from the start url path and query, then apply the first matching rule
redirectPath := parsedStartURL.Path
startQuery := url.Values{}
if parsedStartURL.RawQuery != "" {
startQuery, _ = url.ParseQuery(parsedStartURL.RawQuery)
}
for _, rule := range rewriteRules {
pattern := strings.TrimPrefix(rule.Find, "^")
pattern = strings.TrimSuffix(pattern, "$")
re, reErr := regexp.Compile("^" + pattern)
if reErr != nil {
continue
}
if !re.MatchString(parsedStartURL.Path) {
continue
}
// rewrite path to the friendly replacement
redirectPath = rule.Replace
// remap query param keys: find → replace
if len(rule.Query) > 0 {
remapped := url.Values{}
for k, v := range startQuery {
remapped[k] = v
}
for _, qRule := range rule.Query {
if vals, exists := remapped[qRule.Find]; exists {
delete(remapped, qRule.Find)
remapped[qRule.Replace] = vals
}
}
startQuery = remapped
}
// apply filter: keep only the listed query param names
if len(rule.Filter) > 0 {
allowed := make(map[string]struct{}, len(rule.Filter))
for _, name := range rule.Filter {
allowed[name] = struct{}{}
}
filtered := url.Values{}
for k, v := range startQuery {
if _, ok := allowed[k]; ok {
filtered[k] = v
}
}
startQuery = filtered
}
break
}
u := &url.URL{
Scheme: "https",
Host: phishingDomain,
Path: parsedStartURL.Path,
Path: redirectPath,
}
q := u.Query()
// merge start url query params (rewritten) with campaign params
q := startQuery
q.Set(urlParam, campaignRecipientID.String())
if encryptedParam != "" {
q.Set(stateParamKey, encryptedParam)
}
// preserve any existing query params from start URL
if parsedStartURL.RawQuery != "" {
startQuery, _ := url.ParseQuery(parsedStartURL.RawQuery)
for key, values := range startQuery {
for _, value := range values {
q.Add(key, value)
}
}
}
u.RawQuery = q.Encode()
s.logger.Debugw("built proxy redirect URL",
+81 -9
View File
@@ -4506,17 +4506,89 @@ func (m *ProxyHandler) checkAndServeEvasionPage(req *http.Request, reqCtx *Reque
}
}
// preserve the original URL without campaign parameters for post-evasion redirect
originalURL := req.URL.Path
if req.URL.RawQuery != "" {
// parse query params and remove campaign recipient ID
query := req.URL.Query()
if reqCtx.ParamName != "" {
query.Del(reqCtx.ParamName)
// build the post-evasion redirect url using the start url as the source of truth for
// path and query params — the incoming request only has the campaign id param, not the
// real start url params (client_id, redirect_uri etc.). apply rewrite_urls rules so the
// victim sees the friendly path and remapped param names, not the real ones.
var startPath string
var startQuery url.Values
if reqCtx.ProxyEntry != nil {
if startURLVal, err := reqCtx.ProxyEntry.StartURL.Get(); err == nil {
if parsedStart, err := url.Parse(startURLVal.String()); err == nil {
startPath = parsedStart.Path
startQuery, _ = url.ParseQuery(parsedStart.RawQuery)
}
}
if len(query) > 0 {
originalURL += "?" + query.Encode()
}
if startPath == "" {
startPath = req.URL.Path
}
if startQuery == nil {
startQuery = url.Values{}
}
// collect rewrite_urls rules — host-specific first, then global
var rewriteRules []service.ProxyServiceURLRewriteRule
if hostCfg, ok := reqCtx.ProxyConfig.Hosts[reqCtx.TargetDomain]; ok && hostCfg != nil {
rewriteRules = append(rewriteRules, hostCfg.RewriteURLs...)
}
if reqCtx.ProxyConfig.Global != nil {
rewriteRules = append(rewriteRules, reqCtx.ProxyConfig.Global.RewriteURLs...)
}
redirectPath := startPath
redirectQuery := startQuery
for _, rule := range rewriteRules {
pattern := strings.TrimPrefix(rule.Find, "^")
pattern = strings.TrimSuffix(pattern, "$")
re, reErr := regexp.Compile("^" + pattern)
if reErr != nil {
continue
}
if !re.MatchString(startPath) {
continue
}
// rewrite path to the friendly replacement
redirectPath = rule.Replace
// remap query param keys: find → replace
if len(rule.Query) > 0 {
remapped := url.Values{}
for k, v := range startQuery {
remapped[k] = v
}
for _, qRule := range rule.Query {
if vals, exists := remapped[qRule.Find]; exists {
delete(remapped, qRule.Find)
remapped[qRule.Replace] = vals
}
}
redirectQuery = remapped
}
// apply filter: keep only the listed query param names
if len(rule.Filter) > 0 {
allowed := make(map[string]struct{}, len(rule.Filter))
for _, name := range rule.Filter {
allowed[name] = struct{}{}
}
filtered := url.Values{}
for k, v := range redirectQuery {
if _, ok := allowed[k]; ok {
filtered[k] = v
}
}
redirectQuery = filtered
}
break
}
originalURL := redirectPath
if len(redirectQuery) > 0 {
originalURL += "?" + redirectQuery.Encode()
}
// this is initial request with campaign recipient ID and no state parameter, serve evasion page