commit shit

Signed-off-by: Frank Villaro-Dixon <frank@villaro-dixon.eu>
This commit is contained in:
Frank Villaro-Dixon 2024-08-28 00:58:17 +02:00
parent 9f2f163d78
commit 920af2c506
5 changed files with 82 additions and 6 deletions

23
Dockerfile Normal file
View 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
View 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
View file

@ -1,3 +1,4 @@
#!/usr/bin/env python3
import asyncio
import websockets
import hashlib
@ -22,6 +23,7 @@ async def handle_client(socket_reader, socket_writer):
try:
print(f"New client connected socket {socket_id}")
m = conn.WSMsg(socket_id, conn.MsgType.CONNECT)
print(f'TCP>WS: {m}')
await websocket.send(m.to_bytes())
# Forwarding data from client to WebSocket
@ -31,6 +33,7 @@ async def handle_client(socket_reader, socket_writer):
if not data:
c = conn.WSMsg(socket_id, conn.MsgType.DISCONNECT)
print(f"Client {socket_id} disconnected")
print(f'TCP>WS: {c}')
await websocket.send(c.to_bytes())
break
@ -45,7 +48,9 @@ async def handle_client(socket_reader, socket_writer):
c = conn.WSMsg.from_bytes(message)
# 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:
print('ours')
if c.msg == conn.MsgType.DISCONNECT:
print(f"Client {socket_id} disconnected")
break
@ -57,6 +62,7 @@ async def handle_client(socket_reader, socket_writer):
socket_writer.write(c.payload)
await socket_writer.drain()
else:
print('not ours')
print(f'WS>TCP@{socket_id}: ', hashlib.md5(message).hexdigest(), 'skipping')

3
pod.py Normal file → Executable file
View file

@ -1,3 +1,4 @@
#!/usr/bin/env python3
import asyncio
import websockets
import uuid
@ -15,6 +16,7 @@ async def handle_socket_read(socketid, tcpreader, ws):
print(f"New socket: {socketid}. Waiting on recv")
while True:
data = await tcpreader.read(2024)
print(f"TCP@{socketid} Received {len(data)} bytes")
if data == b'':
print(f"TCP@{socketid} Connection closed")
c = conn.WSMsg(socketid, conn.MsgType.DISCONNECT)
@ -62,6 +64,7 @@ async def handle_ws_incoming(cfg, ws, sockets):
tcpreader, tcpwriter = sockets[socketid]
print(f'WS>TCP: {c}')
tcpwriter.write(c.payload)
print('written')
def get_config():

18
proxy.py Normal file → Executable file
View file

@ -1,21 +1,26 @@
#!/usr/bin/env python3
import asyncio
import websockets
import hashlib
import os
import websockets.asyncio.server
# List to store connected clients
connected_clients = set()
connected_clients: dict[str, set] = dict()
async def handler(websocket):
# Register the new client
print(f"New client connected: {websocket}")
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:
async for message in websocket:
# Forward the message to all connected clients
for client in connected_clients:
# Forward the message to all connected clients on the same path
for client in connected_clients[websocket.request.path]:
if client != websocket:
print(f"WS>WS: ", hashlib.md5(message).hexdigest())
await client.send(message)
@ -28,8 +33,9 @@ async def handler(websocket):
async def main():
# Start the WebSocket server
server = await websockets.asyncio.server.serve(handler, "localhost", 9999)
print("WebSocket server listening on ws://localhost:9999")
ws_port = os.environ.get("WS_PORT", 9999)
server = await websockets.asyncio.server.serve(handler, "::", ws_port)
print(f"WebSocket server listening on ws://[::]:{ws_port}")
await server.wait_closed()
if __name__ == "__main__":