주어진 코드 (app.py)
import os, re, requests, flask
from urllib.parse import urlparse
app = flask.Flask(__name__)
app.flag = '***CENSORED***'
app.re_ip = re.compile('\A(\d+)\.(\d+)\.(\d+)\.(\d+)\Z')
def valid_ip(ip): #Server-Side Request Forgery
matches = app.re_ip.match(ip)
if matches == None:
return False
ip = list(map(int, matches.groups()))
if any(i > 255 for i in ip) == True:
return False
# Stay out of my private!
if ip[0] in [0, 10, 127] \
or (ip[0] == 172 and (ip[1] > 15 or ip[1] < 32)) \
or (ip[0] == 169 and ip[1] == 254) \
or (ip[0] == 192 and ip[1] == 168):
return False
return True
def get(url, recursive_count=0):
r = requests.get(url, allow_redirects=False)
if 'location' in r.headers:
if recursive_count > 2:
return '🤔'
url = r.headers.get('location')
if valid_ip(urlparse(url).netloc) == False:
return '🤔'
return get(url, recursive_count + 1)
return r.text
@app.route('/admin-status')
def admin_status():
if flask.request.remote_addr != '127.0.0.1':
return '🥺'
return app.flag
@app.route('/check-status')
def check_status():
url = flask.request.args.get('url', '')
if valid_ip(urlparse(url).netloc) == False: #scheme://username:passwd@netloc:port/path;params?query#fragment
return '🥺'
return get(url)
처음 들어갔을 때의 화면이다. 특정 URL을 입력하는 것 같아 보인다.
1)
check_status()에서 43번째 줄인 url = flask.request.args.get('url', ' ')부분을 먼저 해석해보았다.
flask은 파이썬 플라스크이며 url 피라미터로 값을 입력받는다. 또한 request모듈이 필요로 하여 위에 import로 추가되었고, request.args를 사용한다. 여기서 request.args는 url피라미터 값을 '키 = 값'으로 갖고있는 딕셔너리이다. flask.request.args.get(‘url’, ‘’)을 통해 ‘url’이라는 키에 값을 넣는 것을 알 수 있다.
44번째 줄에 있는 urlparse(url)은 url을 튜플의 하위 클래스인 ParseResult 클래스로 변환해준다. scheme://username:passwd@netloc:port/path;params?query#fragment 이것과 같은 형태로 나타나며 소스코드에서는 변환된 url의 속성들 중 네트워크 위치를 나타내는 netloc(username:passwd@netloc:port부분)에 접근한다. 그 값을 valid_ip함수에 넣어주고 false이면 이모티콘을 return하고 아니면 get 함수에 url을 매개변수로 넘겨주며 실행한다.
admin_status()에서는 주소값처럼 보이는 ‘127.0.0.1’의 값이 현재의 ip주소를 반환해주는 flask.request.remote_addr의 값과 동일하지 않으면 이모티콘을 return하고 아니면 get함수에 url을 매개변수로 넘겨주며 실행한다.
즉, 입력된 URL은 check_status()의urlparse(url).netloc함수로 전달되어서 URL을 해석하여 스키마(ex-http, ftp) 뒤부터 경로부분(/path) 앞까지 반환하고, get함수에 전달된다.
2)
쌍으로(키와 값)으로 저장하는 map으로 ip를 저장하는 valid_ip(ip)함수에 ip가 전달된다. valid_ip(ip)함수는 Server-Side Request Forgery라는 공격을 피하기 위해서 다시 확인하는 과정이라고 한다. Server-Side Request Forgery 은 Sever Side에서 이루어지는 요청을 변조해 해커가 의도한 서버로 요청을 가게 되거나 요청 자체를 변경할 수 있는 공격이다.
3)
2번 과정에서 SSRF에 대해 통과가 되면 서버는 URL을 요청하고 플래그를 가져 오기 위해 localhost인 127.0.0.1(check_status()함수의 44번째 줄에 나와있음)을 /admin-status에 요청해야한다. ip는 10진수로 표기되기 때문에 8진수 표기법을 사용해서 valid_ip(ip)함수 검사를 우회한다.
4)
127.0.0.1을 8진수로 바꾸기 위해 일단 2진수로 바꾸어 주었더니 1111111.0.0.1이 나왔다. 이를 다시 8진수로 바꾸면 (1/111/111 => 1+7+7) 0177.0.0.1이 나왔다. 따라서 8진수인 0177.0.0.1을 로컬 호스트에 요청을 보내게 된다.
모든 조건을 통과하여 서버가 url을 요청하면 8진수 표기되어있던 ip가 일반ip인 10진수로 변환이 된다.
5)
http://urlcheck1.chal.ctf.westerns.tokyo/에 나와있는 예시와 같은 형태로 http://0177.0.0.1/admin-status을 입력을 해준다.
사진과 같이 TWCTF{4r3_y0u_r34dy?n3x7_57463_15_r34l_55rf!}이라는 Flag를 얻었다.
'[CTF] TokyoWesternsCTF2020' 카테고리의 다른 글
Mask(misc) (0) | 2023.12.21 |
---|---|
Easy_hash(crypto) (0) | 2023.12.21 |