mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-16 16:57:25 +02:00
fix skip campaign template domain when mitm domain is first page
fix align extract IP on phishing server Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -675,7 +675,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
return false, fmt.Errorf("failed to get campaign template: %s", err)
|
||||
}
|
||||
// check that the requesters IP is allow listed
|
||||
ip := c.ClientIP()
|
||||
ip := utils.ExtractClientIP(c.Request)
|
||||
servedByIPFilter, err := s.checkIPFilter(c, ip, campaign, domain, &campaignID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -868,7 +868,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
}
|
||||
newEventID := uuid.New()
|
||||
campaignID := campaign.ID.MustGet()
|
||||
clientIP := vo.NewOptionalString64Must(c.ClientIP())
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
|
||||
submittedData := vo.NewEmptyOptionalString1MB()
|
||||
if campaign.SaveSubmittedData.MustGet() {
|
||||
@@ -1040,7 +1040,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED
|
||||
}
|
||||
eventID := cache.EventIDByName[eventName]
|
||||
clientIP := vo.NewOptionalString64Must(c.ClientIP())
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
|
||||
var visitEvent *model.CampaignEvent
|
||||
if !campaign.IsAnonymous.MustGet() {
|
||||
@@ -1262,7 +1262,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED
|
||||
}
|
||||
eventID := cache.EventIDByName[eventName]
|
||||
clientIP := vo.NewOptionalString64Must(c.ClientIP())
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
|
||||
var visitEvent *model.CampaignEvent
|
||||
if !campaign.IsAnonymous.MustGet() {
|
||||
|
||||
+143
-10
@@ -33,6 +33,7 @@ import (
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
"go.uber.org/zap"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
/*
|
||||
@@ -316,6 +317,9 @@ func (m *ProxyHandler) resolveSessionContext(req *http.Request, reqCtx *RequestC
|
||||
reqCtx.SessionID = newSession.ID
|
||||
reqCtx.SessionCreated = true
|
||||
reqCtx.Session = newSession
|
||||
|
||||
// register page visit event for MITM landing
|
||||
m.registerPageVisitEvent(req, newSession)
|
||||
} else {
|
||||
// load existing session
|
||||
sessionVal, exists := m.sessions.Load(reqCtx.SessionID)
|
||||
@@ -1920,15 +1924,7 @@ func (m *ProxyHandler) createCampaignSubmitEvent(session *ProxySession, captured
|
||||
submitDataEventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA]
|
||||
eventID := uuid.New()
|
||||
|
||||
// get real client ip
|
||||
clientIP := strings.SplitN(req.RemoteAddr, ":", 2)[0]
|
||||
proxyHeaders := []string{"X-Forwarded-For", "X-Real-IP", "X-Client-IP", "Connecting-IP", "True-Client-IP", "Client-IP"}
|
||||
for _, header := range proxyHeaders {
|
||||
if headerValue := req.Header.Get(header); headerValue != "" {
|
||||
clientIP = strings.SplitN(headerValue, ":", 2)[0]
|
||||
break
|
||||
}
|
||||
}
|
||||
clientIP := utils.ExtractClientIP(req)
|
||||
|
||||
event := &model.CampaignEvent{
|
||||
ID: &eventID,
|
||||
@@ -2216,6 +2212,7 @@ func (m *ProxyHandler) createDenyResponse(req *http.Request, reqCtx *RequestCont
|
||||
}
|
||||
|
||||
return m.createStatusResponse(404) // fallback
|
||||
|
||||
}
|
||||
|
||||
// createRedirectResponse creates a redirect response
|
||||
@@ -2232,7 +2229,143 @@ func (m *ProxyHandler) createRedirectResponse(url string) *http.Response {
|
||||
func (m *ProxyHandler) createStatusResponse(statusCode int) *http.Response {
|
||||
return &http.Response{
|
||||
StatusCode: statusCode,
|
||||
Header: make(map[string][]string),
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
}
|
||||
}
|
||||
|
||||
// registerPageVisitEvent registers a page visit event when a new MITM session is created
|
||||
func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *ProxySession) {
|
||||
if session.CampaignRecipientID == nil || session.CampaignID == nil || session.RecipientID == nil {
|
||||
return
|
||||
}
|
||||
|
||||
ctx := req.Context()
|
||||
|
||||
// get campaign template to determine page type
|
||||
templateID, err := session.Campaign.TemplateID.Get()
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to get template ID for page visit event", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
cTemplate, err := m.CampaignTemplateRepository.GetByID(ctx, &templateID, &repository.CampaignTemplateOption{})
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to get campaign template for page visit event", "error", err, "templateID", templateID)
|
||||
return
|
||||
}
|
||||
|
||||
// determine which page type this is
|
||||
currentPageType := m.getCurrentPageType(req, cTemplate, session)
|
||||
|
||||
// determine event name based on page type
|
||||
var eventName string
|
||||
switch currentPageType {
|
||||
case data.PAGE_TYPE_BEFORE:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_BEFORE_PAGE_VISITED
|
||||
case data.PAGE_TYPE_LANDING:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED
|
||||
case data.PAGE_TYPE_AFTER:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_AFTER_PAGE_VISITED
|
||||
default:
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED
|
||||
}
|
||||
|
||||
// get event ID
|
||||
eventID, exists := cache.EventIDByName[eventName]
|
||||
if !exists {
|
||||
m.logger.Errorw("unknown event name", "eventName", eventName)
|
||||
return
|
||||
}
|
||||
|
||||
// create visit event
|
||||
visitEventID := uuid.New()
|
||||
|
||||
clientIP := utils.ExtractClientIP(req)
|
||||
clientIPVO := vo.NewOptionalString64Must(clientIP)
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(req.UserAgent(), 0, 255))
|
||||
|
||||
var visitEvent *model.CampaignEvent
|
||||
if !session.Campaign.IsAnonymous.MustGet() {
|
||||
visitEvent = &model.CampaignEvent{
|
||||
ID: &visitEventID,
|
||||
CampaignID: session.CampaignID,
|
||||
RecipientID: session.RecipientID,
|
||||
IP: clientIPVO,
|
||||
UserAgent: userAgent,
|
||||
EventID: eventID,
|
||||
Data: vo.NewEmptyOptionalString1MB(),
|
||||
}
|
||||
} else {
|
||||
visitEvent = &model.CampaignEvent{
|
||||
ID: &visitEventID,
|
||||
CampaignID: session.CampaignID,
|
||||
RecipientID: nil,
|
||||
IP: vo.NewEmptyOptionalString64(),
|
||||
UserAgent: vo.NewEmptyOptionalString255(),
|
||||
EventID: eventID,
|
||||
Data: vo.NewEmptyOptionalString1MB(),
|
||||
}
|
||||
}
|
||||
|
||||
// save the visit event
|
||||
err = m.CampaignRepository.SaveEvent(ctx, visitEvent)
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to save MITM page visit event",
|
||||
"error", err,
|
||||
"campaignRecipientID", session.CampaignRecipientID.String(),
|
||||
"pageType", currentPageType,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// update most notable event for recipient if needed
|
||||
campaignRecipient, err := m.CampaignRecipientRepository.GetByID(ctx, session.CampaignRecipientID, &repository.CampaignRecipientOption{})
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to get campaign recipient for notable event update", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
currentNotableEventID, _ := campaignRecipient.NotableEventID.Get()
|
||||
if cache.IsMoreNotableCampaignRecipientEventID(¤tNotableEventID, eventID) {
|
||||
campaignRecipient.NotableEventID.Set(*eventID)
|
||||
err := m.CampaignRecipientRepository.UpdateByID(ctx, session.CampaignRecipientID, campaignRecipient)
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to update notable event for MITM visit",
|
||||
"campaignRecipientID", session.CampaignRecipientID.String(),
|
||||
"eventID", eventID.String(),
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// handle webhook for MITM page visit
|
||||
webhookID, err := m.CampaignRepository.GetWebhookIDByCampaignID(ctx, session.CampaignID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
m.logger.Errorw("failed to get webhook id by campaign id for MITM proxy",
|
||||
"campaignID", session.CampaignID.String(),
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
if webhookID != nil && currentPageType != data.PAGE_TYPE_DONE {
|
||||
err = m.CampaignService.HandleWebhook(
|
||||
ctx,
|
||||
webhookID,
|
||||
session.CampaignID,
|
||||
session.RecipientID,
|
||||
eventName,
|
||||
)
|
||||
if err != nil {
|
||||
m.logger.Errorw("failed to handle webhook for MITM page visit",
|
||||
"error", err,
|
||||
"campaignRecipientID", session.CampaignRecipientID.String(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
m.logger.Debugw("registered MITM page visit event",
|
||||
"campaignRecipientID", session.CampaignRecipientID.String(),
|
||||
"pageType", currentPageType,
|
||||
"eventName", eventName,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -528,6 +528,20 @@ func (a *APISender) Send(
|
||||
domain *model.Domain,
|
||||
mailTmpl *template.Template,
|
||||
email *model.Email,
|
||||
) error {
|
||||
return a.SendWithCustomURL(ctx, session, cTemplate, campaignRecipient, domain, mailTmpl, email, "")
|
||||
}
|
||||
|
||||
// SendWithCustomURL sends an API request with optional custom campaign URL
|
||||
func (a *APISender) SendWithCustomURL(
|
||||
ctx context.Context,
|
||||
session *model.Session,
|
||||
cTemplate *model.CampaignTemplate,
|
||||
campaignRecipient *model.CampaignRecipient,
|
||||
domain *model.Domain,
|
||||
mailTmpl *template.Template,
|
||||
email *model.Email,
|
||||
customCampaignURL string,
|
||||
) error {
|
||||
// get sender details
|
||||
apiSenderID, err := cTemplate.APISenderID.Get()
|
||||
@@ -550,19 +564,21 @@ func (a *APISender) Send(
|
||||
if apiSender == nil {
|
||||
return errors.New("api sender did not load")
|
||||
}
|
||||
|
||||
domainName := domain.Name.MustGet()
|
||||
urlIdentifier := cTemplate.URLIdentifier
|
||||
if urlIdentifier == nil {
|
||||
return errors.New("url identifier MUST be loaded in campaign template")
|
||||
}
|
||||
urlPath := cTemplate.URLPath.MustGet().String()
|
||||
url, headers, body, err := a.buildRequest(
|
||||
url, headers, body, err := a.buildRequestWithCustomURL(
|
||||
apiSender,
|
||||
domainName.String(),
|
||||
urlIdentifier.Name.MustGet(),
|
||||
urlPath,
|
||||
campaignRecipient,
|
||||
email,
|
||||
customCampaignURL,
|
||||
)
|
||||
resp, respBodyClose, err := a.sendRequest(
|
||||
context.Background(),
|
||||
@@ -717,6 +733,19 @@ func (a *APISender) buildRequest(
|
||||
urlPath string,
|
||||
campaignRecipient *model.CampaignRecipient,
|
||||
email *model.Email, // todo is this superfluous? it should be in the campaign recipient?
|
||||
) (*apiRequestURL, []*model.HTTPHeader, *apiRequestBody, error) {
|
||||
return a.buildRequestWithCustomURL(apiSender, domainName, urlKey, urlPath, campaignRecipient, email, "")
|
||||
}
|
||||
|
||||
// buildRequestWithCustomURL builds an API request with optional custom campaign URL
|
||||
func (a *APISender) buildRequestWithCustomURL(
|
||||
apiSender *model.APISender,
|
||||
domainName string,
|
||||
urlKey string,
|
||||
urlPath string,
|
||||
campaignRecipient *model.CampaignRecipient,
|
||||
email *model.Email,
|
||||
customCampaignURL string,
|
||||
) (*apiRequestURL, []*model.HTTPHeader, *apiRequestBody, error) {
|
||||
// setup headers
|
||||
apiReqHeaders, err := a.buildHeader(apiSender)
|
||||
@@ -739,6 +768,14 @@ func (a *APISender) buildRequest(
|
||||
email,
|
||||
apiSender,
|
||||
)
|
||||
|
||||
// override campaign URL if custom one is provided
|
||||
if customCampaignURL != "" {
|
||||
templateURL := fmt.Sprintf("https://%s%s?%s=%s", domainName, urlPath, urlKey, campaignRecipient.ID.MustGet().String())
|
||||
if customCampaignURL != templateURL {
|
||||
(*t)["URL"] = customCampaignURL
|
||||
}
|
||||
}
|
||||
var apiURL bytes.Buffer
|
||||
if err := urlTemplate.Execute(&apiURL, t); err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to execute url template: %s", err)
|
||||
|
||||
+248
-50
@@ -7,6 +7,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
go_errors "github.com/go-errors/errors"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
@@ -28,6 +30,7 @@ import (
|
||||
"github.com/phishingclub/phishingclub/log"
|
||||
"github.com/phishingclub/phishingclub/model"
|
||||
"github.com/phishingclub/phishingclub/repository"
|
||||
"github.com/phishingclub/phishingclub/utils"
|
||||
"github.com/phishingclub/phishingclub/validate"
|
||||
"github.com/phishingclub/phishingclub/vo"
|
||||
"github.com/wneessen/go-mail"
|
||||
@@ -1105,7 +1108,7 @@ func (c *Campaign) SaveTrackingPixelLoaded(
|
||||
Data: vo.NewOptionalString1MBMust(""),
|
||||
}
|
||||
} else {
|
||||
ip := vo.NewOptionalString64Must(ctx.ClientIP())
|
||||
ip := vo.NewOptionalString64Must(utils.ExtractClientIP(ctx.Request))
|
||||
ua := ctx.Request.UserAgent()
|
||||
if len(ua) > 255 {
|
||||
ua = strings.TrimSpace(ua[:255])
|
||||
@@ -1601,9 +1604,11 @@ func (c *Campaign) sendCampaignMessages(
|
||||
session,
|
||||
&templateID,
|
||||
&repository.CampaignTemplateOption{
|
||||
WithDomain: true,
|
||||
WithSMTPConfiguration: true,
|
||||
WithIdentifier: true,
|
||||
WithDomain: true,
|
||||
WithSMTPConfiguration: true,
|
||||
WithIdentifier: true,
|
||||
WithBeforeLandingProxy: true,
|
||||
WithLandingProxy: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -1741,7 +1746,16 @@ func (c *Campaign) sendCampaignMessages(
|
||||
fmt.Errorf("failed to update last attempted at: %s \nThis is critical for sending, aborting...", err),
|
||||
)
|
||||
}
|
||||
err = c.APISenderService.Send(
|
||||
// generate custom campaign URL if first page is MITM
|
||||
recipientID := campaignRecipient.ID.MustGet()
|
||||
customCampaignURL, urlErr := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
|
||||
if urlErr != nil {
|
||||
c.Logger.Errorw("failed to get campaign url for API sender", "error", urlErr)
|
||||
customCampaignURL = ""
|
||||
}
|
||||
|
||||
// send via API with custom URL (domain and template stay the same for assets)
|
||||
err = c.APISenderService.SendWithCustomURL(
|
||||
ctx,
|
||||
session,
|
||||
cTemplate,
|
||||
@@ -1749,6 +1763,7 @@ func (c *Campaign) sendCampaignMessages(
|
||||
domain,
|
||||
mailTmpl,
|
||||
email,
|
||||
customCampaignURL,
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to send message via. API", "error", err)
|
||||
@@ -1945,17 +1960,28 @@ func (c *Campaign) sendCampaignMessages(
|
||||
}
|
||||
}
|
||||
m.Subject(email.MailHeaderSubject.MustGet().String())
|
||||
domainName, err := domain.Name.Get()
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get domain name", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
urlIdentifier := cTemplate.URLIdentifier
|
||||
if urlIdentifier == nil {
|
||||
c.Logger.Error("url identifier is MUST be loaded for the campaign template")
|
||||
return fmt.Errorf("url identifier is MUST be loaded for the campaign template")
|
||||
}
|
||||
|
||||
// get template domain for assets and tracking pixel
|
||||
domainName, err := domain.Name.Get()
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get domain name", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
urlPath := cTemplate.URLPath.MustGet().String()
|
||||
|
||||
// generate custom campaign URL if first page is MITM
|
||||
recipientID := campaignRecipient.ID.MustGet()
|
||||
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get campaign url", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
t := c.TemplateService.CreateMail(
|
||||
domainName.String(),
|
||||
urlIdentifier.Name.MustGet(),
|
||||
@@ -1964,6 +1990,12 @@ func (c *Campaign) sendCampaignMessages(
|
||||
email,
|
||||
nil,
|
||||
)
|
||||
|
||||
// override campaign URL if it's different from template domain URL
|
||||
templateURL := fmt.Sprintf("https://%s%s?%s=%s", domainName.String(), urlPath, urlIdentifier.Name.MustGet(), recipientID.String())
|
||||
if customCampaignURL != templateURL {
|
||||
(*t)["URL"] = customCampaignURL
|
||||
}
|
||||
var bodyBuffer bytes.Buffer
|
||||
err = mailTmpl.Execute(&bodyBuffer, t)
|
||||
if err != nil {
|
||||
@@ -2004,13 +2036,22 @@ func (c *Campaign) sendCampaignMessages(
|
||||
attachmentAsEmail.Content = nullable.NewNullableWithValue(
|
||||
*vo.NewUnsafeOptionalString1MB(string(attachmentContent)),
|
||||
)
|
||||
attachmentStr, err := c.TemplateService.CreateMailBody(
|
||||
// generate custom campaign URL for attachment
|
||||
recipientID := campaignRecipient.ID.MustGet()
|
||||
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get campaign url for attachment", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
attachmentStr, err := c.TemplateService.CreateMailBodyWithCustomURL(
|
||||
urlIdentifier.Name.MustGet(),
|
||||
urlPath,
|
||||
domain,
|
||||
campaignRecipient,
|
||||
&attachmentAsEmail,
|
||||
nil,
|
||||
customCampaignURL,
|
||||
)
|
||||
if err != nil {
|
||||
return errs.Wrap(fmt.Errorf("failed to setup attachment with embedded content: %s", err))
|
||||
@@ -2589,7 +2630,9 @@ func (c *Campaign) GetCampaignEmailBody(
|
||||
session,
|
||||
&templateID,
|
||||
&repository.CampaignTemplateOption{
|
||||
WithIdentifier: true,
|
||||
WithIdentifier: true,
|
||||
WithBeforeLandingProxy: true,
|
||||
WithLandingProxy: true,
|
||||
},
|
||||
)
|
||||
emailID, err := cTemplate.EmailID.Get()
|
||||
@@ -2617,9 +2660,15 @@ func (c *Campaign) GetCampaignEmailBody(
|
||||
c.Logger.Errorw("failed to get message by id", "error", err)
|
||||
return "", errs.Wrap(err)
|
||||
}
|
||||
urlIdentifier := cTemplate.URLIdentifier
|
||||
if urlIdentifier == nil {
|
||||
return "", errors.New("url identifier is nil")
|
||||
}
|
||||
|
||||
// get template domain for assets and tracking pixel
|
||||
domainID, err := cTemplate.DomainID.Get()
|
||||
if err != nil {
|
||||
c.Logger.Infow("failed domain from template - template ID: %s", templateID)
|
||||
c.Logger.Infow("failed domain from template - template ID", "templateID", templateID)
|
||||
return "", errs.NewValidationError(
|
||||
errors.New("Campaign template has no domain"),
|
||||
)
|
||||
@@ -2634,24 +2683,30 @@ func (c *Campaign) GetCampaignEmailBody(
|
||||
c.Logger.Errorw("failed to get domain by id", "error", err)
|
||||
return "", errs.Wrap(err)
|
||||
}
|
||||
urlIdentifier := cTemplate.URLIdentifier
|
||||
if urlIdentifier == nil {
|
||||
return "", errors.New("url identifier is nil")
|
||||
}
|
||||
urlPath := cTemplate.URLPath.MustGet().String()
|
||||
|
||||
// generate custom campaign URL if first page is MITM
|
||||
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, campaignRecipientID)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get campaign url", "error", err)
|
||||
return "", errs.Wrap(err)
|
||||
}
|
||||
|
||||
// no audit on read
|
||||
return c.TemplateService.CreateMailBody(
|
||||
return c.TemplateService.CreateMailBodyWithCustomURL(
|
||||
urlIdentifier.Name.MustGet(),
|
||||
urlPath,
|
||||
domain,
|
||||
campaignRecipient,
|
||||
email,
|
||||
nil,
|
||||
customCampaignURL,
|
||||
)
|
||||
}
|
||||
|
||||
// GetLandingPageURLByCampaignRecipientID returns the URL for a campaign recipient
|
||||
// GetLandingPageURLByCampaignRecipientID generates the lure URL for a campaign recipient.
|
||||
// if the first page in the campaign flow is a mitm proxy, the url goes directly to the
|
||||
// mitm domain instead of redirecting through the template domain.
|
||||
func (c *Campaign) GetLandingPageURLByCampaignRecipientID(
|
||||
ctx context.Context,
|
||||
session *model.Session,
|
||||
@@ -2701,31 +2756,139 @@ func (c *Campaign) GetLandingPageURLByCampaignRecipientID(
|
||||
WithIdentifier: true,
|
||||
},
|
||||
)
|
||||
domainID, err := cTemplate.DomainID.Get()
|
||||
if err != nil {
|
||||
c.Logger.Infow("failed email from template - template ID", "templateID", templateID)
|
||||
return "", errs.NewValidationError(
|
||||
errors.New("Campaign template has no email"),
|
||||
)
|
||||
}
|
||||
domain, err := c.DomainService.GetByID(
|
||||
ctx,
|
||||
session,
|
||||
&domainID,
|
||||
&repository.DomainOption{},
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get domain by id", err)
|
||||
return "", errs.Wrap(err)
|
||||
}
|
||||
urlPath := cTemplate.URLPath.MustGet().String()
|
||||
baseURL := "https://" + domain.Name.MustGet().String()
|
||||
// determine if we should use mitm domain for first page
|
||||
var baseURL string
|
||||
var urlPath string
|
||||
idIdentifier := cTemplate.URLIdentifier.Name.MustGet()
|
||||
url := fmt.Sprintf("%s%s?%s=%s", baseURL, urlPath, idIdentifier, campaignRecipientID.String())
|
||||
|
||||
// check if first page is a mitm proxy
|
||||
firstPageProxy := c.getFirstPageProxy(cTemplate)
|
||||
if firstPageProxy != nil {
|
||||
// get the phishing domain for this proxy
|
||||
phishingDomain, err := c.getPhishingDomainForProxy(ctx, firstPageProxy)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get phishing domain for first page proxy", "error", err)
|
||||
// fallback to template domain
|
||||
firstPageProxy = nil
|
||||
} else {
|
||||
// use phishing domain directly
|
||||
startURL, err := firstPageProxy.StartURL.Get()
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get start url from first page proxy", "error", err)
|
||||
return "", errs.Wrap(err)
|
||||
}
|
||||
parsedStartURL, err := url.Parse(startURL.String())
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to parse start url from first page proxy", "error", err)
|
||||
return "", errs.Wrap(err)
|
||||
}
|
||||
baseURL = "https://" + phishingDomain
|
||||
urlPath = parsedStartURL.Path
|
||||
}
|
||||
}
|
||||
|
||||
if firstPageProxy == nil {
|
||||
// use template domain (current behavior)
|
||||
domainID, err := cTemplate.DomainID.Get()
|
||||
if err != nil {
|
||||
c.Logger.Infow("failed email from template - template ID", "templateID", templateID)
|
||||
return "", errs.NewValidationError(
|
||||
errors.New("Campaign template has no email"),
|
||||
)
|
||||
}
|
||||
domain, err := c.DomainService.GetByID(
|
||||
ctx,
|
||||
session,
|
||||
&domainID,
|
||||
&repository.DomainOption{},
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get domain by id", err)
|
||||
return "", errs.Wrap(err)
|
||||
}
|
||||
urlPath = cTemplate.URLPath.MustGet().String()
|
||||
baseURL = "https://" + domain.Name.MustGet().String()
|
||||
}
|
||||
|
||||
// build final url
|
||||
separator := "?"
|
||||
if strings.Contains(baseURL, "?") {
|
||||
separator = "&"
|
||||
}
|
||||
url := fmt.Sprintf("%s%s%s%s=%s", baseURL, urlPath, separator, idIdentifier, campaignRecipientID.String())
|
||||
// no audit on read
|
||||
return url, nil
|
||||
}
|
||||
|
||||
// getFirstPageProxy returns the proxy for the first page in the campaign flow
|
||||
// returns nil if the first page is not a proxy
|
||||
func (c *Campaign) getFirstPageProxy(template *model.CampaignTemplate) *model.Proxy {
|
||||
if template.BeforeLandingPageID != nil {
|
||||
return nil
|
||||
}
|
||||
if template.BeforeLandingProxyID != nil {
|
||||
return template.BeforeLandingProxy
|
||||
}
|
||||
if template.LandingPageID != nil {
|
||||
return nil
|
||||
}
|
||||
if template.LandingProxyID != nil {
|
||||
return template.LandingProxy
|
||||
}
|
||||
if template.AfterLandingPageID != nil {
|
||||
return nil
|
||||
}
|
||||
if template.AfterLandingProxyID != nil {
|
||||
return template.AfterLandingProxy
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getPhishingDomainForProxy finds the phishing domain that maps to the proxy's start url
|
||||
func (c *Campaign) getPhishingDomainForProxy(ctx context.Context, proxy *model.Proxy) (string, error) {
|
||||
startURL, err := proxy.StartURL.Get()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get start url from proxy: %w", err)
|
||||
}
|
||||
|
||||
proxyConfig, err := proxy.ProxyConfig.Get()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get proxy config: %w", err)
|
||||
}
|
||||
|
||||
// parse the proxy configuration to find domain mappings
|
||||
var rawConfig map[string]interface{}
|
||||
err = yaml.Unmarshal([]byte(proxyConfig.String()), &rawConfig)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse proxy config yaml: %w", err)
|
||||
}
|
||||
|
||||
// parse the start URL to get the target domain
|
||||
parsedStartURL, err := url.Parse(startURL.String())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse start url: %w", err)
|
||||
}
|
||||
startDomain := parsedStartURL.Host
|
||||
|
||||
// find the phishing domain mapping for the start URL domain
|
||||
for originalHost, domainData := range rawConfig {
|
||||
if originalHost == "proxy" || originalHost == "global" {
|
||||
continue
|
||||
}
|
||||
if originalHost == startDomain {
|
||||
if domainMap, ok := domainData.(map[string]interface{}); ok {
|
||||
if to, exists := domainMap["to"]; exists {
|
||||
if toStr, ok := to.(string); ok {
|
||||
return toStr, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no phishing domain mapping found for start url domain: %s", startDomain)
|
||||
}
|
||||
|
||||
// SetSentAtByCampaignRecipientID sets the sent at time for a recipient
|
||||
func (c *Campaign) SetSentAtByCampaignRecipientID(
|
||||
ctx context.Context,
|
||||
@@ -3086,9 +3249,11 @@ func (c *Campaign) sendSingleCampaignMessage(
|
||||
session,
|
||||
&templateID,
|
||||
&repository.CampaignTemplateOption{
|
||||
WithDomain: true,
|
||||
WithSMTPConfiguration: true,
|
||||
WithIdentifier: true,
|
||||
WithDomain: true,
|
||||
WithSMTPConfiguration: true,
|
||||
WithIdentifier: true,
|
||||
WithBeforeLandingProxy: true,
|
||||
WithLandingProxy: true,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -3152,8 +3317,16 @@ func (c *Campaign) sendSingleCampaignMessage(
|
||||
}
|
||||
|
||||
if isAPISenderCampaign {
|
||||
// send via API
|
||||
err = c.APISenderService.Send(
|
||||
// generate custom campaign URL if first page is MITM
|
||||
recipientID := campaignRecipient.ID.MustGet()
|
||||
customCampaignURL, urlErr := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
|
||||
if urlErr != nil {
|
||||
c.Logger.Errorw("failed to get campaign url for API sender", "error", urlErr)
|
||||
customCampaignURL = ""
|
||||
}
|
||||
|
||||
// send via API with custom URL (domain and template stay the same for assets)
|
||||
err = c.APISenderService.SendWithCustomURL(
|
||||
ctx,
|
||||
session,
|
||||
cTemplate,
|
||||
@@ -3161,6 +3334,7 @@ func (c *Campaign) sendSingleCampaignMessage(
|
||||
domain,
|
||||
mailTmpl,
|
||||
email,
|
||||
customCampaignURL,
|
||||
)
|
||||
} else {
|
||||
// send via SMTP
|
||||
@@ -3291,17 +3465,26 @@ func (c *Campaign) sendSingleEmailSMTP(
|
||||
m.Subject(email.MailHeaderSubject.MustGet().String())
|
||||
|
||||
// setup template variables
|
||||
domainName, err := domain.Name.Get()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
urlIdentifier := cTemplate.URLIdentifier
|
||||
if urlIdentifier == nil {
|
||||
return errors.New("url identifier must be loaded for the campaign template")
|
||||
}
|
||||
|
||||
// get template domain for assets and tracking pixel
|
||||
domainName, err := domain.Name.Get()
|
||||
if err != nil {
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
urlPath := cTemplate.URLPath.MustGet().String()
|
||||
|
||||
// generate custom campaign URL if first page is MITM
|
||||
recipientID := campaignRecipient.ID.MustGet()
|
||||
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get campaign url", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
t := c.TemplateService.CreateMail(
|
||||
domainName.String(),
|
||||
urlIdentifier.Name.MustGet(),
|
||||
@@ -3311,6 +3494,12 @@ func (c *Campaign) sendSingleEmailSMTP(
|
||||
nil,
|
||||
)
|
||||
|
||||
// override campaign URL if it's different from template domain URL
|
||||
templateURL := fmt.Sprintf("https://%s%s?%s=%s", domainName.String(), urlPath, urlIdentifier.Name.MustGet(), recipientID.String())
|
||||
if customCampaignURL != templateURL {
|
||||
(*t)["URL"] = customCampaignURL
|
||||
}
|
||||
|
||||
var bodyBuffer bytes.Buffer
|
||||
err = mailTmpl.Execute(&bodyBuffer, t)
|
||||
if err != nil {
|
||||
@@ -3351,13 +3540,22 @@ func (c *Campaign) sendSingleEmailSMTP(
|
||||
attachmentAsEmail.Content = nullable.NewNullableWithValue(
|
||||
*vo.NewUnsafeOptionalString1MB(string(attachmentContent)),
|
||||
)
|
||||
attachmentStr, err := c.TemplateService.CreateMailBody(
|
||||
// generate custom campaign URL for attachment
|
||||
recipientID := campaignRecipient.ID.MustGet()
|
||||
customCampaignURL, err := c.GetLandingPageURLByCampaignRecipientID(ctx, session, &recipientID)
|
||||
if err != nil {
|
||||
c.Logger.Errorw("failed to get campaign url for attachment", "error", err)
|
||||
return errs.Wrap(err)
|
||||
}
|
||||
|
||||
attachmentStr, err := c.TemplateService.CreateMailBodyWithCustomURL(
|
||||
urlIdentifier.Name.MustGet(),
|
||||
urlPath,
|
||||
domain,
|
||||
campaignRecipient,
|
||||
&attachmentAsEmail,
|
||||
nil,
|
||||
customCampaignURL,
|
||||
)
|
||||
if err != nil {
|
||||
return errs.Wrap(fmt.Errorf("failed to setup attachment with embedded content: %s", err))
|
||||
|
||||
@@ -208,6 +208,27 @@ func (t *Template) CreateMailBody(
|
||||
campaignRecipient *model.CampaignRecipient,
|
||||
email *model.Email,
|
||||
apiSender *model.APISender, // can be nil
|
||||
) (string, error) {
|
||||
return t.CreateMailBodyWithCustomURL(
|
||||
urlIdentifier,
|
||||
urlPath,
|
||||
domain,
|
||||
campaignRecipient,
|
||||
email,
|
||||
apiSender,
|
||||
"", // empty customURL means use template domain
|
||||
)
|
||||
}
|
||||
|
||||
// CreateMailBodyWithCustomURL returns a rendered mail body to string with optional custom campaign URL
|
||||
func (t *Template) CreateMailBodyWithCustomURL(
|
||||
urlIdentifier string,
|
||||
urlPath string,
|
||||
domain *model.Domain,
|
||||
campaignRecipient *model.CampaignRecipient,
|
||||
email *model.Email,
|
||||
apiSender *model.APISender, // can be nil
|
||||
customCampaignURL string, // if provided, overrides the default campaign URL
|
||||
) (string, error) {
|
||||
mailData := t.CreateMail(
|
||||
domain.Name.MustGet().String(),
|
||||
@@ -217,6 +238,11 @@ func (t *Template) CreateMailBody(
|
||||
email,
|
||||
apiSender,
|
||||
)
|
||||
|
||||
// override campaign URL if custom one is provided
|
||||
if customCampaignURL != "" {
|
||||
(*mailData)["URL"] = customCampaignURL
|
||||
}
|
||||
mailContentTemplate := template.New("mailContent")
|
||||
mailContentTemplate = mailContentTemplate.Funcs(TemplateFuncs())
|
||||
content, err := email.Content.Get()
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ExtractClientIP extracts the real client IP from an HTTP request,
|
||||
// checking common proxy headers in order of preference.
|
||||
// This provides consistent IP extraction across the application.
|
||||
func ExtractClientIP(req *http.Request) string {
|
||||
// start with direct connection IP
|
||||
clientIP := req.RemoteAddr
|
||||
|
||||
// check common proxy headers in order of preference
|
||||
proxyHeaders := []string{
|
||||
"CF-Connecting-IP", // cloudflare - most reliable for cloudflare setups
|
||||
"True-Client-IP", // cloudflare enterprise
|
||||
"X-Forwarded-For", // most common proxy header
|
||||
"X-Real-IP", // nginx standard
|
||||
"X-Client-IP", // some proxies
|
||||
"Connecting-IP", // some cdns
|
||||
"Client-IP", // some load balancers
|
||||
}
|
||||
|
||||
for _, header := range proxyHeaders {
|
||||
if headerValue := req.Header.Get(header); headerValue != "" {
|
||||
// take first IP if comma-separated list
|
||||
ip := strings.SplitN(headerValue, ",", 2)[0]
|
||||
ip = strings.TrimSpace(ip)
|
||||
|
||||
// use first non-empty value found
|
||||
if ip != "" {
|
||||
clientIP = ip
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return clientIP
|
||||
}
|
||||
Reference in New Issue
Block a user