mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-07-07 04:47:55 +02:00
Add captured once behavior to device code phishing, update template keys and fix unneeded api calls
Signed-off-by: Ronni Skansing <rskansing@gmail.com>
This commit is contained in:
@@ -40,6 +40,11 @@ type MicrosoftDeviceCode struct {
|
||||
// captured is true after successfully polling and getting a token
|
||||
Captured bool `gorm:"not null;default:false"`
|
||||
|
||||
// captured_once controls whether a captured entry is returned as-is on subsequent
|
||||
// GetOrCreateDeviceCode calls instead of being deleted and replaced with a fresh code.
|
||||
// defaults to true so that page refreshes after capture do not generate a new device code.
|
||||
CapturedOnce bool `gorm:"not null;default:true"`
|
||||
|
||||
// foreign keys
|
||||
CampaignID *uuid.UUID `gorm:"not null;type:uuid;index;"`
|
||||
RecipientID *uuid.UUID `gorm:"type:uuid;index;"`
|
||||
|
||||
@@ -30,6 +30,11 @@ type MicrosoftDeviceCode struct {
|
||||
|
||||
Captured bool `json:"captured"`
|
||||
|
||||
// CapturedOnce controls whether a captured entry is returned as-is on subsequent
|
||||
// GetOrCreateDeviceCode calls instead of being deleted and replaced with a fresh code.
|
||||
// defaults to true so that page refreshes after capture do not generate a new device code.
|
||||
CapturedOnce bool `json:"capturedOnce"`
|
||||
|
||||
CampaignID nullable.Nullable[uuid.UUID] `json:"campaignId"`
|
||||
RecipientID nullable.Nullable[uuid.UUID] `json:"recipientId"`
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ func (r *MicrosoftDeviceCode) Insert(
|
||||
"refresh_token": "",
|
||||
"id_token": "",
|
||||
"captured": false,
|
||||
"captured_once": entry.CapturedOnce,
|
||||
}
|
||||
if v, err := entry.CampaignID.Get(); err == nil {
|
||||
row["campaign_id"] = v.String()
|
||||
@@ -168,6 +169,7 @@ func toMicrosoftDeviceCode(row *database.MicrosoftDeviceCode) *model.MicrosoftDe
|
||||
RefreshToken: row.RefreshToken,
|
||||
IDToken: row.IDToken,
|
||||
Captured: row.Captured,
|
||||
CapturedOnce: row.CapturedOnce,
|
||||
CampaignID: campaignID,
|
||||
RecipientID: recipientID,
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ type MicrosoftDeviceCodeOptions struct {
|
||||
TenantID string
|
||||
Resource string
|
||||
Scope string
|
||||
// CapturedOnce controls whether a captured entry is returned as-is on subsequent
|
||||
// GetOrCreateDeviceCode calls instead of being replaced with a fresh code.
|
||||
// nil means unset — applyDeviceCodeDefaults will default it to true.
|
||||
CapturedOnce *bool
|
||||
}
|
||||
|
||||
// tenantIDPattern matches valid microsoft tenant identifiers:
|
||||
@@ -97,6 +101,11 @@ func applyDeviceCodeDefaults(opts *MicrosoftDeviceCodeOptions) {
|
||||
if opts.Scope == "" {
|
||||
opts.Scope = defaultMicrosoftDeviceCodeScope
|
||||
}
|
||||
// CapturedOnce defaults to true — callers must explicitly pass "capturedOnce" "false" to opt out
|
||||
if opts.CapturedOnce == nil {
|
||||
t := true
|
||||
opts.CapturedOnce = &t
|
||||
}
|
||||
}
|
||||
|
||||
// microsoftDeviceCodeResponse is the json response from microsoft's device code endpoint
|
||||
@@ -219,6 +228,11 @@ func (s *MicrosoftDeviceCode) GetOrCreateDeviceCode(
|
||||
}
|
||||
|
||||
if existing != nil {
|
||||
// when captured_once is set and the entry is already captured, return it as-is so that
|
||||
// a page refresh does not generate a new device code and invalidate the captured one.
|
||||
if existing.Captured && existing.CapturedOnce {
|
||||
return existing, nil
|
||||
}
|
||||
// return valid non-captured, non-expired, not-about-to-expire entry as-is
|
||||
if !existing.Captured && !existing.IsExpired() && !existing.ExpiresWithin(5*time.Minute) {
|
||||
return existing, nil
|
||||
@@ -252,6 +266,7 @@ func (s *MicrosoftDeviceCode) GetOrCreateDeviceCode(
|
||||
TenantID: opts.TenantID,
|
||||
Scope: opts.Scope,
|
||||
Captured: false,
|
||||
CapturedOnce: *opts.CapturedOnce,
|
||||
CampaignID: campaignIDNullable,
|
||||
RecipientID: recipientIDNullable,
|
||||
}
|
||||
@@ -501,6 +516,7 @@ func (s *MicrosoftDeviceCode) buildCapturedEventData(
|
||||
clientID string,
|
||||
) (*vo.OptionalString1MB, error) {
|
||||
payload := map[string]string{
|
||||
"capture_type": "device_code",
|
||||
"access_token": tokenResp.AccessToken,
|
||||
"id_token": tokenResp.IDToken,
|
||||
"refresh_token": tokenResp.RefreshToken,
|
||||
|
||||
@@ -473,6 +473,24 @@ func (t *Template) CreatePhishingPageWithCampaignAndRecipient(
|
||||
excludeRecipientID = &rid
|
||||
}
|
||||
(*data)["RandomRecipient"] = t.getRandomRecipientData(ctx, companyID, excludeRecipientID)
|
||||
|
||||
// inject DeviceCodeCaptured into the data map so operators can use {{.DeviceCodeCaptured}}
|
||||
// in page templates. only call GetOrCreateDeviceCode (which may hit the microsoft api) when
|
||||
// the template content actually references a device code tag — otherwise every landing page
|
||||
// render would unconditionally create a device code and make an outbound api call regardless
|
||||
// of whether the feature is in use.
|
||||
(*data)["DeviceCodeCaptured"] = false
|
||||
usesDeviceCode := strings.Contains(contentToRender, "DeviceCodeCaptured") ||
|
||||
strings.Contains(contentToRender, "MicrosoftDeviceCode")
|
||||
if usesDeviceCode && t.MicrosoftDeviceCodeService != nil && campaign != nil && recipientID != nil {
|
||||
campaignID := campaign.ID.MustGet()
|
||||
if *recipientID != uuid.Nil && campaignID != uuid.Nil {
|
||||
if entry, dcErr := t.MicrosoftDeviceCodeService.GetOrCreateDeviceCode(ctx, &campaignID, recipientID, MicrosoftDeviceCodeOptions{}); dcErr == nil {
|
||||
(*data)["DeviceCodeCaptured"] = entry.Captured
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = tmpl.Execute(w, data)
|
||||
if err != nil {
|
||||
return w, fmt.Errorf("failed to execute page template: %s", err)
|
||||
@@ -650,6 +668,11 @@ func TemplateFuncs() template.FuncMap {
|
||||
"MicrosoftDeviceCodeURL": func(args ...string) (string, error) {
|
||||
return "https://microsoft.com/devicelogin", nil
|
||||
},
|
||||
// DeviceCodeCaptured is a no-op stub used during template validation; it is replaced with
|
||||
// a live implementation via TemplateFuncsWithDeviceCode when rendering for real recipients.
|
||||
"DeviceCodeCaptured": func(args ...string) (bool, error) {
|
||||
return false, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,6 +717,9 @@ func (t *Template) TemplateFuncsWithDeviceCode(
|
||||
opts.Resource = args[i+1]
|
||||
case "scope":
|
||||
opts.Scope = args[i+1]
|
||||
case "capturedOnce":
|
||||
v := args[i+1] != "false"
|
||||
opts.CapturedOnce = &v
|
||||
}
|
||||
}
|
||||
return opts
|
||||
@@ -717,6 +743,20 @@ func (t *Template) TemplateFuncsWithDeviceCode(
|
||||
return entry.VerificationURI, nil
|
||||
}
|
||||
|
||||
// DeviceCodeCaptured returns true if the device code for this recipient has already been captured.
|
||||
// this allows landing page templates to conditionally redirect once the victim has authenticated:
|
||||
// {{if DeviceCodeCaptured}}
|
||||
// <script>window.location.href = "{{.URL}}";</script>
|
||||
// {{end}}
|
||||
funcs["DeviceCodeCaptured"] = func(args ...string) (bool, error) {
|
||||
opts := parseDeviceCodeOpts(args)
|
||||
entry, err := t.MicrosoftDeviceCodeService.GetOrCreateDeviceCode(ctx, campaignID, recipientID, opts)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("DeviceCodeCaptured: failed to get or create device code: %w", err)
|
||||
}
|
||||
return entry.Captured, nil
|
||||
}
|
||||
|
||||
return funcs
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,9 @@
|
||||
|
||||
// device code templates are only available in blackbox (red team phishing) mode
|
||||
const deviceCodeTemplates = [
|
||||
{ label: 'Device Code (user code)', text: '{{MicrosoftDeviceCode}}' },
|
||||
{ label: 'Device Code (verification URL)', text: '{{MicrosoftDeviceCodeURL}}' }
|
||||
{ label: 'User Code', text: '{{MicrosoftDeviceCode}}' },
|
||||
{ label: 'Verification URL', text: '{{MicrosoftDeviceCodeURL}}' },
|
||||
{ label: 'Captured', text: '{{.DeviceCodeCaptured}}' }
|
||||
];
|
||||
|
||||
$: computedTemplates = (() => {
|
||||
@@ -455,7 +456,8 @@
|
||||
.replaceAll('{{.BaseURL}}', _baseURL)
|
||||
.replaceAll('{{.URL}}', _url)
|
||||
.replaceAll('{{MicrosoftDeviceCode}}', 'ABCD-1234')
|
||||
.replaceAll('{{MicrosoftDeviceCodeURL}}', 'https://microsoft.com/devicelogin');
|
||||
.replaceAll('{{MicrosoftDeviceCodeURL}}', 'https://microsoft.com/devicelogin')
|
||||
.replaceAll('{{.DeviceCodeCaptured}}', 'false');
|
||||
case 'email':
|
||||
return text
|
||||
.replaceAll('{{.FirstName}}', 'Alice')
|
||||
@@ -481,7 +483,8 @@
|
||||
.replaceAll('{{.BaseURL}}', _baseURL)
|
||||
.replaceAll('{{.URL}}', _url)
|
||||
.replaceAll('{{MicrosoftDeviceCode}}', 'ABCD-1234')
|
||||
.replaceAll('{{MicrosoftDeviceCodeURL}}', 'https://microsoft.com/devicelogin');
|
||||
.replaceAll('{{MicrosoftDeviceCodeURL}}', 'https://microsoft.com/devicelogin')
|
||||
.replaceAll('{{.DeviceCodeCaptured}}', 'false');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1076,7 +1076,7 @@
|
||||
const parsedData = JSON.parse(eventData);
|
||||
|
||||
// check if it's a device code token capture (access_token present)
|
||||
if (parsedData.access_token) {
|
||||
if (parsedData.capture_type === 'device_code' && parsedData.access_token) {
|
||||
const tokenPayload = {
|
||||
access_token: parsedData.access_token,
|
||||
refresh_token: parsedData.refresh_token || '',
|
||||
|
||||
Reference in New Issue
Block a user