

Flask 기반 웹 해킹 문제이다.
도커파일 먼저 분석해보겠다.
version: "3.9"
services:
home:
container_name: home
build:
context: ./deploy/app
networks:
- internal
ports:
- "54321:8001"
depends_on:
- user
user:
container_name: user
build:
context: ./deploy/user
networks:
- internal
ports:
- "31337"
networks:
internal:
2개의 컨테이너가 동작하고 있다.
먼저 home의 app.py 도커 환경이 어떻게 구성되어 있는지 확인해보겠다.
FROM python:3.12-slim-bookworm
ENV USER alice
ENV PORT 8001
RUN apt-get update -y && apt-get install -y python3-pip build-essential wget curl unzip
# Add Google Chrome's official GPG key and setup repository
RUN wget -q -O - <https://dl-ssl.google.com/linux/linux_signing_key.pub> | apt-key add - && \\
echo 'deb [arch=amd64] <http://dl.google.com/linux/chrome/deb/> stable main' > /etc/apt/sources.list.d/google-chrome.list
# Install the latest stable version of Google Chrome
RUN apt-get update -y && apt-get install -y google-chrome-stable
# Fetch the latest ChromeDriver version matching the installed Google Chrome version
RUN CHROME_VERSION=$(google-chrome --version | grep -oP '\\d+\\.\\d+\\.\\d+\\.\\d+') && \\
wget <https://storage.googleapis.com/chrome-for-testing-public/${CHROME_VERSION}/linux64/chromedriver-linux64.zip> && \\
unzip chromedriver-linux64.zip && \\
mv chromedriver-linux64/chromedriver /usr/local/bin/ && \\
rm -rf chromedriver-linux64.zip chromedriver-linux64
RUN pip install --upgrade pip
RUN adduser --disabled-password $USER
WORKDIR /app
COPY ./ /app
COPY ./flag /
RUN rm /app/flag
RUN pip install -r requirements.txt
EXPOSE $PORT
CMD ["python3","app.py"]
port:8001을 사용한다.flag 파일을 /app/flag에서 복사한 뒤, 해당 경로(/home)에 둔다. 기존 /app/flag 는 삭제한다.home의 권한으로는 flag를 직접 읽을 수 있을 것 같다.app.py를 확인해 보면,
from flask import Flask, request, render_template, redirect, url_for, abort, request, jsonify
import os
import re
import requests
import ipaddress
import subprocess
app = Flask(__name__)
app.secret_key = os.urandom(64)
info_value = ''
@app.route('/')
def index():
return redirect(url_for('info'))
@app.route('/post', methods=['GET', 'POST'])
def post():
if request.method == 'POST':
global info_value
info_value = request.form.get('info_value', '')
return redirect(url_for('info'))
ip_address = request.remote_addr
ip_obj = ipaddress.ip_address(ip_address)
if ip_obj.is_private or ip_obj.is_loopback:
FLAG = open('/flag', 'r').read()
value = FLAG
else:
value = ''
return render_template('post.html', value=value)
@app.route('/info', methods=['GET'])
def info():
global info_value
return render_template('info.html', info_value=info_value)
@app.route('/callback', methods=['GET'])
def callback():
ip_address = request.remote_addr
ip_obj = ipaddress.ip_address(ip_address)
if not (ip_obj.is_private or ip_obj.is_loopback):
abort(403, 'Access denied')
callback_fn = request.args.get('callback', 'console.log')
if not re.fullmatch(r'[A-Za-z0-9.]+', callback_fn):
abort(400, 'Invalid callback parameter')
global info_value
return render_template('callback.html', callback_fn=callback_fn, info_value=info_value)
@app.route('/report', methods=['GET', 'POST'])
def report():
if request.method == 'POST':
report_url = request.form.get('report_url', '')
resp = requests.post('<http://user:31337/visit>', json={'url': report_url})
if resp.ok:
return render_template('report.html', submitted=True)
return render_template('report.html', submitted=False)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8001)
/info 를 통해 서버 내에 저장된 문장인 info_value를 출력한다.
/post 를 통해 GET 요청에 대해서 사용자가 내부망에 존재하는지 확인한 후,
FLAG값을 기본 값으로 채울지 여부를 결정한다. POST 요청을 받으면, 전달 받은 값을 info_value에 등록한다.