포스트

[드림핵] Where-is-localhost

주어진 웹 서버로 접속하였다. 위와 같은 페이지를 마주칠 수 있다. 아무거나 입력하고 submit하면 no...를 반환한다. 아마도 특정 조건을 만족하는 무언가를 입력해야 flag를 반환하는듯 하다.이제 문제 파일을 살펴보자

[드림핵] Where-is-localhost

문제 설명

주어진 웹 서버로 접속하였다.

위와 같은 페이지를 마주칠 수 있다. 아무거나 입력하고 submit하면 no...를 반환한다. 아마도 특정 조건을 만족하는 무언가를 입력해야 flag를 반환하는듯 하다.

이제 문제 파일을 살펴보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
from flask import Flask, render_template, request
import ipaddress
import urllib.parse
import urllib.request
import urllib.error

app = Flask(__name__)

try:
    with open('flag') as f:
        flag = f.read()
except FileNotFoundError:
    flag = 'flag{this_is_a_fake_flag}'

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

@app.route('/vuln', methods=['POST'])
def vuln():
    name = request.form.get('vulntest')
    try:
        address = ipaddress.ip_address(name)
        if address.version == 4:
            return "no..."
        url = urllib.parse.urlparse(f"http://[{address.exploded}]:5000/localonly")
        if url.netloc != f'[{address.exploded}]:5000':
            print(url.netloc, f'[{address.exploded}]')
            return "no..."
        req = urllib.request.Request(url.geturl())
        return urllib.request.urlopen(req).read().decode('utf-8')
    except ValueError:
        return "no..."
    except urllib.error.URLError:
        return "connection refused"

@app.route('/localonly', methods=['GET'])
def localonly():
    addr = ipaddress.ip_address(request.remote_addr)
    if addr.is_loopback and addr.version == 4:
        return flag
    else:
        return 'not loopback'

if __name__ == '__main__':
    app.run('0.0.0.0', 5000)

해당 코드는 flask를 사용한 웹서버이다. flag를 리턴하는 /localonly에 포커스를 둬보자. localonlyGET요청을 보냈을 때 요청을 보낸 IP주소(이하 addr)가 is_loopback이며 version이 4여야 하는 조건을 만족해야 flag를 리턴한다.

loopback

loopback이라는 용어가 생소해 찾아보게 되었다. 여기서 loopback은 네트워크에서 localhost를 가리키는 특별한 주소를 말한다. 참고 : [[https://www.sciencedirect.com/topics/computer-science/loopback-address]]

127.0.0.1같이 자주 접한 주소가 localhost를 가리키는 주소인것은 알았지만 이러한 주소를 루프백이라고 하는것은 처음 알게 되었다.

IPv4에서 루프백 주소는 127.0.0.1뿐만 아니라 127.x.x.x (0 <= x <= 255), CIDR로는 127.0.0.0/8의 범위에 해당하는 IP주소가 루프백 주소이다. 반면 IPv6에서는 0:0:0:0:0:1 (::1)만이 루프백 주소이다.

이제 /localonly로 루프백 주소로 GET요청을 보내면 flag를 얻을 수 있음을 알게 되었다. Burp Suite를 사용해 HTTP헤더의 X-Forwarded-For의 값을 127.0.0.1로 설정하여 로컬IP로 변조하고자 했다.

하지만 위 사진과 같이 IP주소가 변조되지 못했음을 알게 되었다.

X-Forwarded-For헤더의 의미는 프록시를 사용하는 경우 클라이언트의 실제 IP를 알기 위해 리스트의 형태로 IP주소를 기록하는 것이다. 하지만 해당 웹서버는 프록시를 사용하지 않아서 X-Forwarded-For헤더에서 remote_addr를 가져오지 않는 것을 알게 되었다.

즉, 웹 서버 내부에서 해당 메서드를 실행하는 로직이 필요했다.

그제서야 주어진 코드를 좀 더 자세히 살펴보니 아래와 같은 코드가 눈에 띄었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@app.route('/vuln', methods=['POST'])
def vuln():
    name = request.form.get('vulntest')
    try:
        address = ipaddress.ip_address(name)
        if address.version == 4:
            return "no..."
        url = urllib.parse.urlparse(f"http://[{address.exploded}]:5000/localonly")
        if url.netloc != f'[{address.exploded}]:5000':
            print(url.netloc, f'[{address.exploded}]')
            return "no..."
        req = urllib.request.Request(url.geturl())
        return urllib.request.urlopen(req).read().decode('utf-8')
    except ValueError:
        return "no..."
    except urllib.error.URLError:
        return "connection refused"


/vuln의 POST요청이 /localonly의 요청을 보내는 것을 확인하였다. vulntest값이 ip주소로 변환되어 요청주소가 됨을 알았다.

이때 IP주소가 IPv4이어서는 안되며, 축약형태여서도 안된다는 조건이 존재한다. 해당 조건들을 만족해야 req를 날릴 수 있음을 알 수 있다.

정리하자면 /vuln으로 POST요청을 vulntest라는 이름에 IPv6의 루프백 주소값을 넣어 전송하면 vulntest값이 IP주소가 되어 /localonly를 요청하는 것이다.

하지만 여기서 문제는 /vuln에서는 IPv6이어야하는 조건이 존재하고 /localonly에서는 IPv4이어야하는 각 조건이 충돌하는 것이다.

하나의 IP주소가 IPv6도 만족하고 IPv4도 만족하는 방법이 필요했다. 이를 꽤 오래 구글링하다가 IPv4-mapped IPv6이라는 방식을 발견하였다.

IPv4-mapped IPv6

이는 IPv4주소를 IPv6환경에서 IP주소로 표현하는 방식이다. 즉, IPv4와 IPv6간의 호환을 위한 기술이라고 보면 될 것 같다.

IPv4주소를 IPv6주소로 표현하는 방식은 다음과 같다.

1
::ffff:<IPv4주소>

이를 통해 IPv4주소를 IPv6를 사용하는 네트워크에서 사용 가능하다.

우리는 IPv6주소여야하는 조건에서 IPv4여야하는 조건으로 변경되므로 충분히 적용 가능한 기술이라고 생각했다. IPv4주소로 127.0.0.1를 사용하므로 이를 IPv6주소형식으로 변환하면 ::ffff:127.0.0.1이 된다.

해당 IP주소를 /vuln주소로 형식에 맞게 요청을 보내면 위와 같이 flag가 리턴됨을 확인할 수 있다.

느낀 점

  • 주어진 코드를 너무 급하게 보려고 해서 돌고 돌아 풀게된 것 같았다.
  • 각 핸들러 간의 로직을 확실하게 짚고 넘어가야겠다고 느꼈다.
  • 이번의 IPv4-mapped IPv6와 같이 주어진 조건을 만족하는 새로운 개념에 대해 충분히 이해하고 적용할 수 있는 귀중한 시간이었다.

원문: Velog

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.