
해당 문제는 uid를 GET 파라미터로 입력받아 SQL 쿼리를 실행하는 구조였다. 주어진 코드를 보면 다음과 같이 입력값이 쿼리에 그대로 삽입되는 구조를 갖고 있다.
cur.execute(f"SELECT * FROM user WHERE uid='{uid}';")
즉, 'uid' 값에 '나 SQL 예약어를 넣으면 쉽게 인젝션이 가능한 상태였지만, 문제는 쿼리 결과가 출력되지 않는 점이었다.
입력값은 HTML <pre> 영역에 쿼리 형태로만 노출되고, 실제 SELECT 결과는 출력되지 않기 때문에 일반적인 SQLi가 어렵다고 판단됐다.
이 상황에서 쿼리 결과를 유도하는 방법은 에러 기반(Error-Based) SQLi 중에서도 GROUP BY + RAND() + CONCAT()을 활용한 테크닉이었다.
최종적으로 사용한 payload는 다음과 같다:
' and (select 1 from (select count(*), concat((select upw from user where uid='admin'), floor(rand(0)*2)) a from information_schema.tables group by a) b) -- -
이 쿼리는 다음과 같은 구조로 동작한다:
floor(rand(0)*2)를 통해 0 또는 1 중 하나를 랜덤하게 생성admin의 비밀번호와 concat하여 group key로 설정GROUP BY 키가 중복되면 "Duplicate entry" 에러를 발생시키는데, 그 에러 메시지 안에 concat한 문자열이 들어가게 된다즉, 랜덤 값이 중복되게 되면 에러 메시지를 유도할 수 있고, 그 안에 admin 계정의 upw 값이 포함되어 노출된다. 이 구조를 통해 페이지에 노출되는 쿼리 형태만 보고서도 에러 메시지 내에 플래그 문자열 DH{...}를 식별할 수 있었다.
이번 문제는 일반적인 SQL Injection보다 한 단계 더 나아가, 에러를 유도하는 방식으로 정보를 추출해야 하는 고전적인 Error-Based SQLi 문제였다. 단순히 ' or 1=1 -- - 같은 payload가 통하지 않고, 결과가 직접 출력되지도 않기 때문에 문제를 이해하고 구조를 분석해야만 해결할 수 있었다.
특히 group by concat(..., floor(rand(0)*2))라는 익숙하지 않은 구조가 처음에는 어려웠지만, 이 테크닉이 MySQL의 에러 메시지를 활용해 데이터를 노출시키는 방식이라는 걸 이해한 뒤로는 구조가 명확하게 보였다.
결국 중요한 건,
이 문제를 통해 SQLi 중에서도 에러 기반 기법에 대해 한층 더 깊이 익숙해질 수 있었다.
실전이나 CTF에서 꼭 다시 만나게 될 유형이라 자주 연습해둘 필요가 있을 듯하다.