1. 소스코드
from flask import Flask, request # Flask 웹 프레임워크와 요청 처리를 위한 모듈
from os import urandom # 안전한 랜덤 바이트 생성을 위한 모듈
from subprocess import run, TimeoutExpired # 외부 명령어 실행 및 타임아웃 처리를 위한 모듈
# Flask 애플리케이션 객체 생성
app = Flask(__name__)
# 세션 및 CSRF 보호를 위한 비밀 키 생성 (32바이트 랜덤 값)
app.secret_key = urandom(32)
# 플래그 값 로드 시도: 서버 환경에서 "/flag" 파일 읽기
try:
FLAG = open("/flag", "r").read().strip() # 파일에서 플래그를 읽어 공백 제거
except:
FLAG = "[**FLAG**]" # 파일 읽기 실패 시 기본값 설정 (테스트용)
# curl 요청이 허용되는 호스트 목록 정의
ALLOWED_HOSTS = ['dreamhack.io', 'tools.dreamhack.io']
# POST 요청으로 들어오는 "/api/v1/test/curl" 엔드포인트 정의
@app.route("/api/v1/test/curl", methods=["POST"])
def admin():
# 요청 폼 데이터에서 URL 추출 후 공백 제거
url = request.form["url"].strip()
# 허용된 호스트인지 확인
for host in ALLOWED_HOSTS:
if url.startswith('http://' + host): # URL이 허용된 호스트로 시작하는지 체크
break #http://dreamhack.io 로 시작해야함
else: # for-else: 허용된 호스트가 없으면 실행
return {'result': False, 'msg': 'Not Allowed host'} # 비허용 호스트 응답
# 특정 엔드포인트("/test/internal")로의 요청 차단
if url.endswith('/test/internal'):
return {'result': False, 'msg': 'Not Allowed endpoint'} # 금지된 엔드포인트 응답
# curl 명령어 실행 시도
try:
# subprocess.run을 사용해 curl로 URL 호출, 결과 캡처, 1초 타임아웃 설정
response = run(
["curl", f"{url}"],
capture_output=True, # stdout과 stderr 캡처
text=True, # 문자열로 출력 반환
timeout=1 # 1초 내 응답 없으면 타임아웃
)
return {'result': True, 'msg': response.stdout} # 성공 시 curl 출력 반환
# 타임아웃 발생 시 예외 처리
except TimeoutExpired:
return {'result': False, 'msg': 'Timeout'} # 타임아웃 응답
# GET 요청으로 들어오는 "/api/v1/test/internal" 엔드포인트 정의
@app.route('/api/v1/test/internal', methods=["GET"])
def test():
# 요청자의 IP 주소 추출
ip = request.remote_addr
# 로컬호스트(127.0.0.1)에서만 접근 허용
if not ip == '127.0.0.1':
return {'result': False, 'msg': 'Only local access is allowed'} # 비로컬 접근 차단
return {'result': True, 'msg': FLAG} # 로컬 접근 시 플래그 반환
# 메인 실행 블록: 서버 실행
if __name__ == '__main__':
# 모든 인터페이스(0.0.0.0)에서 8000번 포트로 서버 실행
app.run(host='0.0.0.0', port='8000')
따로 주석을 추가했다.
2. 분석
# curl 요청이 허용되는 호스트 목록 정의
ALLOWED_HOSTS = ['dreamhack.io', 'tools.dreamhack.io']
# POST 요청으로 들어오는 "/api/v1/test/curl" 엔드포인트 정의
@app.route("/api/v1/test/curl", methods=["POST"])
def admin():
# 요청 폼 데이터에서 URL 추출 후 공백 제거
url = request.form["url"].strip()
# 허용된 호스트인지 확인
for host in ALLOWED_HOSTS:
if url.startswith('http://' + host): # URL이 허용된 호스트로 시작하는지 체크
break #http://dreamhack.io 로 시작해야함
else: # for-else: 허용된 호스트가 없으면 실행
return {'result': False, 'msg': 'Not Allowed host'} # 비허용 호스트 응답
1.POST로 경로 "/api/v1/test/curl"에 요청을 보내야 한다.
2. 허용된 호스트인지 확인하는 과정이 존재한다 ALLOWED_HOSTS 안에 있는 목록들을 참고하면 될 거 같다.
3.URL 은 'http://dreamhack.io'로 시작해야 한다.
# 특정 엔드포인트("/test/internal")로의 요청 차단
if url.endswith('/test/internal'):
return {'result': False, 'msg': 'Not Allowed endpoint'} # 금지된 엔드포인트 응답
# curl 명령어 실행 시도
try:
# subprocess.run을 사용해 curl로 URL 호출, 결과 캡처, 1초 타임아웃 설정
response = run(
["curl", f"{url}"],
capture_output=True, # stdout과 stderr 캡처
text=True, # 문자열로 출력 반환
timeout=1 # 1초 내 응답 없으면 타임아웃
)
return {'result': True, 'msg': response.stdout} # 성공 시 curl 출력 반환
# 타임아웃 발생 시 예외 처리
except TimeoutExpired:
return {'result': False, 'msg': 'Timeout'} # 타임아웃 응답
@app.route('/api/v1/test/internal', methods=["GET"])
def test():
# 요청자의 IP 주소 추출
ip = request.remote_addr
# 로컬호스트(127.0.0.1)에서만 접근 허용
if not ip == '127.0.0.1':
return {'result': False, 'msg': 'Only local access is allowed'} # 비로컬 접근 차단
return {'result': True, 'msg': FLAG} # 로컬 접근 시 플래그 반환
1.endswith로 /test/internal 에 대한 경로를 금지시킨다. (우회 필요)
2.curl로 URL을 호출시킨다.
3.GET 요청으로 /api/v1/test/internal 에 대한 경로 그리고 로컬로 접속해야지 플래그가 출력된다.
3. 문제풀이
1.POST 는 코드에 나와있는 대로 경로를 입력
2.Content-Type 은 curl에서 url와 같은 키-값 쌍을 서버로 보내기 위해서 사용해야 한다.
3.Not allowed endpoint 가 출력되는 걸 확인할 수 있다.
이제 우회를 해보도록 하자.
# 특정 엔드포인트("/test/internal")로의 요청 차단
if url.endswith('/test/internal'):
return {'result': False, 'msg': 'Not Allowed endpoint'} # 금지된 엔드포인트 응답
이 부분을 우회해야 한다.
보면 endswith 함수에 대해서 알아야 한다.
text = "hello world"
print(text.endswith("world")) # True ("world"로 끝남)
print(text.endswith("hello")) # False ("hello"로 끝나지 않음)
이런 식으로 endswith는 문자열 끝에 초점을 맞춰 True, False를 반환시킨다.
그렇다면 경로를 건드리지 않는 선에서 그리고 /test/internal 이 연속되지 않게 우회를 해보도록 하자.
중간에"./"를 넣어서 우회를 하면 Time out을 출력하는 걸 알 수 있다.
# 특정 엔드포인트("/test/internal")로의 요청 차단
if url.endswith('/test/internal'):
return {'result': False, 'msg': 'Not Allowed endpoint'} <우회한 부분>
# curl 명령어 실행 시도
try:
# subprocess.run을 사용해 curl로 URL 호출, 결과 캡처, 1초 타임아웃 설정
response = run(
["curl", f"{url}"],
capture_output=True, # stdout과 stderr 캡처
text=True, # 문자열로 출력 반환
timeout=1 # 1초 내 응답 없으면 타임아웃
)
return {'result': True, 'msg': response.stdout} # 성공 시 curl 출력 반환
# 타임아웃 발생 시 예외 처리
except TimeoutExpired:
return {'result': False, 'msg': 'Timeout'} <출력된 부분>
# GET 요청으로 들어오는 "/api/v1/test/internal" 엔드포인트 정의
@app.route('/api/v1/test/internal', methods=["GET"])
def test():
# 요청자의 IP 주소 추출
ip = request.remote_addr
# 로컬호스트(127.0.0.1)에서만 접근 허용
if not ip == '127.0.0.1':
return {'result': False, 'msg': 'Only local access is allowed'} # 비로컬 접근 차단
return {'result': True, 'msg': FLAG} <출력해야하는 부분>
현재상황을 보면 우회해서 타임아웃이 발생했고 그다음 해야 할 일은 local로 인식할 수 있게끔 하면 된다.
3-1.URL 현재, 과거
답을 알려줄 수는 없기에 힌트라도 적어놔야겠다.
URL의 현재 표준은 scheme://host:port/path이다.
ex) "http://dreamhack.io:8000/api/v1/test/internal"
URL 과거 표준은 scheme://username:password@host:port/path이다.
현재 사용자 이름과 비밀번호는 따로 아는 게 없으니 더미데이터를 넣어 양식을 채운뒤 알맞은 값을 입력해 보자.
'DreamHack > CTF' 카테고리의 다른 글
chockshop (0) | 2024.12.10 |
---|---|
Secure Secret (0) | 2024.11.07 |
(CTF 출제) simple-web-request (1) | 2024.10.09 |
(CTF 출제) Tomcat Manager (1) | 2024.09.05 |
(CTF 출제) amocafe (2) | 2024.09.05 |