# 개요
SSRF의 Hostname 필터링(화이트리스트 기반)을
우회하는 문제입니다.
또한,
Flask에서는
URL encoding된 파라미터 값은
자동으로 decoding하지 않고
그대로 받아들인다는 사실을
알게 된 문제였습니다.
+ 대상 확인
사이트에 접속하면
조촐하게
입력란과 제출 버튼이
있습니다.
아무런 값을 입력한 후
제출하면
"Something wrong..." 이라는
문자열이 출력됩니다.
+ Flag 확인
제공된 파일을 보면
public 서버와
internal 서버
두 서버로 나뉘며
Flag는
internal 서버의
secret.py에
선언되어 있습니다.
# 분석
문제 사이트와 함께 제공된
사이트의 코드를 분석합니다.
Exploit을 위한
주된 코드는
아래 파일에서
확인할 수 있었습니다.
주요 파일
- public 서버의 app.py 파일
- docker-compse.yml 파일
- internal 서버의 app.py 파일
+ public 서버 app.py 파일
우선, public 서버의 app.py 파일 내용입니다.
# public 서버 > app.py
import urllib
import urllib.parse
import requests
import ipaddress
from flask import Flask
from flask import request
import socket
app = Flask(__name__)
data = """
<html>
<head>
<title>BabyWeb</title>
</head>
<body>
<form action="/" method="POST">
<input type="text" name="url">
<input type="submit">
</form>
</body>
</html>
"""
def valid_ip(ip):
try:
ip = socket.gethostbyname(ip)
is_internal = ipaddress.ip_address(ip).is_global
if(is_internal):
return False
else:
return True
except:
pass
@app.route('/', methods=['GET','POST'])
def index():
if request.method == "POST":
try:
url = request.form['url']
result = urllib.parse.urlparse(url)
if result.hostname == 'flag.service':
return "Not allow"
else:
if(valid_ip(result.hostname)):
return "huh??"
else:
return requests.get("http://"+result.hostname+result.path, allow_redirects=False).text
except:
return "Something wrong..."
elif request.method == "GET":
return data
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)
index() 함수의 내용을 보면,
아래와 같이
동작하고 있습니다.
public 서버 동작:
1. POST 방식으로 url 파라미터 입력
- url = request.form['url']
2. url parser로 url 분석
- result = urllib.parse.urlparse(url)
3. hostname 필터링
- if result.hostname == 'flag.service':
4. host 유효성 확인(IP 검증)
- if(valid_ip(result.hostname)):
5. 검증된 서버로 접속 및 응답 값 반환
- return requests.get("http://"+result.hostname+result.path, allow_redirects=False).text
이 파일에서는
hostname 필터링과
host 유효성 검증을
우회해야한다는
생각이 들었습니다.
참고로,
Python의 urllib.parse 결과는
다음과 같습니다.
위의 hostname은
아래 예시의 netloc과
동일합니다.
>>> o = urlparse("http://docs.python.org:80/3/library/urllib.parse.html?"
... "highlight=params#url-parsing")
>>> o
ParseResult(scheme='http', netloc='docs.python.org:80',
path='/3/library/urllib.parse.html', params='',
query='highlight=params', fragment='url-parsing')
+ docker-compose.yml 파일
docker-compose.yml 파일은
도커 컨테이너를
어떻게 구성하고 실행할지
정의한 파일로
flag.service 서버가
무엇인지
알 수 있었습니다.
version: '3'
services:
challenge:
build:
context: .
dockerfile: ./public/Dockerfile
ports:
- "80:80"
links:
- flag.service
flag.service:
build:
context: .
dockerfile: ./internal/Dockerfile
flag.service 서버는
제공된 파일의
internal 서버임을
알 수 있습니다.
+ internal 서버 app.py 파일
이제 FLAG가 있는
internal 서버의 app.py 파일
입니다.
from flask import Flask
from flask import request
from secret import FLAG
app = Flask(__name__)
@app.route('/flag', methods=['GET'])
def index():
if request.host == "flag.service":
return FLAG
else:
return "Nice try :)"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=80)
internal 서버 파일의 내용은
간단합니다.
internal 서버 동작:
1. GET 방식으로 /flag 에 접속한 경우
- @app.route('/flag', methods=['GET'])
2. 요청 데이터의 host 검증(flag.service)
- if request.host == "flag.service":
3. 검증된 경우 FLAG 반환
- return FLAG
- FLAG는 secret.py 에서 import
위의 내용으로 보아
요청 데이터의 host가
flag.service 일 때만
FLAG를 획득할 수 있습니다.
# 풀이
이번 문제는
소스 코드는 간단하지만,
flag.service를 그대로 입력하면
필터링에 의해
FLAG를 획득할 수 없었으며,
다른 URL을 입력하면
flag.service로
접속하지 않기 때문에
반드시 flag.service로
인식되야 했습니다.
+ URL encoding
필터링 코드를 보면
문자열만 필터링할 뿐
인코딩된 문자열을
필터링하지
않고 있습니다.
예전에는
Host splitting attack
이라는 방식이
있기도 했으나
인코딩된 문자열을
필터링하지 않는 점을
취약점으로 삼아
단순한 URL Encoding으로
우회했습니다.
문제 풀이 순서:
1. http://flag.service/flag URL 중 Hostname을 URL Encoding
- 한 문자만 해도 됨.
- http://fl%61g.service/flag
2. 인코딩된 문자열을 입력, 제출
아래와 같이
인코딩된 문자열을
입력, 제출하면
FLAG를 얻을 수 있습니다.