commit shit
Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
This commit is contained in:
parent
9f2f163d78
commit
920af2c506
5 changed files with 82 additions and 6 deletions
23
Dockerfile
Normal file
23
Dockerfile
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
FROM python:3.12 AS base
|
||||||
|
|
||||||
|
FROM base AS python-deps
|
||||||
|
|
||||||
|
# Install pipenv and compilation dependencies
|
||||||
|
RUN pip install pipenv
|
||||||
|
|
||||||
|
# Install python dependencies in /.venv
|
||||||
|
COPY Pipfile Pipfile.lock ./
|
||||||
|
RUN PIPENV_VENV_IN_PROJECT=1 pipenv install --deploy
|
||||||
|
|
||||||
|
|
||||||
|
FROM base AS runtime
|
||||||
|
|
||||||
|
# Copy virtual env from python-deps stage
|
||||||
|
COPY --from=python-deps /.venv /.venv
|
||||||
|
ENV PATH="/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Install application into container
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
CMD ["./pod.py"]
|
38
README.md
Normal file
38
README.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Kube-escape
|
||||||
|
|
||||||
|
Exfiltrates a Kubernetes API server over a websocket connection.
|
||||||
|
|
||||||
|
|
||||||
|
This is useful when needing to connect to internal clusters where the API is
|
||||||
|
only reachable via a VPN you don't have access to, or via a slow Windows citrix VM.
|
||||||
|
|
||||||
|
This works by running a pod inside the kubernetes (or AKS, or ECS, or GKE, or..) cluster.
|
||||||
|
|
||||||
|
The pod needs the two following requirements:
|
||||||
|
|
||||||
|
- It should be able to talk with the Interweb (a webserver your control), on HTTP or HTTPs
|
||||||
|
- It should be able to talk with the kubernetes API (supposing that it is not filtered some way)
|
||||||
|
|
||||||
|
|
||||||
|
It will then create a websocket connection to your webserver (running the [proxy.py](proxy.py) proxy application).
|
||||||
|
|
||||||
|
In order to reach the k8s api from your non-corporate-approved laptop, you can use the [client.py](client.py) client.
|
||||||
|
You need to provide the websocket link given by the pod. Once launched, a bidirectional TCP socket will
|
||||||
|
be created from your machine to the kubernetes api, going through the websocket proxy, and the undercover pod.
|
||||||
|
|
||||||
|
Of course, you still need to have valid credentials, through a kubeconfig file
|
||||||
|
|
||||||
|
|
||||||
|
You'll need to edit the kubeconfig file and change the api host to be your localhost.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
I guess you could proxy your websockets through an HTTPs endpoint. Wouldn't be bad.
|
||||||
|
However, the kubeapi proto is already over TLS, so it wouldn't add much value.
|
||||||
|
|
||||||
|
### Compression
|
||||||
|
|
||||||
|
Sadly it's not really possible (efficient-wise) to compress TLS data as it looks
|
||||||
|
random-ish.
|
6
client.py
Normal file → Executable file
6
client.py
Normal file → Executable file
|
@ -1,3 +1,4 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
import asyncio
|
import asyncio
|
||||||
import websockets
|
import websockets
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -22,6 +23,7 @@ async def handle_client(socket_reader, socket_writer):
|
||||||
try:
|
try:
|
||||||
print(f"New client connected socket {socket_id}")
|
print(f"New client connected socket {socket_id}")
|
||||||
m = conn.WSMsg(socket_id, conn.MsgType.CONNECT)
|
m = conn.WSMsg(socket_id, conn.MsgType.CONNECT)
|
||||||
|
print(f'TCP>WS: {m}')
|
||||||
await websocket.send(m.to_bytes())
|
await websocket.send(m.to_bytes())
|
||||||
|
|
||||||
# Forwarding data from client to WebSocket
|
# Forwarding data from client to WebSocket
|
||||||
|
@ -31,6 +33,7 @@ async def handle_client(socket_reader, socket_writer):
|
||||||
if not data:
|
if not data:
|
||||||
c = conn.WSMsg(socket_id, conn.MsgType.DISCONNECT)
|
c = conn.WSMsg(socket_id, conn.MsgType.DISCONNECT)
|
||||||
print(f"Client {socket_id} disconnected")
|
print(f"Client {socket_id} disconnected")
|
||||||
|
print(f'TCP>WS: {c}')
|
||||||
await websocket.send(c.to_bytes())
|
await websocket.send(c.to_bytes())
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -45,7 +48,9 @@ async def handle_client(socket_reader, socket_writer):
|
||||||
c = conn.WSMsg.from_bytes(message)
|
c = conn.WSMsg.from_bytes(message)
|
||||||
|
|
||||||
# XXX this is ugly, because it means that the data is sent twice or more if 2+ connections..
|
# XXX this is ugly, because it means that the data is sent twice or more if 2+ connections..
|
||||||
|
print('c.socketid', c.socketid, 'socket_id', socket_id)
|
||||||
if c.socketid == socket_id:
|
if c.socketid == socket_id:
|
||||||
|
print('ours')
|
||||||
if c.msg == conn.MsgType.DISCONNECT:
|
if c.msg == conn.MsgType.DISCONNECT:
|
||||||
print(f"Client {socket_id} disconnected")
|
print(f"Client {socket_id} disconnected")
|
||||||
break
|
break
|
||||||
|
@ -57,6 +62,7 @@ async def handle_client(socket_reader, socket_writer):
|
||||||
socket_writer.write(c.payload)
|
socket_writer.write(c.payload)
|
||||||
await socket_writer.drain()
|
await socket_writer.drain()
|
||||||
else:
|
else:
|
||||||
|
print('not ours')
|
||||||
print(f'WS>TCP@{socket_id}: ', hashlib.md5(message).hexdigest(), 'skipping')
|
print(f'WS>TCP@{socket_id}: ', hashlib.md5(message).hexdigest(), 'skipping')
|
||||||
|
|
||||||
|
|
||||||
|
|
3
pod.py
Normal file → Executable file
3
pod.py
Normal file → Executable file
|
@ -1,3 +1,4 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
import asyncio
|
import asyncio
|
||||||
import websockets
|
import websockets
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -15,6 +16,7 @@ async def handle_socket_read(socketid, tcpreader, ws):
|
||||||
print(f"New socket: {socketid}. Waiting on recv")
|
print(f"New socket: {socketid}. Waiting on recv")
|
||||||
while True:
|
while True:
|
||||||
data = await tcpreader.read(2024)
|
data = await tcpreader.read(2024)
|
||||||
|
print(f"TCP@{socketid} Received {len(data)} bytes")
|
||||||
if data == b'':
|
if data == b'':
|
||||||
print(f"TCP@{socketid} Connection closed")
|
print(f"TCP@{socketid} Connection closed")
|
||||||
c = conn.WSMsg(socketid, conn.MsgType.DISCONNECT)
|
c = conn.WSMsg(socketid, conn.MsgType.DISCONNECT)
|
||||||
|
@ -62,6 +64,7 @@ async def handle_ws_incoming(cfg, ws, sockets):
|
||||||
tcpreader, tcpwriter = sockets[socketid]
|
tcpreader, tcpwriter = sockets[socketid]
|
||||||
print(f'WS>TCP: {c}')
|
print(f'WS>TCP: {c}')
|
||||||
tcpwriter.write(c.payload)
|
tcpwriter.write(c.payload)
|
||||||
|
print('written')
|
||||||
|
|
||||||
|
|
||||||
def get_config():
|
def get_config():
|
||||||
|
|
18
proxy.py
Normal file → Executable file
18
proxy.py
Normal file → Executable file
|
@ -1,21 +1,26 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
import asyncio
|
import asyncio
|
||||||
import websockets
|
import websockets
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import os
|
||||||
|
|
||||||
import websockets.asyncio.server
|
import websockets.asyncio.server
|
||||||
|
|
||||||
# List to store connected clients
|
# List to store connected clients
|
||||||
connected_clients = set()
|
connected_clients: dict[str, set] = dict()
|
||||||
|
|
||||||
async def handler(websocket):
|
async def handler(websocket):
|
||||||
# Register the new client
|
# Register the new client
|
||||||
print(f"New client connected: {websocket}")
|
print(f"New client connected: {websocket}")
|
||||||
print(f"WRP: {websocket.request.path}")
|
print(f"WRP: {websocket.request.path}")
|
||||||
connected_clients.add(websocket)
|
if websocket.request.path not in connected_clients:
|
||||||
|
connected_clients[websocket.request.path] = set()
|
||||||
|
connected_clients[websocket.request.path].add(websocket)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for message in websocket:
|
async for message in websocket:
|
||||||
# Forward the message to all connected clients
|
# Forward the message to all connected clients on the same path
|
||||||
for client in connected_clients:
|
for client in connected_clients[websocket.request.path]:
|
||||||
if client != websocket:
|
if client != websocket:
|
||||||
print(f"WS>WS: ", hashlib.md5(message).hexdigest())
|
print(f"WS>WS: ", hashlib.md5(message).hexdigest())
|
||||||
await client.send(message)
|
await client.send(message)
|
||||||
|
@ -28,8 +33,9 @@ async def handler(websocket):
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
# Start the WebSocket server
|
# Start the WebSocket server
|
||||||
server = await websockets.asyncio.server.serve(handler, "localhost", 9999)
|
ws_port = os.environ.get("WS_PORT", 9999)
|
||||||
print("WebSocket server listening on ws://localhost:9999")
|
server = await websockets.asyncio.server.serve(handler, "::", ws_port)
|
||||||
|
print(f"WebSocket server listening on ws://[::]:{ws_port}")
|
||||||
await server.wait_closed()
|
await server.wait_closed()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
Loading…
Reference in a new issue