mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-06 04:17:54 +02:00
Evasion page implementation.
Fix ip filtering. Support for ip filter in proxies
This commit is contained in:
+107
-13
@@ -84,6 +84,7 @@ func NewServer(
|
||||
repositories.Proxy,
|
||||
repositories.Identifier,
|
||||
services.Campaign,
|
||||
services.Template,
|
||||
cookieName,
|
||||
)
|
||||
|
||||
@@ -697,6 +698,12 @@ func (s *Server) checkAndServePhishingPage(
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to get recipient: %s", err)
|
||||
}
|
||||
// check for evasion page first
|
||||
var evasionPageID *uuid.UUID
|
||||
if v, err := campaign.EvasionPageID.Get(); err == nil {
|
||||
evasionPageID = &v
|
||||
}
|
||||
|
||||
// figure out which page types this template has
|
||||
var beforePageID *uuid.UUID
|
||||
var beforeProxyID *uuid.UUID
|
||||
@@ -737,19 +744,34 @@ func (s *Server) checkAndServePhishingPage(
|
||||
nextPageType := ""
|
||||
currentPageType := ""
|
||||
|
||||
s.logger.Debugw("determining page flow",
|
||||
"pageTypeQuery", pageTypeQuery,
|
||||
"hasBeforePage", beforePageID != nil,
|
||||
"hasBeforeProxy", beforeProxyID != nil,
|
||||
"hasLandingPage", landingPageID != nil,
|
||||
"hasLandingProxy", landingProxyID != nil,
|
||||
"hasAfterPage", afterPageID != nil,
|
||||
"hasAfterProxy", afterProxyID != nil,
|
||||
"campaignRecipientID", campaignRecipientID.String(),
|
||||
)
|
||||
/*
|
||||
s.logger.Debugw("determining page flow",
|
||||
"pageTypeQuery", pageTypeQuery,
|
||||
"hasBeforePage", beforePageID != nil,
|
||||
"hasBeforeProxy", beforeProxyID != nil,
|
||||
"hasLandingPage", landingPageID != nil,
|
||||
"hasLandingProxy", landingProxyID != nil,
|
||||
"hasAfterPage", afterPageID != nil,
|
||||
"hasAfterProxy", afterProxyID != nil,
|
||||
"campaignRecipientID", campaignRecipientID.String(),
|
||||
)
|
||||
*/
|
||||
|
||||
if len(pageTypeQuery) == 0 {
|
||||
if beforePageID != nil || beforeProxyID != nil {
|
||||
// check if there's an evasion page to serve first
|
||||
if evasionPageID != nil {
|
||||
pageID = evasionPageID
|
||||
s.logger.Debugw("initial request - serving evasion page",
|
||||
"pageID", pageID.String(),
|
||||
)
|
||||
currentPageType = data.PAGE_TYPE_EVASION
|
||||
// determine next page type based on template structure
|
||||
if beforePageID != nil || beforeProxyID != nil {
|
||||
nextPageType = data.PAGE_TYPE_BEFORE
|
||||
} else {
|
||||
nextPageType = data.PAGE_TYPE_LANDING
|
||||
}
|
||||
} else if beforePageID != nil || beforeProxyID != nil {
|
||||
if beforePageID != nil {
|
||||
pageID = beforePageID
|
||||
s.logger.Debugw("initial request - serving before landing page",
|
||||
@@ -785,8 +807,72 @@ func (s *Server) checkAndServePhishingPage(
|
||||
// if there is a page type, then we use that
|
||||
} else {
|
||||
switch pageTypeQuery {
|
||||
// this is not relevant - already taken care of, ignore it
|
||||
// this is set if the previous page was an evasion page
|
||||
case data.PAGE_TYPE_EVASION:
|
||||
// after evasion page, go to before page or landing page
|
||||
if beforePageID != nil || beforeProxyID != nil {
|
||||
if beforePageID != nil {
|
||||
pageID = beforePageID
|
||||
s.logger.Debugw("serving before landing page from evasion state",
|
||||
"pageID", pageID.String(),
|
||||
)
|
||||
} else {
|
||||
proxyID = beforeProxyID
|
||||
s.logger.Debugw("serving before landing Proxy from evasion state",
|
||||
"proxyID", proxyID.String(),
|
||||
)
|
||||
}
|
||||
currentPageType = data.PAGE_TYPE_BEFORE
|
||||
nextPageType = data.PAGE_TYPE_LANDING
|
||||
} else {
|
||||
if landingPageID != nil {
|
||||
pageID = landingPageID
|
||||
s.logger.Debugw("serving landing page from evasion state",
|
||||
"pageID", pageID.String(),
|
||||
)
|
||||
} else {
|
||||
proxyID = landingProxyID
|
||||
s.logger.Debugw("serving landing Proxy from evasion state",
|
||||
"proxyID", proxyID.String(),
|
||||
)
|
||||
}
|
||||
currentPageType = data.PAGE_TYPE_LANDING
|
||||
if afterPageID != nil || afterProxyID != nil {
|
||||
nextPageType = data.PAGE_TYPE_AFTER
|
||||
} else {
|
||||
nextPageType = data.PAGE_TYPE_DONE
|
||||
}
|
||||
}
|
||||
// special case for deny page access from evasion page
|
||||
case "deny":
|
||||
// serve the deny page if one is configured
|
||||
if denyPageID, err := campaign.DenyPageID.Get(); err == nil {
|
||||
err = s.renderDenyPage(c, domain, &denyPageID)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("failed to render deny page from evasion: %s", err)
|
||||
}
|
||||
return true, nil
|
||||
} else {
|
||||
// if no deny page configured, return 403
|
||||
c.String(http.StatusForbidden, "Access denied")
|
||||
c.Abort()
|
||||
return true, nil
|
||||
}
|
||||
// this is set when transitioning to the before page
|
||||
case data.PAGE_TYPE_BEFORE:
|
||||
if beforePageID != nil {
|
||||
pageID = beforePageID
|
||||
s.logger.Debugw("serving before landing page from state",
|
||||
"pageID", pageID.String(),
|
||||
)
|
||||
} else {
|
||||
proxyID = beforeProxyID
|
||||
s.logger.Debugw("serving before landing Proxy from state",
|
||||
"proxyID", proxyID.String(),
|
||||
)
|
||||
}
|
||||
currentPageType = data.PAGE_TYPE_BEFORE
|
||||
nextPageType = data.PAGE_TYPE_LANDING
|
||||
// this is set if the previous page was a before page
|
||||
case data.PAGE_TYPE_LANDING:
|
||||
if landingPageID != nil {
|
||||
@@ -1030,6 +1116,8 @@ func (s *Server) checkAndServePhishingPage(
|
||||
visitEventID := uuid.New()
|
||||
eventName := ""
|
||||
switch currentPageType {
|
||||
case data.PAGE_TYPE_EVASION:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED
|
||||
case data.PAGE_TYPE_BEFORE:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED
|
||||
case data.PAGE_TYPE_LANDING:
|
||||
@@ -1236,6 +1324,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
return true, fmt.Errorf("failed to encrypt next page type: %s", err)
|
||||
}
|
||||
urlPath := cTemplate.URLPath.MustGet().String()
|
||||
|
||||
err = s.renderPageTemplate(
|
||||
c,
|
||||
domain,
|
||||
@@ -1246,6 +1335,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
cTemplate,
|
||||
encryptedParam,
|
||||
urlPath,
|
||||
campaign,
|
||||
)
|
||||
if err != nil {
|
||||
return true, fmt.Errorf("failed to render phishing page: %s", err)
|
||||
@@ -1254,6 +1344,8 @@ func (s *Server) checkAndServePhishingPage(
|
||||
visitEventID := uuid.New()
|
||||
eventName := ""
|
||||
switch currentPageType {
|
||||
case data.PAGE_TYPE_EVASION:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED
|
||||
case data.PAGE_TYPE_BEFORE:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED
|
||||
case data.PAGE_TYPE_LANDING:
|
||||
@@ -1543,12 +1635,13 @@ func (s *Server) renderPageTemplate(
|
||||
campaignTemplate *model.CampaignTemplate,
|
||||
stateParam string,
|
||||
urlPath string,
|
||||
campaign *model.Campaign,
|
||||
) error {
|
||||
content, err := page.Content.Get()
|
||||
if err != nil {
|
||||
return fmt.Errorf("no page content set to render: %s", err)
|
||||
}
|
||||
phishingPage, err := s.services.Template.CreatePhishingPage(
|
||||
phishingPage, err := s.services.Template.CreatePhishingPageWithCampaign(
|
||||
domain,
|
||||
email,
|
||||
campaignRecipientID,
|
||||
@@ -1557,6 +1650,7 @@ func (s *Server) renderPageTemplate(
|
||||
campaignTemplate,
|
||||
stateParam,
|
||||
urlPath,
|
||||
campaign,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create phishing page: %s", err)
|
||||
|
||||
Vendored
+11
-10
@@ -37,16 +37,17 @@ func IsUpdateAvailable() bool {
|
||||
// readonly
|
||||
var CampaignEventPriority = map[string]int{
|
||||
// campaign recipient events
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_REPORTED: 90,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_CANCELLED: 80,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA: 70,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED: 60,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED: 40,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED: 50,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ: 30,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED: 20,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT: 20,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_SCHEDULED: 10,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_REPORTED: 90,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_CANCELLED: 80,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA: 70,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED: 60,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED: 40,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED: 50,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED: 35,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ: 30,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED: 20,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT: 20,
|
||||
data.EVENT_CAMPAIGN_RECIPIENT_SCHEDULED: 10,
|
||||
// campaign events
|
||||
data.EVENT_CAMPAIGN_CLOSED: 30,
|
||||
data.EVENT_CAMPAIGN_ACTIVE: 20,
|
||||
|
||||
@@ -178,6 +178,7 @@ func (c *Campaign) GetByID(g *gin.Context) {
|
||||
WithRecipientGroups: true,
|
||||
WithAllowDeny: true,
|
||||
WithDenyPage: true,
|
||||
WithEvasionPage: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
@@ -210,6 +211,7 @@ func (c *Campaign) GetByName(g *gin.Context) {
|
||||
WithRecipientGroups: true,
|
||||
WithAllowDeny: true,
|
||||
WithDenyPage: true,
|
||||
WithEvasionPage: true,
|
||||
},
|
||||
)
|
||||
// handle responses
|
||||
|
||||
+12
-10
@@ -6,16 +6,17 @@ const (
|
||||
EVENT_CAMPAIGN_SELF_MANAGED = "campaign_self_managed"
|
||||
EVENT_CAMPAIGN_CLOSED = "campaign_closed"
|
||||
|
||||
EVENT_CAMPAIGN_RECIPIENT_SCHEDULED = "campaign_recipient_scheduled"
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT = "campaign_recipient_message_sent"
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED = "campaign_recipient_message_failed"
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ = "campaign_recipient_message_read"
|
||||
EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED = "campaign_recipient_before_page_visited"
|
||||
EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED = "campaign_recipient_page_visited"
|
||||
EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED = "campaign_recipient_after_page_visited"
|
||||
EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA = "campaign_recipient_submitted_data"
|
||||
EVENT_CAMPAIGN_RECIPIENT_REPORTED = "campaign_recipient_reported"
|
||||
EVENT_CAMPAIGN_RECIPIENT_CANCELLED = "campaign_recipient_cancelled"
|
||||
EVENT_CAMPAIGN_RECIPIENT_SCHEDULED = "campaign_recipient_scheduled"
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT = "campaign_recipient_message_sent"
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED = "campaign_recipient_message_failed"
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ = "campaign_recipient_message_read"
|
||||
EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED = "campaign_recipient_evasion_page_visited"
|
||||
EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED = "campaign_recipient_before_page_visited"
|
||||
EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED = "campaign_recipient_page_visited"
|
||||
EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED = "campaign_recipient_after_page_visited"
|
||||
EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA = "campaign_recipient_submitted_data"
|
||||
EVENT_CAMPAIGN_RECIPIENT_REPORTED = "campaign_recipient_reported"
|
||||
EVENT_CAMPAIGN_RECIPIENT_CANCELLED = "campaign_recipient_cancelled"
|
||||
)
|
||||
|
||||
var Events = []string{
|
||||
@@ -29,6 +30,7 @@ var Events = []string{
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_SENT,
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_FAILED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_MESSAGE_READ,
|
||||
EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED,
|
||||
EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package data
|
||||
|
||||
const (
|
||||
PAGE_TYPE_EVASION = "evasion"
|
||||
PAGE_TYPE_BEFORE = "before"
|
||||
PAGE_TYPE_LANDING = "landing"
|
||||
PAGE_TYPE_AFTER = "after"
|
||||
|
||||
@@ -48,10 +48,12 @@ type Campaign struct {
|
||||
CampaignTemplate *CampaignTemplate
|
||||
|
||||
// can has one
|
||||
CompanyID *uuid.UUID `gorm:"index;type:uuid;index;uniqueIndex:idx_campaigns_unique_name_and_company_id;"`
|
||||
Company *Company
|
||||
DenyPageID *uuid.UUID `gorm:"type:uuid;index;"`
|
||||
DenyPage *Page `gorm:"foreignKey:DenyPageID;references:ID"`
|
||||
CompanyID *uuid.UUID `gorm:"index;type:uuid;index;uniqueIndex:idx_campaigns_unique_name_and_company_id;"`
|
||||
Company *Company
|
||||
DenyPageID *uuid.UUID `gorm:"type:uuid;index;"`
|
||||
DenyPage *Page `gorm:"foreignKey:DenyPageID;references:ID"`
|
||||
EvasionPageID *uuid.UUID `gorm:"type:uuid;index;"`
|
||||
EvasionPage *Page `gorm:"foreignKey:EvasionPageID;references:ID"`
|
||||
// NotableEventID notable event for this campaign
|
||||
NotableEvent *Event `gorm:"foreignKey:NotableEventID;references:ID"`
|
||||
NotableEventID *uuid.UUID `gorm:"type:uuid;index"`
|
||||
|
||||
+25
-12
@@ -46,6 +46,8 @@ type Campaign struct {
|
||||
AllowDenyIDs nullable.Nullable[[]*uuid.UUID] `json:"allowDenyIDs,omitempty"`
|
||||
DenyPageID nullable.Nullable[uuid.UUID] `json:"denyPageID,omitempty"`
|
||||
DenyPage *Page `json:"denyPage"`
|
||||
EvasionPageID nullable.Nullable[uuid.UUID] `json:"evasionPageID,omitempty"`
|
||||
EvasionPage *Page `json:"evasionPage"`
|
||||
WebhookID nullable.Nullable[uuid.UUID] `json:"webhookID"`
|
||||
|
||||
// must not be set by a user
|
||||
@@ -85,7 +87,7 @@ func (c *Campaign) Validate() error {
|
||||
if len(c.RecipientGroupIDs.MustGet()) == 0 {
|
||||
return validate.WrapErrorWithField(errors.New("must have at least one recipient group"), "RecipientGroupIDs")
|
||||
}
|
||||
if err := c.ValidateDenyPage(); err != nil {
|
||||
if err := c.ValidateEvasionAndDenyPages(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -208,13 +210,22 @@ func (c *Campaign) ValidateNoSendTimesSet() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDenyPage checks that a deny page is set
|
||||
func (c *Campaign) ValidateDenyPage() error {
|
||||
if c.DenyPageID.IsSpecified() && !c.DenyPageID.IsNull() {
|
||||
if c.AllowDenyIDs.IsSpecified() && (c.AllowDenyIDs.IsNull() || len(c.AllowDenyIDs.MustGet()) == 0) {
|
||||
return validate.WrapErrorWithField(errors.New("requires a allow deny IDs to be set"), "denyPage")
|
||||
// ValidateEvasionAndDenyPages checks evasion page and deny page requirements
|
||||
func (c *Campaign) ValidateEvasionAndDenyPages() error {
|
||||
// if evasion page is set, deny page must also be set
|
||||
if c.EvasionPageID.IsSpecified() && !c.EvasionPageID.IsNull() {
|
||||
if !c.DenyPageID.IsSpecified() || c.DenyPageID.IsNull() {
|
||||
return validate.WrapErrorWithField(errors.New("evasion page requires a deny page to be set"), "evasionPage")
|
||||
}
|
||||
}
|
||||
|
||||
// if allow/deny IDs are set (IP filtering), deny page must be set
|
||||
if c.AllowDenyIDs.IsSpecified() && !c.AllowDenyIDs.IsNull() && len(c.AllowDenyIDs.MustGet()) > 0 {
|
||||
if !c.DenyPageID.IsSpecified() || c.DenyPageID.IsNull() {
|
||||
return validate.WrapErrorWithField(errors.New("IP filtering requires a deny page to be set"), "denyPage")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -320,15 +331,17 @@ func (c *Campaign) ToDBMap() map[string]any {
|
||||
m["company_id"] = c.CompanyID.MustGet()
|
||||
}
|
||||
}
|
||||
allowDenyIsSet := c.AllowDenyIDs.IsSpecified() && !c.AllowDenyIDs.IsNull() && len(c.AllowDenyIDs.MustGet()) > 0
|
||||
if allowDenyIsSet {
|
||||
if c.DenyPageID.IsSpecified() {
|
||||
m["deny_page_id"] = nil
|
||||
if v, err := c.DenyPageID.Get(); err == nil {
|
||||
m["deny_page_id"] = v.String()
|
||||
} else {
|
||||
m["deny_page_id"] = nil
|
||||
}
|
||||
} else {
|
||||
m["deny_page_id"] = nil
|
||||
}
|
||||
if c.EvasionPageID.IsSpecified() {
|
||||
m["evasion_page_id"] = nil
|
||||
if v, err := c.EvasionPageID.Get(); err == nil {
|
||||
m["evasion_page_id"] = v.String()
|
||||
}
|
||||
}
|
||||
if c.WebhookID.IsSpecified() {
|
||||
m["webhook_id"] = nil
|
||||
|
||||
+432
-2
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
@@ -113,6 +114,7 @@ type ProxyHandler struct {
|
||||
ProxyRepository *repository.Proxy
|
||||
IdentifierRepository *repository.Identifier
|
||||
CampaignService *service.Campaign
|
||||
TemplateService *service.Template
|
||||
cookieName string
|
||||
}
|
||||
|
||||
@@ -126,6 +128,7 @@ func NewProxyHandler(
|
||||
proxyRepository *repository.Proxy,
|
||||
identifierRepository *repository.Identifier,
|
||||
campaignService *service.Campaign,
|
||||
templateService *service.Template,
|
||||
cookieName string,
|
||||
) *ProxyHandler {
|
||||
return &ProxyHandler{
|
||||
@@ -139,6 +142,7 @@ func NewProxyHandler(
|
||||
ProxyRepository: proxyRepository,
|
||||
IdentifierRepository: identifierRepository,
|
||||
CampaignService: campaignService,
|
||||
TemplateService: templateService,
|
||||
cookieName: cookieName,
|
||||
}
|
||||
}
|
||||
@@ -153,6 +157,22 @@ func (m *ProxyHandler) HandleHTTPRequest(w http.ResponseWriter, req *http.Reques
|
||||
return err
|
||||
}
|
||||
|
||||
// check ip filtering if we have a campaign recipient
|
||||
if reqCtx.CampaignRecipientID != nil {
|
||||
blocked, resp := m.checkIPFilter(req, reqCtx)
|
||||
if blocked {
|
||||
if resp != nil {
|
||||
return m.writeResponse(w, resp)
|
||||
}
|
||||
// if no deny page configured, return 404
|
||||
return m.writeResponse(w, &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// create http client
|
||||
client, err := m.createHTTPClient(req, reqCtx.ProxyConfig)
|
||||
if err != nil {
|
||||
@@ -259,9 +279,20 @@ func (m *ProxyHandler) processRequestWithContext(req *http.Request, reqCtx *Requ
|
||||
reqURL := req.URL.String()
|
||||
createSession := reqCtx.CampaignRecipientID != nil
|
||||
|
||||
// handle existing session cleanup if this is an initial request
|
||||
// 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)
|
||||
@@ -1951,6 +1982,14 @@ func (m *ProxyHandler) getCurrentPageType(req *http.Request, template *model.Cam
|
||||
|
||||
func (m *ProxyHandler) getNextPageType(currentPageType string, template *model.CampaignTemplate) string {
|
||||
switch currentPageType {
|
||||
case data.PAGE_TYPE_EVASION:
|
||||
if _, errPage := template.BeforeLandingPageID.Get(); errPage == nil {
|
||||
return data.PAGE_TYPE_BEFORE
|
||||
}
|
||||
if _, errProxy := template.BeforeLandingProxyID.Get(); errProxy == nil {
|
||||
return data.PAGE_TYPE_BEFORE
|
||||
}
|
||||
return data.PAGE_TYPE_LANDING
|
||||
case data.PAGE_TYPE_BEFORE:
|
||||
return data.PAGE_TYPE_LANDING
|
||||
case data.PAGE_TYPE_LANDING:
|
||||
@@ -2417,7 +2456,7 @@ func (m *ProxyHandler) writeResponse(w http.ResponseWriter, resp *http.Response)
|
||||
// check for nil response
|
||||
if resp == nil {
|
||||
m.logger.Errorw("response is nil in writeResponse")
|
||||
w.WriteHeader(500)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return errors.New("response is nil")
|
||||
}
|
||||
|
||||
@@ -2435,6 +2474,7 @@ func (m *ProxyHandler) writeResponse(w http.ResponseWriter, resp *http.Response)
|
||||
|
||||
// copy body
|
||||
if resp.Body != nil {
|
||||
defer resp.Body.Close()
|
||||
_, err := io.Copy(w, resp.Body)
|
||||
return err
|
||||
}
|
||||
@@ -2606,6 +2646,8 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *ProxyS
|
||||
// determine event name based on page type
|
||||
var eventName string
|
||||
switch currentPageType {
|
||||
case data.PAGE_TYPE_EVASION:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED
|
||||
case data.PAGE_TYPE_BEFORE:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED
|
||||
case data.PAGE_TYPE_LANDING:
|
||||
@@ -2714,3 +2756,391 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *ProxyS
|
||||
"eventName", eventName,
|
||||
)
|
||||
}
|
||||
|
||||
// checkAndServeEvasionPage checks if an evasion page should be served and returns the response if so
|
||||
func (m *ProxyHandler) checkAndServeEvasionPage(req *http.Request, reqCtx *RequestContext) *http.Response {
|
||||
// get campaign info first
|
||||
campaign, _, _, err := m.getCampaignInfo(req.Context(), reqCtx.CampaignRecipientID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if there's an evasion page configured
|
||||
evasionPageID, err := campaign.EvasionPageID.Get()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get the campaign template
|
||||
templateID, err := campaign.TemplateID.Get()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := req.Context()
|
||||
cTemplate, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{
|
||||
WithIdentifier: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if this is an initial request (no state parameter)
|
||||
// we already know we have a campaign recipient ID from reqCtx
|
||||
if cTemplate.StateIdentifier != nil {
|
||||
stateParamKey := cTemplate.StateIdentifier.Name.MustGet()
|
||||
encryptedParam := req.URL.Query().Get(stateParamKey)
|
||||
|
||||
// if there is a state parameter, this is not initial request
|
||||
if encryptedParam != "" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// this is initial request with campaign recipient ID and no state parameter, serve evasion page
|
||||
return m.serveEvasionPageResponseDirect(req, reqCtx, &evasionPageID, campaign, cTemplate)
|
||||
}
|
||||
|
||||
// checkAndServeDenyPage checks if a deny page should be served and returns the response if so
|
||||
func (m *ProxyHandler) checkAndServeDenyPage(req *http.Request, reqCtx *RequestContext) *http.Response {
|
||||
// get campaign info first
|
||||
campaign, _, _, err := m.getCampaignInfo(req.Context(), reqCtx.CampaignRecipientID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// get the campaign template
|
||||
templateID, err := campaign.TemplateID.Get()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := req.Context()
|
||||
cTemplate, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{
|
||||
WithIdentifier: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) serveEvasionPageResponseDirect(req *http.Request, reqCtx *RequestContext, evasionPageID *uuid.UUID, campaign *model.Campaign, cTemplate *model.CampaignTemplate) *http.Response {
|
||||
ctx := req.Context()
|
||||
evasionPage, err := m.PageRepository.GetByID(ctx, evasionPageID, &repository.PageOption{})
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to get evasion page", "error", err, "pageID", evasionPageID)
|
||||
return nil
|
||||
}
|
||||
|
||||
campaignID, err := campaign.ID.Get()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// determine next page type after evasion
|
||||
var nextPageType string
|
||||
if _, err := cTemplate.BeforeLandingPageID.Get(); err == nil {
|
||||
nextPageType = data.PAGE_TYPE_BEFORE
|
||||
} else if _, err := cTemplate.BeforeLandingProxyID.Get(); err == nil {
|
||||
nextPageType = data.PAGE_TYPE_BEFORE
|
||||
} else {
|
||||
nextPageType = data.PAGE_TYPE_LANDING
|
||||
}
|
||||
|
||||
htmlContent, err := m.renderEvasionPageTemplate(req, reqCtx, evasionPage, campaign, cTemplate, nextPageType)
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to render evasion page template", "error", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// create HTTP response
|
||||
resp := &http.Response{
|
||||
StatusCode: 200,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(htmlContent)),
|
||||
}
|
||||
|
||||
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(htmlContent)))
|
||||
resp.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
|
||||
// register evasion page visit event
|
||||
m.registerEvasionPageVisitEventDirect(req, reqCtx.CampaignRecipientID, &campaignID, campaign)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) serveDenyPageResponseDirect(req *http.Request, reqCtx *RequestContext, campaign *model.Campaign) *http.Response {
|
||||
denyPageID, err := campaign.DenyPageID.Get()
|
||||
if err != nil {
|
||||
// if no deny page configured, return 403
|
||||
resp := &http.Response{
|
||||
StatusCode: 403,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("Access denied")),
|
||||
}
|
||||
resp.Header.Set("Content-Type", "text/plain")
|
||||
resp.Header.Set("Content-Length", "13")
|
||||
return resp
|
||||
}
|
||||
|
||||
ctx := req.Context()
|
||||
denyPage, err := m.PageRepository.GetByID(ctx, &denyPageID, &repository.PageOption{})
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to get deny page", "error", err, "pageID", denyPageID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// render the deny page
|
||||
htmlContent, err := denyPage.Content.Get()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
content := htmlContent.String()
|
||||
|
||||
// create HTTP response
|
||||
resp := &http.Response{
|
||||
StatusCode: 200,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader(content)),
|
||||
}
|
||||
|
||||
resp.Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||
resp.Header.Set("Content-Length", fmt.Sprintf("%d", len(content)))
|
||||
resp.Header.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) renderEvasionPageTemplate(req *http.Request, reqCtx *RequestContext, page *model.Page, campaign *model.Campaign, cTemplate *model.CampaignTemplate, nextPageType string) (string, error) {
|
||||
// get recipient
|
||||
ctx := req.Context()
|
||||
cRecipient, err := m.CampaignRecipientRepository.GetByID(ctx, reqCtx.CampaignRecipientID, &repository.CampaignRecipientOption{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get campaign recipient: %w", err)
|
||||
}
|
||||
recipientID, err := cRecipient.RecipientID.Get()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get recipient ID: %w", err)
|
||||
}
|
||||
|
||||
// get recipient details
|
||||
recipientRepo := repository.Recipient{DB: m.CampaignRecipientRepository.DB}
|
||||
recipient, err := recipientRepo.GetByID(ctx, &recipientID, &repository.RecipientOption{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get recipient: %w", err)
|
||||
}
|
||||
|
||||
// get email for template
|
||||
templateID, err := campaign.TemplateID.Get()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get template ID: %w", err)
|
||||
}
|
||||
|
||||
cTemplateWithEmail, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{
|
||||
WithEmail: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get campaign template with email: %w", err)
|
||||
}
|
||||
|
||||
emailID := cTemplateWithEmail.EmailID.MustGet()
|
||||
emailRepo := repository.Email{DB: m.CampaignRepository.DB}
|
||||
email, err := emailRepo.GetByID(ctx, &emailID, &repository.EmailOption{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get email: %w", err)
|
||||
}
|
||||
|
||||
// get domain
|
||||
hostVO, err := vo.NewString255(req.Host)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create host VO: %w", err)
|
||||
}
|
||||
domain, err := m.DomainRepository.GetByName(ctx, hostVO, &repository.DomainOption{})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get domain: %w", err)
|
||||
}
|
||||
|
||||
// create encrypted state parameter for next page
|
||||
campaignID := campaign.ID.MustGet()
|
||||
encryptedNextState, err := utils.Encrypt(nextPageType, utils.UUIDToSecret(&campaignID))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encrypt next state: %w", err)
|
||||
}
|
||||
|
||||
// get page content
|
||||
htmlContent, err := page.Content.Get()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get evasion page HTML content: %w", err)
|
||||
}
|
||||
|
||||
// convert model.Domain to database.Domain
|
||||
var proxyID *uuid.UUID
|
||||
if id, err := domain.ProxyID.Get(); err == nil {
|
||||
proxyID = &id
|
||||
}
|
||||
|
||||
dbDomain := &database.Domain{
|
||||
ID: domain.ID.MustGet(),
|
||||
Name: domain.Name.MustGet().String(),
|
||||
Type: domain.Type.MustGet().String(),
|
||||
ProxyID: proxyID,
|
||||
ProxyTargetDomain: domain.ProxyTargetDomain.MustGet().String(),
|
||||
HostWebsite: domain.HostWebsite.MustGet(),
|
||||
RedirectURL: domain.RedirectURL.MustGet().String(),
|
||||
}
|
||||
|
||||
// use template service to render the page
|
||||
urlPath := req.URL.Path
|
||||
buf, err := m.TemplateService.CreatePhishingPageWithCampaign(
|
||||
dbDomain,
|
||||
email,
|
||||
reqCtx.CampaignRecipientID,
|
||||
recipient,
|
||||
htmlContent.String(),
|
||||
cTemplate,
|
||||
encryptedNextState,
|
||||
urlPath,
|
||||
campaign,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to render evasion page template: %w", err)
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, campaignRecipientID *uuid.UUID, campaignID *uuid.UUID, campaign *model.Campaign) {
|
||||
// get recipient from campaign recipient
|
||||
ctx := req.Context()
|
||||
cRecipient, err := m.CampaignRecipientRepository.GetByID(ctx, campaignRecipientID, &repository.CampaignRecipientOption{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
recipientID, err := cRecipient.RecipientID.Get()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED]
|
||||
newEventID := uuid.New()
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(req.UserAgent(), 0, 1000)) // MAX_USER_AGENT_SAVED equivalent
|
||||
|
||||
var event *model.CampaignEvent
|
||||
if !campaign.IsAnonymous.MustGet() {
|
||||
event = &model.CampaignEvent{
|
||||
ID: &newEventID,
|
||||
CampaignID: campaignID,
|
||||
RecipientID: &recipientID,
|
||||
IP: clientIP,
|
||||
UserAgent: userAgent,
|
||||
EventID: eventID,
|
||||
Data: vo.NewEmptyOptionalString1MB(),
|
||||
}
|
||||
} else {
|
||||
ua := vo.NewEmptyOptionalString255()
|
||||
event = &model.CampaignEvent{
|
||||
ID: &newEventID,
|
||||
CampaignID: campaignID,
|
||||
RecipientID: nil,
|
||||
IP: vo.NewEmptyOptionalString64(),
|
||||
UserAgent: ua,
|
||||
EventID: eventID,
|
||||
Data: vo.NewEmptyOptionalString1MB(),
|
||||
}
|
||||
}
|
||||
|
||||
err = m.CampaignRepository.SaveEvent(req.Context(), event)
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to save evasion page visit event", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// checkIPFilter checks if the IP is allowed for proxy requests
|
||||
// returns (blocked, response) where blocked=true means the IP should be blocked
|
||||
func (m *ProxyHandler) checkIPFilter(req *http.Request, reqCtx *RequestContext) (bool, *http.Response) {
|
||||
// get campaign info
|
||||
campaign, _, campaignID, err := m.getCampaignInfo(req.Context(), reqCtx.CampaignRecipientID)
|
||||
if err != nil {
|
||||
return false, nil // allow if we can't get campaign info
|
||||
}
|
||||
|
||||
// extract client IP and strip port if present using net.SplitHostPort for IPv6 safety
|
||||
ip := utils.ExtractClientIP(req)
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil {
|
||||
ip = host
|
||||
}
|
||||
|
||||
// get allow/deny list entries
|
||||
allowDenyEntries, err := m.CampaignRepository.GetAllDenyByCampaignID(req.Context(), campaignID)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return false, nil // allow if we can't get the list
|
||||
}
|
||||
|
||||
// if there are no entries, allow access
|
||||
if len(allowDenyEntries) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// check IP against allow/deny lists
|
||||
isAllowListing := false
|
||||
allowed := false // for allow lists, default is deny
|
||||
|
||||
for i, allowDeny := range allowDenyEntries {
|
||||
if i == 0 {
|
||||
isAllowListing = allowDeny.Allowed.MustGet()
|
||||
if !isAllowListing {
|
||||
// if deny listing, then by default the IP is allowed until proven otherwise
|
||||
allowed = true
|
||||
}
|
||||
}
|
||||
|
||||
ok, err := allowDeny.IsIPAllowed(ip)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if isAllowListing && ok {
|
||||
allowed = true
|
||||
break
|
||||
} else if !isAllowListing && !ok {
|
||||
allowed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
// try to serve deny page
|
||||
if _, err := campaign.DenyPageID.Get(); err == nil {
|
||||
resp := m.serveDenyPageResponseDirect(req, reqCtx, campaign)
|
||||
return true, resp
|
||||
}
|
||||
|
||||
// if no deny page, block with 404
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ type CampaignOption struct {
|
||||
WithRecipientGroupCount bool
|
||||
WithAllowDeny bool
|
||||
WithDenyPage bool
|
||||
WithEvasionPage bool
|
||||
IncludeTestCampaigns bool
|
||||
}
|
||||
|
||||
@@ -105,6 +106,9 @@ func (r *Campaign) load(db *gorm.DB, options *CampaignOption) *gorm.DB {
|
||||
if options.WithDenyPage {
|
||||
db = db.Preload("DenyPage")
|
||||
}
|
||||
if options.WithEvasionPage {
|
||||
db = db.Preload("EvasionPage")
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -1500,6 +1504,21 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) {
|
||||
denyPageID.SetNull()
|
||||
}
|
||||
|
||||
var evasionPage *model.Page
|
||||
if row.EvasionPage != nil {
|
||||
ep, err := ToPage(row.EvasionPage)
|
||||
if err != nil {
|
||||
return nil, errs.Wrap(err)
|
||||
}
|
||||
evasionPage = ep
|
||||
}
|
||||
evasionPageID := nullable.NewNullNullable[uuid.UUID]()
|
||||
if row.EvasionPageID != nil {
|
||||
evasionPageID.Set(*row.EvasionPageID)
|
||||
} else {
|
||||
evasionPageID.SetNull()
|
||||
}
|
||||
|
||||
constraintWeekDays := nullable.NewNullNullable[vo.CampaignWeekDays]()
|
||||
if row.ConstraintWeekDays != nil {
|
||||
weekDays, err := vo.NewCampaignWeekDays(*row.ConstraintWeekDays)
|
||||
@@ -1573,6 +1592,8 @@ func ToCampaign(row *database.Campaign) (*model.Campaign, error) {
|
||||
AllowDeny: allowDeny,
|
||||
DenyPage: denyPage,
|
||||
DenyPageID: denyPageID,
|
||||
EvasionPage: evasionPage,
|
||||
EvasionPageID: evasionPageID,
|
||||
WebhookID: webhookID,
|
||||
NotableEventID: notableEventID,
|
||||
NotableEventName: notableEventName,
|
||||
|
||||
@@ -1336,6 +1336,15 @@ func (c *Campaign) UpdateByID(
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// handle evasion page ID
|
||||
if incoming.EvasionPageID.IsSpecified() {
|
||||
if incoming.EvasionPageID.IsNull() {
|
||||
current.EvasionPageID.SetNull()
|
||||
} else {
|
||||
current.EvasionPageID.Set(incoming.EvasionPageID.MustGet())
|
||||
}
|
||||
}
|
||||
// validate and update
|
||||
if err := current.Validate(); err != nil {
|
||||
return errs.Wrap(err)
|
||||
|
||||
@@ -188,7 +188,7 @@ func (t *Template) ApplyPageMock(content string) (*bytes.Buffer, error) {
|
||||
URLIdentifier: urlIdentifier,
|
||||
StateIdentifier: stateIdentifier,
|
||||
}
|
||||
return t.CreatePhishingPage(
|
||||
return t.CreatePhishingPageWithCampaign(
|
||||
domain,
|
||||
email,
|
||||
&campaignRecipientID,
|
||||
@@ -197,6 +197,7 @@ func (t *Template) ApplyPageMock(content string) (*bytes.Buffer, error) {
|
||||
campaignTemplate,
|
||||
"stateParam",
|
||||
"urlPath",
|
||||
nil, // no campaign for mock, so DenyURL will be empty string
|
||||
)
|
||||
}
|
||||
|
||||
@@ -279,6 +280,21 @@ func (t *Template) CreatePhishingPage(
|
||||
campaignTemplate *model.CampaignTemplate,
|
||||
stateParam string,
|
||||
urlPath string,
|
||||
) (*bytes.Buffer, error) {
|
||||
return t.CreatePhishingPageWithCampaign(domain, email, campaignRecipientID, recipient, contentToRender, campaignTemplate, stateParam, urlPath, nil)
|
||||
}
|
||||
|
||||
// CreatePhishingPageWithCampaign creates a new phishing page with optional campaign for deny URL support
|
||||
func (t *Template) CreatePhishingPageWithCampaign(
|
||||
domain *database.Domain,
|
||||
email *model.Email,
|
||||
campaignRecipientID *uuid.UUID,
|
||||
recipient *model.Recipient,
|
||||
contentToRender string,
|
||||
campaignTemplate *model.CampaignTemplate,
|
||||
stateParam string,
|
||||
urlPath string,
|
||||
campaign *model.Campaign,
|
||||
) (*bytes.Buffer, error) {
|
||||
w := bytes.NewBuffer([]byte{})
|
||||
id := campaignRecipientID.String()
|
||||
@@ -289,6 +305,20 @@ func (t *Template) CreatePhishingPage(
|
||||
urlIdentifier := campaignTemplate.URLIdentifier.Name.MustGet()
|
||||
stateIdentifier := campaignTemplate.StateIdentifier.Name.MustGet()
|
||||
url := fmt.Sprintf("%s?%s=%s&%s=%s", baseURL, urlIdentifier, id, stateIdentifier, stateParam)
|
||||
|
||||
// create deny URL if campaign and deny page exist
|
||||
denyURL := ""
|
||||
if campaign != nil {
|
||||
if _, err := campaign.DenyPageID.Get(); err == nil {
|
||||
// create a special state param that indicates deny page should be served
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tmpl, err := template.New("page").
|
||||
Funcs(TemplateFuncs()).
|
||||
Parse(contentToRender)
|
||||
@@ -296,10 +326,11 @@ func (t *Template) CreatePhishingPage(
|
||||
if err != nil {
|
||||
return w, fmt.Errorf("failed to parse page template: %s", err)
|
||||
}
|
||||
data := t.newTemplateDataMap(
|
||||
data := t.newTemplateDataMapWithDenyURL(
|
||||
id,
|
||||
baseURL,
|
||||
url,
|
||||
denyURL,
|
||||
recipient,
|
||||
"", // trackingPixelPath
|
||||
"", // trackingPixelMarkup
|
||||
@@ -406,6 +437,27 @@ func (t *Template) newTemplateDataMap(
|
||||
return &m
|
||||
}
|
||||
|
||||
// newTemplateDataMapWithDenyURL creates a new data map for templates with deny URL for evasion pages
|
||||
func (t *Template) newTemplateDataMapWithDenyURL(
|
||||
id string,
|
||||
baseURL string,
|
||||
url string,
|
||||
denyURL string,
|
||||
recipient *model.Recipient,
|
||||
trackingPixelPath string,
|
||||
trackingPixelMarkup string,
|
||||
email *model.Email,
|
||||
apiSender *model.APISender,
|
||||
) *map[string]any {
|
||||
// get the standard template data
|
||||
data := t.newTemplateDataMap(id, baseURL, url, recipient, trackingPixelPath, trackingPixelMarkup, email, apiSender)
|
||||
|
||||
// add the deny URL for evasion pages
|
||||
(*data)["DenyURL"] = denyURL
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
// TemplateFuncs returns template functions for templates
|
||||
func TemplateFuncs() template.FuncMap {
|
||||
return template.FuncMap{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
@@ -36,6 +37,10 @@ func ExtractClientIP(req *http.Request) string {
|
||||
}
|
||||
}
|
||||
}
|
||||
// strip port
|
||||
if host, _, err := net.SplitHostPort(clientIP); err == nil {
|
||||
clientIP = host
|
||||
}
|
||||
|
||||
return clientIP
|
||||
}
|
||||
|
||||
@@ -508,6 +508,7 @@ export class API {
|
||||
* @param {string[]} campaign.recipientGroupIDs []uuid
|
||||
* @param {string[]} campaign.allowDenyIDs []uuid
|
||||
* @param {string} campaign.denyPageID uuid
|
||||
* @param {string} campaign.evasionPageID uuid
|
||||
* @param {string} campaign.webhookID uuid
|
||||
* @param {Array} [campaign.constraintWeekDays]
|
||||
* @param {string} [campaign.constraintStartTime]
|
||||
@@ -530,6 +531,7 @@ export class API {
|
||||
recipientGroupIDs,
|
||||
allowDenyIDs,
|
||||
denyPageID,
|
||||
evasionPageID,
|
||||
webhookID,
|
||||
constraintWeekDays,
|
||||
constraintStartTime,
|
||||
@@ -551,6 +553,7 @@ export class API {
|
||||
recipientGroupIDs,
|
||||
allowDenyIDs,
|
||||
denyPageID,
|
||||
evasionPageID,
|
||||
webhookID,
|
||||
constraintWeekDays,
|
||||
constraintStartTime,
|
||||
@@ -787,6 +790,7 @@ export class API {
|
||||
* @param {string[]} [campaign.recipientGroupIDs] []uuid
|
||||
* @param {string[]} campaign.allowDenyIDs []uuid
|
||||
* @param {string} campaign.denyPageID uuid
|
||||
* @param {string} campaign.evasionPageID uuid
|
||||
* @param {string} campaign.webhookID uuid
|
||||
* @returns {Promise<ApiResponse>}
|
||||
*/
|
||||
@@ -810,6 +814,7 @@ export class API {
|
||||
recipientGroupIDs,
|
||||
allowDenyIDs,
|
||||
denyPageID,
|
||||
evasionPageID,
|
||||
webhookID
|
||||
}) => {
|
||||
return await postJSON(this.getPath(`/campaign/${id}`), {
|
||||
@@ -831,6 +836,7 @@ export class API {
|
||||
recipientGroupIDs,
|
||||
allowDenyIDs,
|
||||
denyPageID,
|
||||
evasionPageID,
|
||||
webhookID
|
||||
});
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
campaign_recipient_message_sent: true,
|
||||
campaign_recipient_message_failed: true,
|
||||
campaign_recipient_message_read: true,
|
||||
campaign_recipient_evasion_page_visited: true,
|
||||
campaign_recipient_before_page_visited: true,
|
||||
campaign_recipient_page_visited: true,
|
||||
campaign_recipient_after_page_visited: true,
|
||||
|
||||
@@ -10,6 +10,11 @@ const eventNameMap = {
|
||||
color: 'bg-failed-sending'
|
||||
},
|
||||
campaign_recipient_message_read: { name: 'Message Read', priority: 40, color: 'bg-message-read' },
|
||||
campaign_recipient_evasion_page_visited: {
|
||||
name: 'Evasion Page Visited',
|
||||
priority: 45,
|
||||
color: 'bg-evasion-page-visited'
|
||||
},
|
||||
campaign_recipient_before_page_visited: {
|
||||
name: 'Before Page Visited',
|
||||
priority: 50,
|
||||
|
||||
@@ -94,13 +94,13 @@
|
||||
icon: ''
|
||||
},
|
||||
{
|
||||
label: 'Add allow-list',
|
||||
label: 'Allow',
|
||||
value: 'allow',
|
||||
icon: ''
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Add deny-list',
|
||||
label: 'Deny',
|
||||
value: 'deny',
|
||||
icon: ''
|
||||
}
|
||||
@@ -148,6 +148,23 @@
|
||||
let scheduleType = 'basic';
|
||||
let allowDenyType = 'none';
|
||||
let allAllowDeny = [];
|
||||
let showSecurityOptions = false;
|
||||
|
||||
// reactive statement to enable security options when deny page is set
|
||||
$: if (formValues.denyPageValue && formValues.denyPageValue.trim() !== '') {
|
||||
showSecurityOptions = true;
|
||||
}
|
||||
|
||||
// reactive statement to clear evasion page and IP filtering when deny page is cleared
|
||||
$: if (!formValues.denyPageValue) {
|
||||
if (formValues.evasionPageValue) {
|
||||
formValues.evasionPageValue = null;
|
||||
}
|
||||
if (allowDenyType !== 'none') {
|
||||
allowDenyType = 'none';
|
||||
formValues.allowDeny = [];
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSendField = 'Email';
|
||||
const defaultSendOrder = 'Random';
|
||||
@@ -228,6 +245,7 @@
|
||||
recipientGroups: [],
|
||||
allowDeny: [],
|
||||
denyPageValue: null,
|
||||
evasionPageValue: null,
|
||||
constraintWeekDays: [],
|
||||
contraintStartTime: null,
|
||||
contraintEndTime: null,
|
||||
@@ -406,6 +424,15 @@
|
||||
};
|
||||
|
||||
const validateMisc = () => {
|
||||
// validate that deny page is selected if evasion page or IP filtering is used
|
||||
if (formValues.evasionPageValue && !formValues.denyPageValue) {
|
||||
modalError = 'Deny page is required when using an evasion page';
|
||||
return false;
|
||||
}
|
||||
if (allowDenyType !== 'none' && !formValues.denyPageValue) {
|
||||
modalError = 'Deny page is required when using IP filtering';
|
||||
return false;
|
||||
}
|
||||
return checkCurrentStepValidity();
|
||||
};
|
||||
|
||||
@@ -573,6 +600,7 @@
|
||||
recipientGroupIDs: recipientGroupIDs,
|
||||
allowDenyIDs: allowDenyIDs,
|
||||
denyPageID: denyPageMap.byValueOrNull(formValues.denyPageValue),
|
||||
evasionPageID: denyPageMap.byValueOrNull(formValues.evasionPageValue),
|
||||
constraintWeekDays: weekDaysAvailableToBinary(formValues.constraintWeekDays),
|
||||
constraintStartTime: contraintStartTimeUTC,
|
||||
constraintEndTime: contraintEndTimeUTC,
|
||||
@@ -636,6 +664,7 @@
|
||||
recipientGroupIDs: recipientGroupIDs,
|
||||
allowDenyIDs: allowDenyIDs,
|
||||
denyPageID: denyPageMap.byValueOrNull(formValues.denyPageValue),
|
||||
evasionPageID: denyPageMap.byValueOrNull(formValues.evasionPageValue),
|
||||
webhookID: webhookMap.byValueOrNull(formValues.webhookValue)
|
||||
});
|
||||
|
||||
@@ -752,6 +781,7 @@
|
||||
recipientGroups: [],
|
||||
allowDeny: [],
|
||||
denyPageValue: null,
|
||||
evasionPageValue: null,
|
||||
constraintWeekDays: [],
|
||||
contraintStartTime: null,
|
||||
contraintEndTime: null,
|
||||
@@ -779,7 +809,6 @@
|
||||
|
||||
const onChangeAllowDenyType = () => {
|
||||
formValues.allowDeny = [];
|
||||
formValues.denyPageValue = null;
|
||||
setAllowDenyType(allAllowDeny);
|
||||
};
|
||||
|
||||
@@ -881,6 +910,10 @@
|
||||
if (campaign.denyPage) {
|
||||
formValues.denyPageValue = campaign.denyPage.name;
|
||||
}
|
||||
|
||||
if (campaign.evasionPage) {
|
||||
formValues.evasionPageValue = campaign.evasionPage.name;
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
@@ -1462,36 +1495,6 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<SelectSquare
|
||||
label="IP filtering"
|
||||
options={ipFilterOptions}
|
||||
bind:value={allowDenyType}
|
||||
onChange={() => {
|
||||
onChangeAllowDenyType();
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if allowDenyType !== 'none'}
|
||||
<TextFieldMultiSelect
|
||||
id="allowDenyIDs"
|
||||
toolTipText="Select the IP groups to allow or block"
|
||||
bind:value={formValues.allowDeny}
|
||||
options={Array.from(allowDenyMap.values())}>Lists</TextFieldMultiSelect
|
||||
>
|
||||
|
||||
<TextFieldSelect
|
||||
id="deny-page"
|
||||
bind:value={formValues.denyPageValue}
|
||||
optional
|
||||
onSelect={(page) => {
|
||||
formValues.denyPageValue = page;
|
||||
}}
|
||||
options={Array.from(denyPageMap.values())}>Blocked Access Page</TextFieldSelect
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<TextFieldSelect
|
||||
id="webhook"
|
||||
bind:value={formValues.webhookValue}
|
||||
@@ -1499,6 +1502,101 @@
|
||||
options={Array.from(webhookMap.values())}>Webhook</TextFieldSelect
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<SelectSquare
|
||||
optional
|
||||
label="Security Configuration"
|
||||
options={[
|
||||
{ value: false, label: 'Disabled' },
|
||||
{ value: true, label: 'Enabled' }
|
||||
]}
|
||||
bind:value={showSecurityOptions}
|
||||
onChange={() => {
|
||||
if (!showSecurityOptions) {
|
||||
formValues.denyPageValue = '';
|
||||
formValues.evasionPageValue = '';
|
||||
allowDenyType = 'none';
|
||||
formValues.allowDeny = [];
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if showSecurityOptions}
|
||||
<div class="mb-6">
|
||||
<TextFieldSelect
|
||||
id="deny-page"
|
||||
bind:value={formValues.denyPageValue}
|
||||
optional
|
||||
toolTipText="Page to show when access is denied. Required for evasion pages and IP filtering."
|
||||
onSelect={(page) => {
|
||||
formValues.denyPageValue = page;
|
||||
}}
|
||||
options={Array.from(denyPageMap.values())}>Deny Page</TextFieldSelect
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
{#if formValues.denyPageValue}
|
||||
<TextFieldSelect
|
||||
id="evasion-page"
|
||||
bind:value={formValues.evasionPageValue}
|
||||
optional
|
||||
toolTipText="Select an anti-bot/evasion page to be served before the first real page. If evasion fails, the deny page will be shown instead."
|
||||
onSelect={(page) => {
|
||||
formValues.evasionPageValue = page;
|
||||
}}
|
||||
options={Array.from(denyPageMap.values())}
|
||||
>Anti-bot / Evasion Page</TextFieldSelect
|
||||
>
|
||||
{:else}
|
||||
<div
|
||||
class="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600"
|
||||
>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
<strong>Anti-bot / Evasion Page</strong><br />
|
||||
You must select a deny page first to use evasion pages.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
{#if formValues.denyPageValue}
|
||||
<SelectSquare
|
||||
label="IP filtering"
|
||||
toolTipText="Filter access based on IP address lists"
|
||||
options={ipFilterOptions}
|
||||
width="small"
|
||||
bind:value={allowDenyType}
|
||||
onChange={() => {
|
||||
onChangeAllowDenyType();
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if allowDenyType !== 'none'}
|
||||
<div class="mt-4">
|
||||
<TextFieldMultiSelect
|
||||
id="allowDenyIDs"
|
||||
toolTipText="Select the IP groups to allow or block"
|
||||
bind:value={formValues.allowDeny}
|
||||
options={Array.from(allowDenyMap.values())}>Lists</TextFieldMultiSelect
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600"
|
||||
>
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm">
|
||||
<strong>IP filtering mode</strong><br />
|
||||
You must select a deny page first to use IP filtering.
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</FormColumn>
|
||||
</FormColumns>
|
||||
{:else if currentStep === 5}
|
||||
@@ -1748,6 +1846,13 @@
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if formValues.evasionPageValue}
|
||||
<span class="text-grayblue-dark font-medium">Evasion Page:</span>
|
||||
<span class="text-pc-darkblue dark:text-white"
|
||||
>{formValues.evasionPageValue}</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if formValues.anonymizeAt}
|
||||
<span class="text-grayblue-dark font-medium">Anonymize at:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
|
||||
@@ -216,6 +216,8 @@
|
||||
campaign.saveSubmittedData = t.saveSubmittedData;
|
||||
campaign.isAnonymous = t.isAnonymous;
|
||||
campaign.allowDeny = t.allowDeny;
|
||||
campaign.denyPage = t.denyPage;
|
||||
campaign.evasionPage = t.evasionPage;
|
||||
campaign.webhookID = t.webhookID;
|
||||
campaign.template = templateMap.byKey(t.templateID);
|
||||
campaign.recipientGroups = t.recipientGroupIDs.map((id) => recipientGroupMap.byKey(id));
|
||||
@@ -1286,6 +1288,24 @@
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Deny Page:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{#if campaign.denyPage}
|
||||
{campaign.denyPage.name}
|
||||
{:else}
|
||||
None
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Evasion Page:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{#if campaign.evasionPage}
|
||||
{campaign.evasionPage.name}
|
||||
{:else}
|
||||
None
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<span class="text-grayblue-dark font-medium">Webhook:</span>
|
||||
<span class="text-pc-darkblue dark:text-white">
|
||||
{#if campaign.webhookID}
|
||||
|
||||
Reference in New Issue
Block a user