1. 문제 설명
문제도 풀기도 전에 이제 바로 보자마자 SQLi 문제이라는 것을 알 수 있다.
클릭해보자.
Admin page라는 문구와 "auth"라는 버튼과 view-source가 있다.
별 다른 기능이 없으니 auth라는 버튼을 클릭해보자.
"Access_Denied!"라는 문구와 함께 alert가 떴다.
다음 시도해볼 수 있는 것이 없기 때문에 view-source를 클릭해 소스코드를 살펴보자.
참 소스 코드가 길다...
하나 하나 보자.
$go=$_GET['val'];
if(!$go)
{
echo("<meta http-equiv=refresh content=0;url=index.php?val=1>");
}
echo("<html><head><title>admin page</title></head><body bgcolor='black'><font size=2 color=gray><b><h3>Admin page</h3></b><p>");
if(preg_match("/2|-|\+|from|_|=|\\s|\*|\//i",$go))
exit("Access Denied!");
$db = dbconnect();
$go=$_GET['val'];를 보아 일단 GET 형식으로 서버로 val라는 파라미터 값을 보낸다는것을 알 수 있다.
그리고 if문의 조건문에 go라는 값, 즉 사용자로부터 입력 받는 val 값이 없으면 다시 refresh 된다.
무조건 어떤 값이던지 넣어야지 다음으로 넘어갈 수 있다는 소리이다.
그후 preg_match라는 함수를 볼 수 있다.
PHP 공식 홈페이지에 보면 친절하게 설명이 되어있다.
regular expression을 수행하는 함수이다.
문제에서는 인자값이 2개가 들어간다.
각각의 인자값은 다음과 같다.
"/2|-|\+|from|_|=|\\s|\*|\//i"는 패턴을 의미한다.
$go는 사용자가 입력한 val 값이 들어가있다.
정리하자면 사용자가 입력한 val=go에 패턴 "/2|-|\+|from|_|=|\\s|\*|\//i"에
매칭이 되는것이 하나라도 있으면
exit("Access Denied!");가 실행된다.
패턴의 제일 앞에 2가 있으니 2로 테스트 해보자!
webhacking.kr/challenge/web-07/index.php?val=2라고 입력해보았다.
역시 "Access Denied!"라는 문구를 볼 수 있다.
그렇다면 어떤 글자가 매칭이 될까?
"/2|-|\+|from|_|=|\\s|\*|\//i"
일단 regex에서 패턴은 한 쌍의 슬래시(/) 문자 사이에 위치한다.
그 다음은 많이 보았던 | -> or이고 \s란 공백을 의미한다.
그 후 보이는 것은 i인데 나도 regex를 많이 사용해보지 않아서 i가 뭔지 궁금했다.
따라서 찾아보았다.
즉, 입력한 문자열을 패턴으로 검사하는데 검사할때 대/소문자를 구문없이 매칭한다는 말이다.
그렇다면 이제 큰 뼈대는 이해를 했고 이제 어떤것이 매칭이 되는지 살펴볼 차례이다.
"/2|-|\+|from|_|=|\\s|\*|\//i"
(문자를 필터링 하고 싶으면 \를 하나 추가해주어야한다.)
이렇게 총 9개의 문자들이 필터링 되어있다.
그 다음 DB랑 연결을 한다.
여기까지 정리하면 다음과 같다.
사용자가 입력한 val를 go라는 변수에 넣고 go라는 변수에 담긴 사용자값에서
preg_match 함수를 이용해서 패턴에 매칭이되면 "Access Denied!"라는 문구와 함께 exit가 된다.
그럼 일단 무조건 preg_match 함수에 걸리지 않게 val를 입력해야할것이다.
다음 코드를 보자.
$rand=rand(1,5);
if($rand==1)
{
$result=mysqli_query($db,"select lv from chall7 where lv=($go)") or die("nice try!");
}
if($rand==2)
{
$result=mysqli_query($db,"select lv from chall7 where lv=(($go))") or die("nice try!");
}
if($rand==3)
{
$result=mysqli_query($db,"select lv from chall7 where lv=((($go)))") or die("nice try!");
}
if($rand==4)
{
$result=mysqli_query($db,"select lv from chall7 where lv=(((($go))))") or die("nice try!");
}
if($rand==5)
{
$result=mysqli_query($db,"select lv from chall7 where lv=((((($go)))))") or die("nice try!");
}
$data=mysqli_fetch_array($result);
그 다음 rand 함수를 통해 1~5까지 수를 rand라는 변수에 넣는다.
그 후 총 5개의 if문이 있는데 어느 곳에 들어갈지는 5분의 1의 확률을 가지고 있다.
일단 어느 if문에 들어가도 query가 수행하는 행위는 같다.
rand가 1일때를 기준으로 설명하면 다음과 같다.
mysqli_query 함수로 DB 핸들러와 쿼리를 인자로 넣어주면
DB에 입력한 쿼리로 요청하게 되고 오류 없이 실행되면 true, 에러가 발생하면 false를 반환한다.
따라서 true로 실행이되면 쿼리 결과값을 result 변수에 넣는다.
만약 false가 난다면 "nice try!"라는 문구를 볼 수 있다.
예를들어 val에 패턴에 없는 문자 ^를 입력해보자.
위의 퀴리는 당연히 error가 날것이며 그렇다면 "nice try!" 문구를 볼 수 있어야한다.
이처럼 nice try라는 문구를 볼 수 있다.
다시 정상적인 SQL문을 살펴보자.
만약 별 문제가 없다면 chall7이라는 테이블에 존재하는 lv 컬럼에서
사용자가 입력한 값이 존재하면 출력해주는 간단한 쿼리이다.
그 다음 코드를 보자.
if(!$data[0])
{
echo("query error"); exit();
}
if($data[0]==1)
{
echo("<input type=button style=border:0;bgcolor='gray' value='auth' onclick=\"alert('Access_Denied!')\"><p>");
}
elseif($data[0]==2)
{
echo("<input type=button style=border:0;bgcolor='gray' value='auth' onclick=\"alert('Hello admin')\"><p>");
solve(7);
}
만약에 쿼리한 결과값이 없으면 "query error"가 출력되고 exit로 종료가 된다.
이제 쿼리한 결과값이 1이냐 2이냐로 나뉘는데
1이면 제일 처음에 봤던 "Access_Denied!"이라는 alert창을 볼 수있다.
하지만 2이면 "Hello admin"이라는 alert창이 나오면서 solve(7)이라는 함수가 실행되면서
문제가 풀릴것이다.
따라서 우리가 원하는 최종적인 목표는 쿼리 결과가 2가 나와야 한다는것이다.
여기까지가 PHP 소스코드 설명이였다.
그럼 문제를 어떻게 풀어야할까?
최종적으로 정리하면 다음과 같다.
- val라는 값을 무조건 입력해야함.
- 총 9개의 preg_match에 걸리지 않게 입력해야함.
- rand 함수로 인해 총 5분의 1의 확률을 가짐.
- 입력한 val=go가 쿼리상 에러가 있으면 "nice try!"를 볼 수 있음
- 쿼리상 문제가 없으면 "query error", alert('Access_Denied!'), alert('Hello admin') 이 3개임
- 최종적으로 쿼리한 결과값이 2가 되야함
위의 총 6개의 리스트를 가지고 문제를 풀어보자.
2. 문제 풀이
먼저 최종으로 쿼리한 결과값이 2가 되야한다.
그러기 위해서는 $go라는 곳에 2를 넣어야한다.
하지만 preg_match에 의해 2는 필터링되어 있다.
근데 위에처럼 정리를 해보니 -,+,*,/가 존재한다.
이걸보고 아 사칙연산을 통해 2를 구하는걸 막기위해서 넣은거구나라고 생각이들었다.
보안에서 mod는 많이 활용된다.
그렇다면 5%3, 11%3 등 mod를 통해서 2가 나오게 하면될것같았다.
따라서 11%3을 val에 넣어 서버로 전송해보았다.
"query eeror"라는 문구가 보인다.
그렇다면 총 5개의 if문에서는 아무런 쿼리 에러가 일어나지 않았으므로 이는 통과한것이다.
하지만 왜 정답에서 요구하는 2를 계산해서 $go에 넣었는데 왜 solve(7)이 실행이 안됬을까?
생각을 했다.
한참을 생각하다보니 조회하는 chall7 테이블에 lv 이름을 가진 컬럼 중 2가 없다는 소리이다.
따라서 아래처럼 11%3...을 통해서 저 쿼리문이 실행되고나면 결과값이 2가 나오게 해야한다.
또한 chr 함수를 이용해서 2를 표현 할 수 있다.
하지만 chall7에는 lv가 2인값이 없다.
그렇다고 insert를 할 수도 없고 따라서 union을 사용할 수 밖에 없다.
union이란
영어 그대로 합동, 합친다는 의미로 2개의 select문을 합치는것을 말한다.
어떤 두 단체가 합병이나 합쳐질때는 서로의 이해관계나 기타 제약 조건이 반드시 따라온다.
크게 아래의 3가지로 볼 수 있다.
- UNION 내의 각 SELECT 문은 같은 수의 열을 가져야 한다.
- 열은 유사한 데이터 형식을 가져야 한다.
- 각 SELECT 문의 열은 또한 동일한 순서로 있어야 한다.
그렇다면 union을 이용해서 2를 출력하게 해보자.
위에서 %s로 인해 공백을 사용할 수 없으니 공백의 의미를 가지는 괄호를 이용할것이다.
일단 처음 select문을 거짓으로 만들어줘야한다.
왜냐하면 아무리 첫번때 select문을 2로 만들어도 값이 chall7에는 2가 존재하지 않는다.
따라서 첫번째 select문을 거짓으로 만들고 union으로 2번째 select문으로 2를 만들어줘야한다.
내가 만들어낸 페이로드는 다음과 같다.
이것을 조금 더 풀어서 설명하면 다음과 같다.
앞의 select문은 false로 무시가 되고 뒤의 select문은 쿼리상 문제는 없으니
2가 테이블에 없어도 출력이 된다. (이 이유 때문에 union을 사용)
출력이 된다는것은 result에 2가 들어간다는것이고 최종적으로 data[0]에도 2가 들어가
solve(7) 함수가 실행이되어 문제가 clear 될것이다.
하지만 위에 작성한 페이로는 rand 함수로 1이 된 경우이며
해당 if문에 가서 보면 괄호가 한쌍이다. ()
따라서 3 뒤에 괄호 1개를 입력한것도 그 이유이다.
끝으로 위의 페이로드를 val에 입력해도 정답이 될 확률 또한 5분의 1일것이다.
총 3번만의 시도 끝에 clear 했다.