A
crawling
Dreamhack
조회수 37
풀이자수
web

드림이는 웹 크롤링 사이트를 구축했습니다.
크롤링 사이트에서 취약점을 찾고 flag를 획득하세요!

crawling

  • 출제된 CTF: Dreamhack CTF Season 1 Round #11
  • 분야: Web
  • 키워드: SSRF(Server side request forgery), ToCToU(Time of check to Time of use), DNS rebinding attack
  • 난이도: ★★☆☆☆

배경

리얼월드 웹 환경에서는 다양한 취약점이 동시에 발견되고, 해당 취약점들을 연계하여 공격 가능한 시나리오가 자주 도출됩니다. ToCToU는 'Time of check to time of use’의 약자로, 로직에서 조건과 조건의 결과 사이에 race condition이 있을 경우 발생하는 취약점입니다. SSRF는 서버 측 애플리케이션이 공격자가 선택한 임의의 도메인에 HTTP 요청을 할 수있는 웹 보안 취약점입니다. 이 문제를 해결하기 위해서는 소스코드를 분석하여 취약점을 찾는 것 뿐만 아니라, 취약점을 트리거하기 위한 DNS rebinding이라는 공격기법을 이해해야 합니다. 본 문제의 취지는 소스코드를 분석하여 ToCToU 취약점과 SSRF 취약점을 찾아내고, 두 취약점을 연계하여 DNS rebinding attack을 수행할 수 있는 방법을 익히는데에 있습니다.

풀이

먼저 문제에 접속해보면 crawling이라는 제목의 페이지가 나옵니다. input 값을 넣을 수 있는 텍스트상자에 유효한 URL을 입력하고 Submit을 누르면 입력한 URL의 IP주소와 크롤링된 html 데이터가 출력됩니다. submit을 눌렀을때 /validation페이지로 get method를 통해서 url이라는 매개변수의 값으로 사용자의 입력값이 전달되는 것을 알 수 있습니다.

app.py 소스코드가 제공되므로, 코드를 확인해 봅시다.

#app.py
import socket
import requests
import ipaddress
from urllib.parse import urlparse
from flask import Flask, request, render_template

app = Flask(__name__)
app.flag = '__FLAG__'

def lookup(url):
    try:
        return socket.gethostbyname(url)
    except:
        return False

def check_global(ip):
    try:
        return (ipaddress.ip_address(ip)).is_global
    except:
        return False

def check_get(url):
    ip = lookup(urlparse(url).netloc)
    if ip == False or ip =='0.0.0.0':
        return "Not a valid URL."
    res=requests.get(url)
    if check_global(ip) == False:
        return "Can you access my admin page~?"
    for i in res.text.split('>'):
        if 'referer' in i:
            ref_host = urlparse(res.headers.get('refer')).netloc
            if ref_host == 'localhost':
                return False
            if ref_host == '127.0.0.1':
                return False 
    res=requests.get(url)
    return res.text

@app.route('/admin')
def admin_page():
    if request.remote_addr != '127.0.0.1':
        return "This is local page!"
    return app.flag

@app.route('/validation')
def validation():
    url = request.args.get('url', '')
    ip = lookup(urlparse(url).netloc)
    res = check_get(url)
    return render_template('validation.html', url=url, ip=ip, res=res)

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

if __name__=='__main__':
    app.run(host='0.0.0.0', port=3333)

app.py 파일을 통해 admin page에서 flag를 반환해준다는 것을 알 수 있습니다. 그러나 admin page는 local page로, 외부에서 접근해서는 if문을 통해 "This is local page!"라는 문자열이 출력되고, flag는 출력되지 않습니다.

취약점 1: SSRF

local page로 접속하기 위해서 SSRF 취약점을 생각해볼 수 있습니다. SSRF 취약점은 드림핵 강의를 통해 확인하실 수 있습니다. 본 문제는 validation 페이지에 get요청 전송 시, 웹 서비스에서 이용자의 입력값인 url 파라미터의 값으로 요청을 전송합니다. 따라서 validation 페이지의 url 파라미터에 내부망 주소를 넣으면 validation 페이지에서 요청을 전송하는 것이므로 외부망에서도 해당 페이지의 값을 볼 수 있습니다.

해당 취약점을 이용하여

http://127.0.0.1:3333/admin

을 입력하면 아래의 문구가 출력되고, flag는 출력되지 않습니다.

Can you access my admin page~?

따라서 SSRF 만으로는 flag를 얻을 수 없다는 것을 확인할 수 있습니다.

취약점 2: ToCToU

SSRF가 어떻게 막혀있는지 확인해봅시다.

def check_get(url):
    ip = lookup(urlparse(url).netloc)
    if ip == False or ip =='0.0.0.0':
        return "Not a valid URL."
    res=requests.get(url)
    if check_global(ip) == False:
        return "Can you access my admin page~?"
    for i in res.text.split('>'):
        if 'referer' in i:
            ref_host = urlparse(res.headers.get('refer')).netloc
            if ref_host == 'localhost':
                return False
            if ref_host == '127.0.0.1':
                return False 
    res=requests.get(url)
    return res.text

@app.route('/admin')
def admin_page():
    if request.remote_addr != '127.0.0.1':
        return "This is local page!"
    return app.flag

app.py코드에서 check_get 함수를 보면, ip를 검증하여 global ip, 즉, 외부 ip가 아닌 경우 "Can you access my admin page?"를 출력합니다. 따라서 admin 페이지로 정상적인 get 요청을 전송하기 위해서는 외부 ip여야 합니다. 이는 마치 SSRF 취약점이 막혀있는 것으로 보일 수 있습니다.
그러나, 해당 도메인이 어떤 ip를 가지고 있는지 확인하는 lookup메소드를 호출한 후, 많은 연산을 한 뒤에 admin 페이지로 get 요청을 날리고 있음을 확인할 수 있습니다. 따라서 실제로 ip를 확인하는 로직인 lookup메소드와 admin 페이지의 request.remote_addr 사이에 연산으로 인한 race condition이 발생합니다. 이러한 취약점을 Time of check to time of use(ToCToU) 취약점이라고 합니다.

이 취약점을 트리거하기 위해서는 DNS rebinding 공격을 해야합니다. DNS rebinding 공격이란, 도메인 이름을 확인하는 로직을 우회하는 방법입니다. lookup을 통해 도메인에 대한 ip를 확인할 때에는 check_global(ip)를 우회할 수 있도록 도메인에 매칭되는 ip가 global ip 여야하고, 이후 admin 페이지에서 request.remote_addr을 통해 ip를 검사할 때에는 도메인에 매칭되는 ip가 local ip인 127.0.0.1이여야 합니다. 그렇다면 같은 도메인에 2개의 ip가 번갈아가며 매칭되도록 한다면, flag를 얻을 수 있을 것 입니다.

익스플로잇

공격을 수행하기 위해서는 하나의 도메인에 2개의 IP를 매핑해두고 TTL값을 빠르게하여 request가 전송되기 전에 global IP인 상태로 검증로직을 우회하고, 다시 local IP로 변환되어야 합니다.

이를 위해 하나의 도메인에 2개의 IP를 걸어두고 TTL은 30정도로 두면 30초에 한번씩 랜덤으로 2개의 IP중 하나를 도메인에 바인딩합니다.
만약 도메인이 없는 경우에는 DNS rebinding tool을 사용하여 2가지 ip를 하나의 도메인에 바인딩하고, 해당 도메인으로 admin 페이지에 접속하여 타이밍을 맞춰주면 flag를 얻을 수 있다.

http://7f000001.6565a4b0.rbndr.us:3333/admin

레퍼런스