
문제는 uid 파라미터를 통해 SQL 쿼리에 삽입되는 구조였고, 다음과 같이 작성되어 있다.
cur.execute(f"SELECT * FROM user WHERE uid='{uid}';")
result = cur.fetchone()
if result:
return template.format(uid=uid, result=result[1])
즉, uid 값은 쿼리의 ' 안에 들어가고, fetchone()으로 가져온 결과 중 result[1]만 출력된다. result[1]은 uid 필드이기 때문에, 우리가 직접 원하는 upw는 화면에 출력되지 않는다.
처음에는 Blind SQL Injection으로 한 글자씩 upw 값을 추측해야 한다고 생각했지만, 해당 문제는 UNION SELECT를 통한 직접 추출이 가능하도록 설계되어 있었다.
다만, 필터링이 존재했기 때문에 몇 가지 제한사항이 있었다.
keywords = ['union', 'select', 'from', 'and', 'or', 'admin', ' ', '*', '/']
이 중 ' ' (공백) 필터가 가장 강력한 제약이었고 다른 문자열 필터들은 대문자로 우회가 가능했으며, 일반적인 UNION SELECT 쿼리를 구성할 수 없었다. 그러나 브라우저와 달리 서버 측에서는 %09 (탭 문자)를 공백처럼 처리하므로, 이를 통해 필터를 우회할 수 있었다.
최종적으로 사용된 페이로드는 다음과 같다.
'%09UNION%09SELECT%091,(SELECT%09upw%09FROM%09user%09WHERE%09uid='ADMIN'),1;--

URL 디코딩 후에는 아래와 같이 동작한다.
SELECT * FROM user WHERE uid=''
UNION SELECT 1, (SELECT upw FROM user WHERE uid='ADMIN'), 1;
이 때 uid=''는 결과가 없기 때문에 무시되고, UNION SELECT의 결과만 fetchone()으로 반환된다. 이 결과의 두 번째 컬럼 (result[1])이 우리가 넣은 (SELECT upw ...) 값이기 때문에, 결국 플래그가 그대로 출력된다.
이번 문제는 WAF 우회를 포함한 UNION-based SQLi 문제였다. 특히 ' ' 공백 필터가 존재함에도 %09 (탭 문자)를 통해 이를 우회할 수 있다는 점에서 입력 파라미터 필터 우회의 중요성을 다시 느낄 수 있었다.
또한 SQL에서 쿼리 구조를 정확히 이해하고, 출력 구조(result[1]가 출력되는지 등)를 분석함으로써 최소한의 정보만으로도 전체 흐름을 유추하고 활용할 수 있다는 점이 흥미로웠다.
단순히 UNION SELECT를 넣는 것뿐만 아니라, 정확한 컬럼 개수와 순서, 반환 구조를 모두 고려해야 하며, 공백 필터 우회는 탭(%09)등의 대체 기법을 기억해두면 다른 문제에서도 충분히 응용할 수 있을 것이다.