4주차/CSRF 과제

gkdudans·2023년 11월 25일

EVI$ION/웹

목록 보기
6/11

[드림핵] csrf-1

https://dreamhack.io/wargame/challenges/26

@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>'

취약점:

  • /vuln: xss_filter를 거쳐 param을 전달받는다. xss_filter에서는 frame, script, on을 *로 대체해 필터링하고 있다는 것을 알 수 있었다.
  • javascript, iframe, svg/onload 등 사용 불가능
  • /flag: 입력받은 값을 check_csrf 함수에 전달한 뒤 false를 리턴하면 wrong??이라는 메시지를, true를 리턴하면 good이라는 메시지의 창을 띄운다.
@app.route("/admin/notice_flag")
def admin_notice_flag():
    global memo_text
    if request.remote_addr != "127.0.0.1":
        return "Access Denied"
    if request.args.get("userid", "") != "admin":
        return "Access Denied 2"
    memo_text += f"[Notice] flag is {FLAG}\n"
    return "Ok"

문제 풀이:

  • /admin/notice_flag: addr가 127.0.0.1이면서 userid==admin일 때 memo 페이지의 text로 flag를 작성한다.

문제 해결:

  • script/iframe/on 문자열을 사용할 수 없으므로 img 태그를 이용하는 것이 좋다.
  • 이용자가 /admin/notice_flag 페이지를 방문하도록 해야 한다. 해당 페이지에서는 userid가 admin인지 검사하고 있다.
  • CSRF 공격으로 관리자가 /admin/notice_flag 페이지 방문하도록 한다. src 속성에 admin_notice_flag를 요청해 flag를 얻어내자.

정답:

  • flag 페이지에 img 태그를 이용한 csrf 공격을 다음과 같이 입력한다: <img src=/admin/notice_flag?userid=admin>
  • memo 페이지로 돌아가면 admin/notice_flag 페이지의 조건에 따라 memo에 flag가 출력될 것이다.


[드림핵] csrf-2

https://dreamhack.io/wargame/challenges/269/

페이지 분석:

  • /csrf: csrf-1과 마찬가지로
  • /flag: 이곳에 구문을 입력해 해당 페이지의 취약점을 공격할 수 있을 것 같다.
  • /login: username과 password를 입력하는 창이다.

코드 분석:

  • try:
        FLAG = open("./flag.txt", "r").read()
    except:
        FLAG = "[**FLAG**]"
    
    users = {
        'guest': 'guest',
        'admin': FLAG
    } #admin으로 로그인하면 flag를 얻을 수 있을 것 같다
  • /
    @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
    @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
    취약점:
    • /vuln 페이지는 xss를 막기 위해 "frame", "script", "on"를 필터링하고 있다.
    • 하지만 csrf-1을 해결했을 때와 마찬가지로 img 태그를 이용한 scrf 공격이 가능할 것 같다.
  • /flag
    @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", "")
            session_id = os.urandom(16).hex()
            session_storage[session_id] = 'admin'
            if not check_csrf(param, {"name":"sessionid", "value": session_id}):
                return '<script>alert("wrong??");history.go(-1);</script>'
    
            return '<script>alert("good");history.go(-1);</script>'
  • /login
    @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("not found user");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
                resp.set_cookie('sessionid', session_id)
                return resp 
            return '<script>alert("wrong password");history.go(-1);</script>'
  • /change_password
    @app.route("/change_password")
    def change_password():
        pw = request.args.get("pw", "")
        session_id = request.cookies.get('sessionid', None)
        try:
            username = session_storage[session_id]
        except KeyError:
            return render_template('index.html', text='please login')
    
        users[username] = pw
        return 'Done'

문제 풀이:

  • /change_password: 홈 화면에서는 보이지 않는 페이지다.
  • change_password() 함수는 GET 요청이 들어오면, 사용자의 쿠키에서 sessionid 값을 가져온다.
  • username: sessionid를 사용하여 해당 세션에 연결된 username을 가져온다. 이때 users[ username]의 비밀번호를 입력받은 새로운 pw로 변경한다.

문제 해결:

  • “frame”, “script”, “on”을 필터링하고 있어 csrf-1과 마찬가지로 img 태그를 사용해야 할 것 같다.
  • 이용자가 /change_password에 접근하도록 한 다음, pw를 내가 원하는 것으로 변경하면 admin 권환을 획득하여 flag를 얻을 수 있을 것이다.

정답:

  • 다음과 같이 입력한다: <img src="/change_password?pw=aaaa">
  • 바뀐 pw(aaa)로 admin 계정에 로그인하면 flag를 얻을 수 있다.
  • flag: DH{c57d0dc12bb9ff023faf9a0e2b49e470a77271ef}

[드림핵] CSRF Advanced

https://dreamhack.io/wargame/challenges/442

@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

취약점:

  • /vuln에 존재한다. 사용자로부터 전달된 "param" 매개변수를 받아서 .lower()를 사용하여 소문자로 변환한다.

  • "xss_filter" 리스트에 있는 문자열("frame", "script", "on")은 "*"로 대체하고 있지만 여전히 취약점이 존재한다. csrf-1, crsf-2와 마찬가지로 img 태그를 이용할 수 있을 것 같다.```c
    @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 ''

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

```c
@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>'

문제풀이:

  • 로그인이 성공하면, 서버는 새로운 seesionid를 생성하고, 해당 세션 id를 브라우저에 쿠키로 설정한다.
  • 사용자의 IP 주소와 사용자 이름을 결합하여 고유한 CSRF 토큰을 다음과 같이 생성한다: token_storage[session_id] = md5((username + request.remote_addr).encode()).hexdigest()
@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)
  • /change_password: 홈 화면에서는 보이지 않는 메시지로, change_password() 함수는 GET 요청이 들어오면, 사용자의 쿠키에서 sessionid 값을 가져온다.
  • try 블록 내: username 변수를 사용하여 현재 세션 id에 연결된 사용자의 이름을 가져온다. csrf_token 변수를 사용하여 로그인할 때 생성된 CSRF 토큰을 가져온다.
  • 요청이 GET 요청인 경우: 사용자에게 비밀번호 변경 양식을 표시한다.
  • 요청이 POST 요청인 경우: 사용자가 제출한 데이터에서 pw (새 비밀번호) 및 csrftoken (CSRF 토큰)을 가져온다. 여기서 CSRF 토큰을 검사해 공격을 방지한다.

문제 해결:

  • csrf-2 문제와 같이 /change_password에 접근해서 pw를 내가 원하는 것으로 변경해야 하는 것까지는 맞는데 여기에 CSRF 토큰이 추가되었다. 토큰 만드는 방식이 어렵지 않아서 아마 직접 토큰을 구해야 할 것 같다.
  • CSRF 토큰이란? 서버측 애플리케이션에서 생성되고 클라이언트와 공유되는 인증 값이다. 요청을 보낼 때 클라이언트는 올바른 CSRF 토큰을 포함해야 하고, 그렇지 않으면 서버는 요청된 작업 수행을 거부한다. 다음 페이지의 내용을 참고했다.Bypassing CSRF token validation | Web Security Academy

문제 풀이:

  • python 코드를 통해 username = "admin”, ip_address = "127.0.0.1”로 설정한 CSRF 토큰을 생성한다. 결과는 다음과 같다: 7505b9c72ab4aa94b1a4ed7b207b67fb
  • 다른 부분은 crsf-2와 똑같이 풀면 될 것 같다. 이용자가 /change_password에 접근하도록 한 다음, pw를 내가 원하는 것으로 변경하면 admin 권환을 획득하여 flag를 얻는다. 이때 익스플로잇 코드에 알아낸 CSRF 토큰을 포함해야 한다.

    정답:
  • /flag 페이지에 img의 src 속성을 이용하고, CSRF 토큰을 포함해 다음과 같이 입력한다: <img src="/change_password?pw=aaa&csrftoken=7505b9c72ab4aa94b1a4ed7b207b67fb">
  • /login 페이지로 돌아가 id=admin, pw=aaa(바꾼 password)를 입력하면 flag를 얻을 수 있다.
  • flag: DH{77bb582329a1b2fc9f8dc2a50b70d586}
profile
https://github.com/gkdudans

0개의 댓글