Web Hacking/Dreamhack 풀이

CSRF Advanced

박연준 2023. 6. 26. 01:03

문제 정보

  • Exercise: CSRF Advanced에서 실습하는 문제입니다.

 

 

풀이

문제 파일인 app.py의 소스 코드는 다음과 같다.

#!/usr/bin/python3
from flask import Flask, request, render_template, make_response, redirect, url_for
from selenium.webdriver.common.by import By
from selenium import webdriver
from hashlib import md5
import urllib
import os

app = Flask(__name__)
app.secret_key = os.urandom(32)

try:
    FLAG = open("./flag.txt", "r").read()
except:
    FLAG = "[**FLAG**]"

users = {
    'guest': 'guest',
    'admin': FLAG
}

session_storage = {}
token_storage = {}

def read_url(url, cookie={"name": "name", "value": "value"}):
    cookie.update({"domain": "127.0.0.1"})
    options = webdriver.ChromeOptions()
    try:
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome("/chromedriver", options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)
        driver.get("http://127.0.0.1:8000/login")
        driver.add_cookie(cookie)
        driver.find_element(by=By.NAME, value="username").send_keys("admin")
        driver.find_element(by=By.NAME, value="password").send_keys(users["admin"])
        driver.find_element(by=By.NAME, value="submit").click()
        driver.get(url)
    except Exception as e:
        driver.quit()
        # return str(e)
        return False
    driver.quit()
    return True


def check_csrf(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
    return read_url(url, cookie)


@app.route("/")
def index():
    session_id = request.cookies.get('sessionid', None)
    try:
        username = session_storage[session_id]
    except KeyError:
        return render_template('index.html', text='please login')

    return render_template('index.html', text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not an admin"}')


@app.route("/vuln")
def vuln():
    param = request.args.get("param", "").lower()
    xss_filter = ["frame", "script", "on"]
    for _ in xss_filter:
        param = param.replace(_, "*")
    return param


@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html")
    elif request.method == "POST":
        param = request.form.get("param", "")
        if not check_csrf(param):
            return '<script>alert("wrong??");history.go(-1);</script>'

        return '<script>alert("good");history.go(-1);</script>'


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    elif request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        try:
            pw = users[username]
        except:
            return '<script>alert("user not found");history.go(-1);</script>'
        if pw == password:
            resp = make_response(redirect(url_for('index')) )
            session_id = os.urandom(8).hex()
            session_storage[session_id] = username
            token_storage[session_id] = md5((username + request.remote_addr).encode()).hexdigest()
            resp.set_cookie('sessionid', session_id)
            return resp 
        return '<script>alert("wrong password");history.go(-1);</script>'


@app.route("/change_password")
def change_password():
    session_id = request.cookies.get('sessionid', None)
    try:
        username = session_storage[session_id]
        csrf_token = token_storage[session_id]
    except KeyError:
        return render_template('index.html', text='please login')
    pw = request.args.get("pw", None)
    if pw == None:
        return render_template('change_password.html', csrf_token=csrf_token)
    else:
        if csrf_token != request.args.get("csrftoken", ""):
            return '<script>alert("wrong csrf token");history.go(-1);</script>'
        users[username] = pw
        return '<script>alert("Done");history.go(-1);</script>'

app.run(host="0.0.0.0", port=8000)

 

 

먼저 문제 파일인 app.py의 소스 코드를 분석해보면 users라는 객체에 아이디와 비밀번호가 있다고 추측할 수 있고, guest 계정이 존재하며 admin 아이디의 비밀번호를 알아내면 FLAG를 얻을 수 있을 것이라고 추측할 수 있다.

users = {
    'guest': 'guest',
    'admin': FLAG
}

 

 

다음으로 session_storage와 token_storage를 생성해 세션과 CSRF 토큰을 사용하는 것을 알 수 있다. 또한 read_url 함수에 도메인은 127.0.0.1의 로컬 호스트와 admin 계정을 사용한다는 것을 알 수 있다.

session_storage = {}
token_storage = {}

def read_url(url, cookie={"name": "name", "value": "value"}):
    cookie.update({"domain": "127.0.0.1"})
    options = webdriver.ChromeOptions()
    try:
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome("/chromedriver", options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)
        driver.get("http://127.0.0.1:8000/login")
        driver.add_cookie(cookie)
        driver.find_element(by=By.NAME, value="username").send_keys("admin")
        driver.find_element(by=By.NAME, value="password").send_keys(users["admin"])
        driver.find_element(by=By.NAME, value="submit").click()
        driver.get(url)
    except Exception as e:
        driver.quit()
        # return str(e)
        return False
    driver.quit()
    return True

 

check_csrf라는 함수를 정의해서 로컬호스트의 param 인자를 넣을 수 있도록 정의했다. 또한 / 엔드 포인트에는 please login이 나오고, username이 admin이라면 flag를 보여준다. 만약 username이 admin이 아니라면 you are not an admin이라는 문구가 나온다.

def check_csrf(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
    return read_url(url, cookie)


@app.route("/")
def index():
    session_id = request.cookies.get('sessionid', None)
    try:
        username = session_storage[session_id]
    except KeyError:
        return render_template('index.html', text='please login')

    return render_template('index.html', text=f'Hello {username}, {"flag is " + FLAG if username == "admin" else "you are not an admin"}')

 

vuln 엔드포인트에서는 param의 인자를 받아와 html 태그를 그대로 출력하는 것을 알 수 있다. 또한 받은 인자를 전부 소문자로 변환한다음 frame, script, on의 키워드를 *문자로 치환하여 반환한다.

@app.route("/vuln")
def vuln():
    param = request.args.get("param", "").lower()
    xss_filter = ["frame", "script", "on"]
    for _ in xss_filter:
        param = param.replace(_, "*")
    return param

 

 

flag 엔드포인트에서는 flag.html를 GET 메소드로 받아오고, check_csrf의 param의 파라미터 값을 넣어 POST 메소드로 전송한다.

@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html")
    elif request.method == "POST":
        param = request.form.get("param", "")
        if not check_csrf(param):
            return '<script>alert("wrong??");history.go(-1);</script>'

        return '<script>alert("good");history.go(-1);</script>'

 

 

login 엔드포인트를 분석해보면 중요해보이는 단서가 존재한다. login.html을 GET 요청으로 받아와 username과 password를 POST 메소드로 전송한다. 또한 pw를 검사할 때 users 데이터에 있는 이름인지도 검사하고 있다. 다음으로 중요한 CSRF 토큰을 지정하고 있는데 여기에서는 username과 IP주소를 더하여 md5로 암화한 후 hexdigest 함수를 이용하여 해싱한 문자열을 얻어 CSRF 토큰으로 사용하고 있다. 하지만 여기서 username은 admin을 사용하고 IP주소는 127.0.0.1 이기 때문에 CSRF 토큰 취약점이 존재한다.

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    elif request.method == 'POST':
        username = request.form.get('username')
        password = request.form.get('password')
        try:
            pw = users[username]
        except:
            return '<script>alert("user not found");history.go(-1);</script>'
        if pw == password:
            resp = make_response(redirect(url_for('index')) )
            session_id = os.urandom(8).hex()
            session_storage[session_id] = username
            token_storage[session_id] = md5((username + request.remote_addr).encode()).hexdigest()
            resp.set_cookie('sessionid', session_id)
            return resp 
        return '<script>alert("wrong password");history.go(-1);</script>'

 

 

change_password 엔드포인트에서는 pw를 get 요청으로 받아와 변경하는데 CSRF 토큰도 같이 검사하는 것을 알 수 있다. 따라서 CSRF 토큰 취약점을 이용해 CSRF 토큰을 탈취한 후 URL의 파라미터 값으로 CSRF 토큰을 넘겨주면 admin의 비밀번호를 변경할 수 있다.

@app.route("/change_password")
def change_password():
    session_id = request.cookies.get('sessionid', None)
    try:
        username = session_storage[session_id]
        csrf_token = token_storage[session_id]
    except KeyError:
        return render_template('index.html', text='please login')
    pw = request.args.get("pw", None)
    if pw == None:
        return render_template('change_password.html', csrf_token=csrf_token)
    else:
        if csrf_token != request.args.get("csrftoken", ""):
            return '<script>alert("wrong csrf token");history.go(-1);</script>'
        users[username] = pw
        return '<script>alert("Done");history.go(-1);</script>'

 

 

login 페이지에 접속해 guest guest를 입력해보면 다음과 같이 admin이 아니라고 나온다.

 

 

우선 CSRF 토큰 취약점이 있다는 것을 알고 있기 때문에 CSRF 토큰을 구하는 스크립트를 짜야한다. 파이썬을 이용해 스크립트를 짜기 위해선 먼저 md5의 해쉬함수를 사용하기 때문에 from hashlib import md5를 통해 md5를 import 해주어야 한다. 다음으로는 username과 ip를 각각 admin과 127.0.0.1로 정의하고 문자열 앞에 b를 붙여 바이트 코드로 변경한다. 위에 있는 CSRF 토큰은 둘을 더하고 md5 암호화를 거쳐 hexdigest() 함수를 통해 CSRF 토큰을 구하기 때문에 다음과 같이 스크립트를 짜보았다.

 

코드를 실행시키면 CSRF 토큰 값이 나온다.

 

 

다음으로 vuln 페이지에서 html 태그를 작성하여 change_pass 스크립트가 잘 동작되는지 테스트해보아야 한다. 요청이 잘 가는지 확인하기 위해서는 드림핵 툴즈의 Request bin을 이용할 수 있다.

 

dreamhack-tools

 

tools.dreamhack.games

 

 

vuln 페이지에서 <img> 태그를 테스트 해보면 요청이 잘 가는 것을 확인할 수 있었다.

 

 

그럼 이제 <img> 태그가 사용 가능하다는 것을 알았으니까 flag 페이지로 들어가 익스플로잇을 수행하는데, change_password 페이지에서 비밀번호를 변경해야 admin 계정으로 로그인 가능하다. 여기서 change_password는 pw와 csrftoken을 인자로 받고 있으므로 스크립트는 다음과 같이 쓸 수 있다.

<img src="/change_password?pw=yeonjun&csrftoken=7505b9c72ab4aa94b1a4ed7b207b67fb">

 

 

flag 페이지에서 다음과 같이 작성한 스크립트를 붙여넣는다.

 

 

change_password에 요청이 잘 가서 admin의 비밀번호가 yeonjun으로 변경되었다고 믿으며, admin yeonjun으로 로그인을 시도한다.

 

잘못되었다고 나왔다.. 뭐가 문제지?? 

 

다시 pw의 값을 yeonjun이 아닌 admin으로 바꾸어서 시도해보니 flag 값이 잘 나왔다.

 

 

왜 yeonjun은 안되었을까 생각해보다가 change_password 페이지에 다음과 같은 코드 때문에 안되는 거 같다.

users[username] = pw

'Web Hacking > Dreamhack 풀이' 카테고리의 다른 글

CSS Injection 풀이  (0) 2023.07.01
Client Side Template Injection 풀이  (0) 2023.06.26
file-download-1  (0) 2023.06.25
image-storage  (0) 2023.06.25
command-injection-chatgpt  (0) 2023.06.25