[DreamHack] [Web] SentenceNet

image.png

image.png

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"]

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)