<?php
include "./config.php";
login_chk();
$db = dbconnect();
if(preg_match('/admin|and|or|if|coalesce|case|_|\.|prob|time/i', $_GET['no'])) exit("No Hack ~_~");
$query = "select id from prob_alien where no={$_GET[no]}";
echo "<hr>query : <strong>{$query}</strong><hr><br>";
$query2 = "select id from prob_alien where no='{$_GET[no]}'";
echo "<hr>query2 : <strong>{$query2}</strong><hr><br>";
if($_GET['no']){
$r = mysqli_fetch_array(mysqli_query($db,$query));
if($r['id'] !== "admin") exit("sandbox1");
$r = mysqli_fetch_array(mysqli_query($db,$query));
if($r['id'] === "admin") exit("sandbox2");
$r = mysqli_fetch_array(mysqli_query($db,$query2));
if($r['id'] === "admin") exit("sandbox");
$r = mysqli_fetch_array(mysqli_query($db,$query2));
if($r['id'] === "admin") solve("alien");
}
highlight_file(__FILE__);
?>
query와 query2의 차이는 no의 값에 '가 붙냐 마냐의 차이다.
내가 no에 전달한 값이 query에서도 실행되어야 하고 query2에서도 실행되어야 한다.
no의 값이 99 union select 1 %23' union select 2 %23라고 가정하면 쿼리는 다음과 같이 완성된다.
query: select (생략) no=99 union select 1 #' union select 2 #;
query2: select (생략) no='99 union select 1 #' union select 2 #';
query는 빨간 글자들은 주석처리되어 앞의 union select 1까지만 실행된다. 참고로 no=99인 데이터는 없다.
query2는 초록 글자는 ' ' 안에 존재하므로 문자열 취급된다. 참고로 #(한 줄 주석)도 ' '안에 존재하면 주석 역할을 잃고 문자 그 자체로 해석된다. 따라서 union select 2 #' 부분만 실행된다.
php코드의 하단엔 쿼리를 한 번씩 실행하며 결괏값을 admin과 비교한다.
admin -> not admin -> not admin -> admin 형태가 되어야 문제가 풀린다.
이때 생각해볼 수 있는 것이 시간 함수이다. 시간을 이용해 같은 쿼리문이라도 매초 쿼리의 결과가 달라지게 하는 것이다. 공격에 사용되는 시간 함수는 다음과 같다.
sleep(1): 1초 동안 정지, 반환값은 0.
now(): 현재 시간 초단위까지 출력. time은 필터링되기에 now 사용.
답은 다음과 같다.
답: ?no=99 union select concat(lower(hex(10%2b(!sleep(1)%26%26now()%2=0))),0x646d696e)%23' union select concat(lower(hex(10%2b(!sleep(1)%26%26now()%2=1))),0x646d696e)%23
union select 앞부분만 떼어서 생각해 보자.
union select concat(lower(hex(10%2b(!sleep(1)%26%26now()%2=0))),0x646d696e)
먼저 괄호 가장 안에 있는 !sleep(1)&&now()%2=0을 보자.
sleep() 함수는 반환값이 0이기에 !연산을 하면 1로 변환된다. (!: 논리 not 연산자)
now()%2=0은 현재 시간이 짝수 초일 때는 참이 되어 1 반환, 홀수 초일 때는 거짓이라 0을 반환한다.
둘을 논리곱연산(&&)을 하여 시간에 따라 0 또는 1이 되고 10에 더해진다. 그럼 결과는 10 또는 11이 될 것이다.
참고로, url에서 &은 url key를 여러 개 전달할 때 사용하는 예약 문자이므로 url 인코딩하여 문자 그 자체로 표현해야 한다.
마찬가지로 +는 띄어쓰기 역할의 예약 문자이므로 url 인코딩하여 전달해야 덧셈 역할을 할 수 있다.
다시 hex함수의 인자로 사용되어 10 또는 11의 16진수 값이 반환된다. 10은 'A', 11은 'B'가 반환된다.
다시 lower 함수의 인자로 전달되어 'A'는 'a'가 되고 'B'는 'b'가 된다.
그다음 concat 함수의 인자로 'a' 또는 'b'와 0x646d696e(='dmin')가 전달되어 문자열들이 합쳐진다. 결과적으로
'admin' 또는 'bdmin'이 완성된다.
참고로 'dmin'으로 대체하면 쿼리문에 이미 '가 존재하기에 문자열이 엉망이 되어 대체 불가능하다. 대신 "dmin"은 가능하다.
이제 if문들을 어떻게 통과하는지 생각해 보자.
첫 번째 union select는 query의 경우에 실행되어 실행될 때마다 1초 쉬고 다시 진행된다. 다시 진행될 때 홀수 초라면 'admin'이 완성되어 첫 번째 if문을 통과한다. 단, 짝수 초라면 'bdmin'이 반환되기에 홀수 초가 될 때까지 새로고침 해야 한다.
이어서 또 query가 실행되는데 1초를 또 쉬고 진행된다. 첫 번째 실행 시 홀수 초였기에 짝수 초가 되며 첫 번째 union select 실행 결과 'bdmin'이 완성되어 두 번째 if문을 통과한다.
세 번째 조건문부턴 query2가 실행되는데 그럼 두 번째 union select에 해당된다. 또 1초를 쉬면 홀수 초가 된다. 홀수 초일 때는 반환값은 'bdmin'이 되고 마지막 조건문에선 query2에서 1초 쉬어 짝수 초가 되고 두 번째 union select 결과 'admin'이 반환된다. 따라서 문제가 풀린다.