├── .dockerignore ├── .gitignore ├── Dockerfile ├── Dockerfile.distroless ├── LICENSE ├── README.md ├── app.py ├── requirements.txt ├── screenshots ├── distroish-ping.png ├── distroless-log.png └── distroless-ping.png ├── static └── styles.css └── templates └── index.html /.dockerignore: -------------------------------------------------------------------------------- 1 | screenshots/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # OS X 132 | .DS_Store 133 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-slim-stretch 2 | 3 | WORKDIR /app 4 | COPY . /app 5 | 6 | RUN apt-get clean \ 7 | && apt-get -y update \ 8 | && apt-get -y install iputils-ping \ 9 | && pip install --upgrade pip \ 10 | && pip install -r requirements.txt 11 | 12 | EXPOSE 8080 13 | CMD ["python", "app.py"] 14 | -------------------------------------------------------------------------------- /Dockerfile.distroless: -------------------------------------------------------------------------------- 1 | # source: https://github.com/GoogleContainerTools/distroless 2 | FROM debian:buster-slim AS build 3 | RUN apt-get update && \ 4 | apt-get install --no-install-suggests --no-install-recommends --yes \ 5 | python3-venv gcc libpython3-dev iputils-ping libcap2 libunistring2 libidn2-0 libnettle6 && \ 6 | python3 -m venv /venv && \ 7 | /venv/bin/pip install --upgrade pip 8 | 9 | FROM build AS build-venv 10 | COPY requirements.txt /requirements.txt 11 | RUN /venv/bin/pip install --disable-pip-version-check -r /requirements.txt 12 | 13 | FROM gcr.io/distroless/python3-debian10 14 | COPY --from=build-venv /venv /venv 15 | COPY --from=build-venv /bin/ping /bin/ping 16 | COPY --from=build-venv /lib/x86_64-linux-gnu/libcap.so.2 /lib/x86_64-linux-gnu/libcap.so.2 17 | COPY --from=build-venv /usr/lib/x86_64-linux-gnu/libidn2.so.0 /usr/lib/x86_64-linux-gnu/libidn2.so.0 18 | COPY --from=build-venv /usr/lib/x86_64-linux-gnu/libnettle.so.6 /usr/lib/x86_64-linux-gnu/libnettle.so.6 19 | COPY --from=build-venv /usr/lib/x86_64-linux-gnu/libunistring.so.2 /usr/lib/x86_64-linux-gnu/libunistring.so.2 20 | COPY . /app 21 | WORKDIR /app 22 | 23 | EXPOSE 8080 24 | ENTRYPOINT ["/venv/bin/python3", "app.py"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Erick Durán 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-distroless-poc 2 | This repository is a simple Proof of Concept about the advantages of using ["distroless" Docker images](https://github.com/GoogleContainerTools/distroless). 3 | 4 | ## Motivation 5 | I encountered a few open source projects starting to implement distroless images, so I was curious about at what extent you can truly win by following this idea. Distroless images come with the promise of being more secure by design, so this proof of concept tries to point out a few pros and cons to demonstrate if it's worth the extra effort. 6 | 7 | ## Objectives 8 | - Build a simple vulnerable web app with both distroless and distro(ish?) approaches 9 | - Deploy the vulnerable app and attempt RCE 10 | - Deploy distroless app and attempt RCE 11 | 12 | ## Application description 13 | The purpose of the application is to prompt the user for an address to ping to, and display the results of the ping. It was poorly coded using Python3 and the Flask framework. 14 | 15 | The application is vulnerable to RCE as there is no input sanitization and the user's input is passed directly to the OS's `ping` command. 16 | ## Installation 17 | ### Prerequisites 18 | - Docker 19 | 20 | ### Usage 21 | To run the distroish version: 22 | ```bash 23 | docker run -p 8080:8080 erickduran/vulnerable-flask-app:latest 24 | ``` 25 | And browse to http://127.0.0.1:8080. 26 | 27 | To run the distroless version: 28 | ```bash 29 | docker run -p 8080:8080 erickduran/vulnerable-flask-app-distroless:latest 30 | ``` 31 | And browse to http://127.0.0.1:8080. 32 | 33 | You can test the RCE by typing `erickduran.com; ls` into the text box. 34 | 35 | ### Proof of Concept 36 | Attempting a simple RCE with a OS command using the regular Docker image, we receive the output: 37 | ![Hacked](screenshots/distroish-ping.png) 38 | 39 | However, in the distroless image, we do not get the same result: 40 | ![Not hacked](screenshots/distroless-ping.png) 41 | 42 | But... the command was actually attempted: 43 | ![Oops](screenshots/distroless-log.png) 44 | 45 | The RCE happened, but it is way harder for an attacker to perform any malicious task or retrieve any information as the common commands and tools are not available. 46 | 47 | ## Conclusions 48 | - You are alone with your code: **distroless** means that, **distroless**. Having a completely empty image means that you will need to explicitly pass all the libraries and binaries that your project requires. This may be fairly straightforward for this project, but it might be a headache for big and complex projects. 49 | - If your code is vulnerable, you are **less** susceptible a RCE that leads to a shell, but the RCE is still happening. Even though you don't have any of the default Linux binaries, your programming language is still there and it needs to work. For this case, it is a bit hard to achieve something using Python as there was virtually no way to install something (no `requests`, no `pip`, no `curl` (OS), etc.), but you can potentially draft a pure-Python script to do any dirty stuff, or make use of the modules installed within the project. 50 | - You have more control of your environment. As it is already one of the great advantages of Docker, distroless images give you a bit more control so you are more aware of what you need and what your code is doing. However, this comes with the disadvantage of having to spend more time setting up a working build. 51 | 52 | ## Future tasks 53 | - Further pentesting (attempt a decent shell using the implementation programming language, retrieve files using project libraries, etc.) 54 | - Explore advantages Bazel builds 55 | - Implement vulnerable projects with other distroless images 56 | 57 | ## Authors 58 | Erick Durán. Copyright © 2020. 59 | 60 | ## License 61 | Released under the MIT LICENSE. 62 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from waitress import serve 4 | from flask import Flask, render_template, request 5 | 6 | app = Flask(__name__, template_folder='templates') 7 | 8 | 9 | @app.route('/', methods=['GET']) 10 | def index(): 11 | return render_template('index.html') 12 | 13 | 14 | @app.route('/', methods=['POST']) 15 | def submit(): 16 | text = request.form['text'] 17 | if text: 18 | try: 19 | output = subprocess.check_output(f'ping -c1 {text}', shell=True) 20 | except subprocess.CalledProcessError: 21 | output = None 22 | 23 | if output: 24 | result = output.decode("utf-8") 25 | else: 26 | result = 'no pong' 27 | else: 28 | result = 'please enter an address' 29 | return render_template('index.html', output=result) 30 | 31 | 32 | if __name__ == '__main__': 33 | serve(app, host='0.0.0.0') 34 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==7.1.1 2 | Flask==1.1.2 3 | itsdangerous==1.1.0 4 | Jinja2==2.11.2 5 | MarkupSafe==1.1.1 6 | waitress==1.4.3 7 | Werkzeug==1.0.1 8 | -------------------------------------------------------------------------------- /screenshots/distroish-ping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erickduran/docker-distroless-poc/676de611d135af71567dda8db8caa5f22681c308/screenshots/distroish-ping.png -------------------------------------------------------------------------------- /screenshots/distroless-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erickduran/docker-distroless-poc/676de611d135af71567dda8db8caa5f22681c308/screenshots/distroless-log.png -------------------------------------------------------------------------------- /screenshots/distroless-ping.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erickduran/docker-distroless-poc/676de611d135af71567dda8db8caa5f22681c308/screenshots/distroless-ping.png -------------------------------------------------------------------------------- /static/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | -webkit-box-sizing: border-box; 6 | -moz-box-sizing: border-box; 7 | } 8 | 9 | .wrapper { 10 | margin: 25px auto; 11 | width: 500px; 12 | overflow: auto; 13 | } 14 | 15 | .bar { 16 | height: 5px; 17 | width: 0; 18 | background: #333; 19 | transition: 1s ease-in-out; 20 | -webkit-transition: 0.5s ease-in-out; 21 | -moz-transition: .5s ease-in-out; 22 | } 23 | 24 | .wrapper input { 25 | height: 35px; 26 | padding: 4px 8px; 27 | width: 400px; 28 | border: 0; 29 | border-bottom: 1px solid #0f9d58; 30 | font-family: Arial; 31 | font-size: 22px; 32 | } 33 | 34 | .wrapper input:focus { 35 | border: 0; 36 | box-shadow: 0; 37 | outline: 0; 38 | } 39 | 40 | .wrapper span { 41 | position: relative; 42 | transition: 1s ease-in-out; 43 | -webkit-transition: 1s ease-in-out; 44 | -moz-transition: 1s ease-in-out; 45 | opacity: 0; 46 | right: 0; 47 | font-size: 25px; 48 | font-family: Arial; 49 | font-weight: bold; 50 | color: #0f9d58; 51 | } 52 | 53 | .wrapper input:hover + span + .bar, 54 | .wrapper input:focus + span + .bar{ 55 | width: 400px; 56 | background: #0f9d58; 57 | } 58 | 59 | .wrapper input:hover + span, 60 | .wrapper input:focus + span { 61 | right: 100px; 62 | opacity: 1; 63 | } 64 | 65 | .button input { 66 | height: 35px; 67 | padding: 4px 8px; 68 | width: 400px; 69 | border: 0; 70 | font-family: Arial; 71 | font-size: 22px; 72 | font-weight: bold; 73 | background: transparent; 74 | position: relative; 75 | overflow: auto; 76 | } 77 | 78 | .result { 79 | position: relative; 80 | overflow: auto; 81 | } 82 | 83 | .result h1 { 84 | font-size: 25px; 85 | font-family: Arial; 86 | font-weight: bold; 87 | color: #0f9d58; 88 | } 89 | 90 | .result .text { 91 | font-size: 20px; 92 | font-family: Arial; 93 | } -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Safe ping pong 4 | 5 | 6 | 7 |
8 |
9 | 10 | address 11 |
12 |
13 | 14 |
15 |
16 |
17 | {% if output %} 18 |
19 |
20 |

pong

21 |
22 | {{ output }} 23 |
24 |
25 |
26 | {% endif %} 27 | 28 | --------------------------------------------------------------------------------