mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-08-28 13:20:30 +02:00
added trusted ip forward header to phishing server
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -206,6 +206,7 @@ func NewControllers(
|
||||
CampaignService: services.Campaign,
|
||||
ExecPath: conf.RemoteBrowser.ExecPath,
|
||||
Enabled: conf.RemoteBrowser.Enabled,
|
||||
TrustedProxies: conf.IPSecurity.TrustedProxies,
|
||||
}
|
||||
|
||||
return &Controllers{
|
||||
|
||||
+11
-7
@@ -63,6 +63,7 @@ type Server struct {
|
||||
proxyServer *proxy.ProxyHandler
|
||||
ja4Middleware *middleware.JA4Middleware
|
||||
remoteBrowserWSPath string
|
||||
trustedProxies []string
|
||||
}
|
||||
|
||||
// NewServer returns a new server
|
||||
@@ -76,6 +77,7 @@ func NewServer(
|
||||
logger *zap.SugaredLogger,
|
||||
certMagicConfig *certmagic.Config,
|
||||
remoteBrowserWSPath string,
|
||||
trustedProxies []string,
|
||||
) *Server {
|
||||
// setup ja4 middleware for tls fingerprinting
|
||||
ja4Middleware := middleware.NewJA4Middleware(logger)
|
||||
@@ -95,6 +97,7 @@ func NewServer(
|
||||
services.IPAllowList,
|
||||
repositories.Option,
|
||||
services.Option,
|
||||
trustedProxies,
|
||||
)
|
||||
|
||||
// setup proxy session cleanup routine
|
||||
@@ -121,6 +124,7 @@ func NewServer(
|
||||
proxyServer: proxyServer,
|
||||
ja4Middleware: ja4Middleware,
|
||||
remoteBrowserWSPath: remoteBrowserWSPath,
|
||||
trustedProxies: trustedProxies,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -712,7 +716,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 := utils.ExtractClientIP(c.Request)
|
||||
ip := utils.ExtractClientIP(c.Request, s.trustedProxies)
|
||||
servedByIPFilter, err := s.checkIPFilter(c, ip, campaign, domain, &campaignID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -990,7 +994,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
submitDataEventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_SUBMITTED_DATA]
|
||||
newEventID := uuid.New()
|
||||
campaignID := campaign.ID.MustGet()
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
|
||||
submittedData := vo.NewEmptyOptionalString1MB()
|
||||
|
||||
@@ -1306,7 +1310,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
// only create synthetic event if no message_read event exists
|
||||
if !hasMessageRead {
|
||||
syntheticReadEventID := uuid.New()
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
|
||||
syntheticData := vo.NewOptionalString1MBMust("synthetic_from_page_visit")
|
||||
|
||||
@@ -1376,7 +1380,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
eventName = data.EVENT_CAMPAIGN_RECIPIENT_PAGE_VISITED
|
||||
}
|
||||
eventID := cache.EventIDByName[eventName]
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
|
||||
var visitEvent *model.CampaignEvent
|
||||
if !campaign.IsAnonymous.MustGet() {
|
||||
@@ -1669,7 +1673,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
// only create synthetic event if no message_read event exists
|
||||
if !hasMessageRead {
|
||||
syntheticReadEventID := uuid.New()
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
|
||||
syntheticData := vo.NewOptionalString1MBMust("synthetic_from_page_visit")
|
||||
|
||||
@@ -1738,7 +1742,7 @@ func (s *Server) checkAndServePhishingPage(
|
||||
|
||||
campaignEventID := cache.EventIDByName[eventName]
|
||||
eventID := uuid.New()
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
|
||||
var event *model.CampaignEvent
|
||||
if !campaign.IsAnonymous.MustGet() {
|
||||
@@ -1941,7 +1945,7 @@ func (s *Server) renderDenyPage(
|
||||
// log deny page visited event
|
||||
denyPageVisitEventID := uuid.New()
|
||||
eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED]
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request))
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(c.Request, s.trustedProxies))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(c.Request.UserAgent(), 0, MAX_USER_AGENT_SAVED))
|
||||
var event *model.CampaignEvent
|
||||
if !campaign.IsAnonymous.MustGet() {
|
||||
|
||||
@@ -59,6 +59,7 @@ func NewServices(
|
||||
certMagicConfig *certmagic.Config,
|
||||
certMagicCache *certmagic.Cache,
|
||||
filePath string,
|
||||
trustedProxies []string,
|
||||
) *Services {
|
||||
common := service.Common{
|
||||
Logger: logger,
|
||||
@@ -222,6 +223,7 @@ func NewServices(
|
||||
MicrosoftDeviceCodeRepository: repositories.MicrosoftDeviceCode,
|
||||
AttachmentPath: attachmentPath,
|
||||
RemoteBrowserService: remoteBrowser,
|
||||
TrustedProxies: trustedProxies,
|
||||
}
|
||||
// wire campaign service into microsoft device code service now that campaign is constructed
|
||||
microsoftDeviceCodeService.CampaignService = campaign
|
||||
|
||||
@@ -164,7 +164,8 @@ type RemoteBrowserController struct {
|
||||
ExecPath string
|
||||
// Enabled mirrors config.RemoteBrowserServerConfig.Enabled. When false
|
||||
// every endpoint returns 404 and the feature is fully unavailable.
|
||||
Enabled bool
|
||||
Enabled bool
|
||||
TrustedProxies []string
|
||||
}
|
||||
|
||||
func (m *RemoteBrowserController) isEnabled(g *gin.Context) bool {
|
||||
@@ -708,7 +709,7 @@ func (m *RemoteBrowserController) ServeVictim(g *gin.Context) {
|
||||
}
|
||||
}()
|
||||
|
||||
clientIP := utils.ExtractClientIP(g.Request)
|
||||
clientIP := utils.ExtractClientIP(g.Request, m.TrustedProxies)
|
||||
userAgent := g.Request.UserAgent()
|
||||
|
||||
// processEvent handles server-side effects for a RunEvent (DB writes, session state
|
||||
|
||||
@@ -231,6 +231,7 @@ func main() {
|
||||
certMagicConfig,
|
||||
certMagicCache,
|
||||
*flagFilePath,
|
||||
conf.IPSecurity.TrustedProxies,
|
||||
)
|
||||
// get entra-id options and setup msal client
|
||||
ssoOpt, err := services.SSO.GetSSOOptionWithoutAuth(context.Background())
|
||||
@@ -371,6 +372,7 @@ func main() {
|
||||
logger,
|
||||
certMagicConfig,
|
||||
rbWSPath,
|
||||
conf.IPSecurity.TrustedProxies,
|
||||
)
|
||||
|
||||
var r *gin.Engine
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/phishingclub/phishingclub/config"
|
||||
"github.com/phishingclub/phishingclub/utils"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -17,33 +16,8 @@ func NewAllowIPMiddleware(conf *config.Config, logger *zap.SugaredLogger) gin.Ha
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
c.RemoteIP()
|
||||
clientIP := c.ClientIP()
|
||||
allowed := false
|
||||
for _, allowedIP := range conf.IPSecurity.AdminAllowed {
|
||||
// check if the allowed entry is a CIDR
|
||||
if strings.Contains(allowedIP, "/") {
|
||||
_, ipNet, err := net.ParseCIDR(allowedIP)
|
||||
if err != nil {
|
||||
logger.Errorw("Invalid CIDR in allowed IPs",
|
||||
"cidr", allowedIP,
|
||||
"error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
ip := net.ParseIP(clientIP)
|
||||
if ipNet.Contains(ip) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
} else {
|
||||
// Direct IP comparison
|
||||
if clientIP == allowedIP {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
allowed := utils.IPMatchesList(clientIP, conf.IPSecurity.AdminAllowed)
|
||||
|
||||
if !allowed {
|
||||
logger.Infow("blocked unauthorized IP access attempt",
|
||||
|
||||
+10
-7
@@ -125,6 +125,7 @@ type ProxyHandler struct {
|
||||
OptionRepository *repository.Option
|
||||
OptionService *service.Option
|
||||
cookieName string
|
||||
trustedProxies []string
|
||||
}
|
||||
|
||||
func NewProxyHandler(
|
||||
@@ -142,6 +143,7 @@ func NewProxyHandler(
|
||||
ipAllowListService *service.IPAllowListService,
|
||||
optionRepo *repository.Option,
|
||||
optionService *service.Option,
|
||||
trustedProxies []string,
|
||||
) *ProxyHandler {
|
||||
// get proxy cookie name from database
|
||||
cookieName := "ps" // fallback default
|
||||
@@ -165,6 +167,7 @@ func NewProxyHandler(
|
||||
OptionRepository: optionRepo,
|
||||
OptionService: optionService,
|
||||
cookieName: cookieName,
|
||||
trustedProxies: trustedProxies,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3612,7 +3615,7 @@ func (m *ProxyHandler) createCampaignInfoEvent(session *service.ProxySession, ca
|
||||
}
|
||||
|
||||
eventID := uuid.New()
|
||||
clientIP := utils.ExtractClientIP(req)
|
||||
clientIP := utils.ExtractClientIP(req, m.trustedProxies)
|
||||
metadata := model.ExtractCampaignEventMetadataFromHTTPRequest(req, campaign)
|
||||
|
||||
event := &model.CampaignEvent{
|
||||
@@ -3682,7 +3685,7 @@ func (m *ProxyHandler) createCampaignSubmitEvent(session *service.ProxySession,
|
||||
eventID := uuid.New()
|
||||
// use the event creation below instead of service call
|
||||
|
||||
clientIP := utils.ExtractClientIP(req)
|
||||
clientIP := utils.ExtractClientIP(req, m.trustedProxies)
|
||||
|
||||
metadata := model.ExtractCampaignEventMetadataFromHTTPRequest(req, campaign)
|
||||
|
||||
@@ -4305,7 +4308,7 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *servic
|
||||
// only create synthetic event if no message_read event exists
|
||||
if !hasMessageRead {
|
||||
syntheticReadEventID := uuid.New()
|
||||
clientIP := utils.ExtractClientIP(req)
|
||||
clientIP := utils.ExtractClientIP(req, m.trustedProxies)
|
||||
clientIPVO := vo.NewOptionalString64Must(clientIP)
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(session.UserAgent, 0, 255))
|
||||
syntheticData := vo.NewOptionalString1MBMust("synthetic_from_page_visit")
|
||||
@@ -4384,7 +4387,7 @@ func (m *ProxyHandler) registerPageVisitEvent(req *http.Request, session *servic
|
||||
// create visit event
|
||||
visitEventID := uuid.New()
|
||||
|
||||
clientIP := utils.ExtractClientIP(req)
|
||||
clientIP := utils.ExtractClientIP(req, m.trustedProxies)
|
||||
clientIPVO := vo.NewOptionalString64Must(clientIP)
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(session.UserAgent, 0, 255))
|
||||
|
||||
@@ -5022,7 +5025,7 @@ func (m *ProxyHandler) registerDenyPageVisitEventDirect(req *http.Request, reqCt
|
||||
|
||||
eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_DENY_PAGE_VISITED]
|
||||
newEventID := uuid.New()
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req))
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req, m.trustedProxies))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(reqCtx.OriginalUserAgent, 0, 1000)) // MAX_USER_AGENT_SAVED equivalent
|
||||
|
||||
var event *model.CampaignEvent
|
||||
@@ -5096,7 +5099,7 @@ func (m *ProxyHandler) registerEvasionPageVisitEventDirect(req *http.Request, re
|
||||
|
||||
eventID := cache.EventIDByName[data.EVENT_CAMPAIGN_RECIPIENT_EVASION_PAGE_VISITED]
|
||||
newEventID := uuid.New()
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req))
|
||||
clientIP := vo.NewOptionalString64Must(utils.ExtractClientIP(req, m.trustedProxies))
|
||||
userAgent := vo.NewOptionalString255Must(utils.Substring(reqCtx.OriginalUserAgent, 0, 1000)) // MAX_USER_AGENT_SAVED equivalent
|
||||
|
||||
var event *model.CampaignEvent
|
||||
@@ -5158,7 +5161,7 @@ func (m *ProxyHandler) checkFilter(req *http.Request, reqCtx *RequestContext) (b
|
||||
campaignID := reqCtx.CampaignID
|
||||
|
||||
// extract client IP and strip port if present using net.SplitHostPort for IPv6 safety
|
||||
ip := utils.ExtractClientIP(req)
|
||||
ip := utils.ExtractClientIP(req, m.trustedProxies)
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil {
|
||||
ip = host
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ type Campaign struct {
|
||||
MicrosoftDeviceCodeRepository *repository.MicrosoftDeviceCode
|
||||
AttachmentPath string
|
||||
RemoteBrowserService *RemoteBrowser
|
||||
TrustedProxies []string
|
||||
}
|
||||
|
||||
// Create creates a new campaign
|
||||
@@ -1327,7 +1328,7 @@ func (c *Campaign) SaveTrackingPixelLoaded(
|
||||
Metadata: vo.NewEmptyOptionalString1MB(),
|
||||
}
|
||||
} else {
|
||||
ip := vo.NewOptionalString64Must(utils.ExtractClientIP(ctx.Request))
|
||||
ip := vo.NewOptionalString64Must(utils.ExtractClientIP(ctx.Request, c.TrustedProxies))
|
||||
ua := ctx.Request.UserAgent()
|
||||
if len(ua) > 255 {
|
||||
ua = strings.TrimSpace(ua[:255])
|
||||
|
||||
+40
-12
@@ -6,12 +6,22 @@ import (
|
||||
"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 {
|
||||
// ExtractClientIP extracts the real client IP from an HTTP request.
|
||||
// trustedProxies is a list of proxy IPs/CIDRs whose forwarded headers should
|
||||
// be trusted. When empty all forwarded headers are trusted (legacy behaviour).
|
||||
// When non-empty, forwarded headers are only honoured if RemoteAddr is in the
|
||||
// trusted list; otherwise RemoteAddr is returned directly.
|
||||
func ExtractClientIP(req *http.Request, trustedProxies []string) string {
|
||||
// start with direct connection IP
|
||||
clientIP := req.RemoteAddr
|
||||
// strip port for comparison and return value
|
||||
if host, _, err := net.SplitHostPort(clientIP); err == nil {
|
||||
clientIP = host
|
||||
}
|
||||
|
||||
if len(trustedProxies) > 0 && !IPMatchesList(clientIP, trustedProxies) {
|
||||
return clientIP
|
||||
}
|
||||
|
||||
// check common proxy headers in order of preference
|
||||
proxyHeaders := []string{
|
||||
@@ -29,18 +39,36 @@ func ExtractClientIP(req *http.Request) string {
|
||||
// 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
|
||||
if host, _, err := net.SplitHostPort(ip); err == nil {
|
||||
return host
|
||||
}
|
||||
return ip
|
||||
}
|
||||
}
|
||||
}
|
||||
// strip port
|
||||
if host, _, err := net.SplitHostPort(clientIP); err == nil {
|
||||
clientIP = host
|
||||
}
|
||||
|
||||
return clientIP
|
||||
}
|
||||
|
||||
// IPMatchesList reports whether ip matches any entry in the list, which may
|
||||
// contain plain IPs or CIDR ranges.
|
||||
func IPMatchesList(ip string, list []string) bool {
|
||||
parsed := net.ParseIP(ip)
|
||||
for _, entry := range list {
|
||||
if strings.Contains(entry, "/") {
|
||||
_, ipNet, err := net.ParseCIDR(entry)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if parsed != nil && ipNet.Contains(parsed) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if ip == entry {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user