[Webhacking.kr] old-59


문제에 접속하면 다음과 같은 테이블이 보인다. view-source 버튼을 눌러 소스 코드를 확인해보자.

image-20240129191309816


소스코드

<?php
  include "../../config.php";
  if($_GET['view_source']) view_source();
  $db = dbconnect();
  if($_POST['lid'] && isset($_POST['lphone'])){
    $_POST['lid'] = addslashes($_POST['lid']);
    $_POST['lphone'] = addslashes($_POST['lphone']);
    $result = mysqli_fetch_array(mysqli_query($db,"select id,lv from chall59 where id='{$_POST['lid']}' and phone='{$_POST['lphone']}'"));
    if($result['id']){
      echo "id : {$result['id']}<br>lv : {$result['lv']}<br><br>";
      if($result['lv'] == "admin"){
      mysqli_query($db,"delete from chall59");
      solve(59);
    }
    echo "<br><a href=./?view_source=1>view-source</a>";
    exit();
    }
  }
  if($_POST['id'] && isset($_POST['phone'])){
    $_POST['id'] = addslashes($_POST['id']);
    $_POST['phone'] = addslashes($_POST['phone']);
    if(strlen($_POST['phone'])>=20) exit("Access Denied");
    if(preg_match("/admin/i",$_POST['id'])) exit("Access Denied");
    if(preg_match("/admin|0x|#|hex|char|ascii|ord|select/i",$_POST['phone'])) exit("Access Denied");
    mysqli_query($db,"insert into chall59 values('{$_POST['id']}',{$_POST['phone']},'guest')");
  }
?>
<html><head><title>Challenge 59</title></head><body>
<form method=post>
<table border=1>
<tr><td></td><td>ID</td><td>PHONE</td><td></td></tr>
<tr><td>JOIN</td><td><input name=id></td><td><input name=phone></td><td><input type=submit></td></tr>
<tr><td>LOGIN</td><td><input name=lid></td><td><input name=lphone></td><td><input type=submit></td></tr>
</form>
<br>
<a href=./?view_source=1>view-source</a>
</body></html>


solve 조건

소스 코드에서 solve(59)의 조건은 다음과 같다.

  • POST로 전송되는 lid와 lphone 변수가 존재해야 한다.
  • lid와 lhone 변수 모두 addslashes 함수를 통해 문자열이 반환된다.
    • addslashes 함수란 싱글쿼터로 인해 오류가 발생할 경우 싱글쿼터 앞에 \ 문자를 넣어줌으로써 오류를 최소화시키는 함수이다.
  • result 변수에 요청되는 쿼리는 select id,lv from chall59 where id='{$_POST['lid']}' and phone='{$_POST['lphone']}' 이다.
  • 요청된 쿼리의 id 값이 존재한다면 echo 문으로 요청된 쿼리 결과의 id와 lv 값을 출력해준다.
  • 마지막으로 result[‘lv’] 값이 admin이면 solve이다.


추가 소스 코드

sovle 조건 뒤에 소스 코드가 추가적으로 더 존재한다. 소스 코드를 해석해보면 다음과 같다.

  1. POST로 요청된 id와 phone 값이 둘 다 존재한다면

  2. id와 phone 변수를 전과 똑같이 addslashes 함수를 통해 문자열을 변환한다.

  3. POST로 요청되는 phone 값의 문자열의 길이가 20 이상인 경우 Access Denied를 출력한다.

  4. POST로 요청되는 id 값이 정규표현식을 통해 대소문자 구분 없이 admin이면 Access Denied를 출력한다.

  5. phone 값에는 admin, 0x, #, hex, char, ascii, ord, select 키워드가 대소문자 구분 없이 존재한다면 Access Denied를 출력한다.

  6. 위와 같은 조건을 만족한다면 "insert into chall59 values('{$_POST['id']}',{$_POST['phone']},'guest')" 의 insert문의 요청 쿼리를 날린다.

if($_POST['id'] && isset($_POST['phone'])){
    $_POST['id'] = addslashes($_POST['id']);
    $_POST['phone'] = addslashes($_POST['phone']);
    if(strlen($_POST['phone'])>=20) exit("Access Denied");
    if(preg_match("/admin/i",$_POST['id'])) exit("Access Denied");
    if(preg_match("/admin|0x|#|hex|char|ascii|ord|select/i",$_POST['phone'])) exit("Access Denied");
    mysqli_query($db,"insert into chall59 values('{$_POST['id']}',{$_POST['phone']},'guest')");
  }


이 그림을 보면 일단 JOIN 부분이 바로 위에 있는 id와 phone 변수로 존재하는 것 같고, LOGIN 부분이 lid와 lphone으로 추측할 수 있다.

image-20240129191309816


Burp Suite를 이용해 JOIN 부분의 제출 패킷을 잡았다. JOIN의 ID와 PHONE에 각각 testid와 testpassword를 넣고 요청한 결과, lid와 lphone 변수도 함께 요청된다.

image-20240129195843202


Insert문 쿼리 만족 조건

JOIN의 insert문의 요청 쿼리를 만족하기 위한 조건은 다음과 같다.

  • phone 문자열의 길이가 20 미만이어야 한다.
  • id에 대소문자를 구별하지 않고 admin 문자열이 들어가 있으면 안된다.
  • phone에 admin, 0x, #, hex, char, ascii, ord, select 의 키워드가 대소문자를 구별하지 않고 들어가 있으면 안된다.


addslashes() 함수 우회

addslashes() 함수에 대해서 처음 들어보았고, addslashes 함수는 멀티바이트를 사용하는 언어로 인코딩을 할 때 우회가 가능하다고 한다. 멀티바이트를 사용하는 언어셋 환경에서는 \ 문자 앞에 %a1 ~ %fe의 값이 들어오면 해당 값이 \의 인코딩인 %5c와 합쳐져 하나의 문자를 나타내게 되고, 이를 통해 싱글 쿼터로 인해 SQL Injection 우회가 가능하다.


예상 시나리오

따라서 공격 시나리오를 예상해보면, Insert 요청문인 mysqli_query($db,"insert into chall59 values('{$_POST['id']}',{$_POST['phone']},'guest')"); 를 살펴보면 guest 부분이 lv라고 추측하였다. phone 값에 싱글 쿼터를 통해 guest 자리에 admin으로 변경해주면 lid,lphone에서 이 값으로 로그인하면 해결할 수 있을거라 생각하였다.


요청되는 쿼리를 살펴보면 phone 값이 들어가는 자리는 문자열 처리가 되지 않았으므로 숫자 타입인 것을 알 수 있다. 따라서 id, phone, lid, lphone값을 test, 1을넣어보면 다음과 같이 id는 test, lv는 guest가 나온 것을 확인할 수 있다.

image-20240129203757538


여기서 guest라는 값을 1로 바꿔보기 위해 id에는 guest를 넣고 phone에는 1, 1)– 를 입력해주었다. 이는 다음과 같은 쿼리문을 요청한다.

insert into chall59 values ('guest', 1, 1)-- , 'guest')


제출 버튼을 클릭하면 다음과 같이 lv를 1로 변경된 것을 알 수 있다.

image-20240129230409003


이제 1을 admin으로 변경해주면 될 것 같다. phone 입력 부분에 admin을 삽입하기 위해서 함수를 살펴보면 자주 사용하는 hex, char, ascii, ord 와 같은 함수를 막은 것을 알 수 있다. 여기에선 REVERSE 함수를 사용해서 시도해 볼 것이다.

  • REVERSE 함수란?
    • 함수 인자의 문자열 부분을 거꾸로 만들어 변환해주는 함수


REVERSE 함수를 사용하기 이전에 PHONE의 문자열의 길이가 20 미만이어야 한다는 조건이 있었다. 따라서 1,REVERSE("nimda")-- 이것은 20자가 넘어가므로 다른 방법을 생각해보아야 한다. 바로 id에 nimda라는 값을 넣어주고 phone 부분에 1, REVERSE(id))-- 를 입력해주면 된다.

id: nimda / phone: 1, REVERSE(id))-- 


아래와 같이 위에서 작성한 코드를 넣어주고 제출 버튼을 누른다. addslashes() 함수를 이용해서도 풀 수 있는 방법이 있을 것 같은데 다음에 시간 되면 작성해보겠다.

image-20240129231318068


한 번 제출하고 다시 한 번 입력하면 다음과 같이 해결한 것을 볼 수 있다.

image-20240129231502200

댓글남기기