워게임 드림핵 error based sql injection/challenges-412

손주은·2024년 7월 17일
0

워게임

목록 보기
7/13

문제 출처: https://dreamhack.io/wargame/challenges/412

이 문제는 Error based sql injection와 관련된 문제이고, 문제 풀이에 앞서 이 문제를 풀기 위해 필요한 간단한 개념정리 및 실전 코드를 설명하고 풀이를 시작하도록 하겠다.

Error based sql injection이란 에러를 발생시켜서 데이터베이스 및 운영 체제의 정보를 획득하는 공격 기법으로, 데이터베이스가 반환하는 에러 메시지를 이용해 공격자가 데이터베이스 구조, 쿼리 결과 등을 알아내는 방법으로 에러 메시지를 자세히 반환하는 경우에 효과적이다. Mysql에서는 대표적으로 extractvalue()함수를 이용한다. 이 함수는 extractvalue(xml_frag, xpath_expr)형태로 이용되며 여기서 ‘xml_frag’는 xml 데이터 조각을 뜻하고, ‘xpath_expr’은 추출한 값을 지정하는 xpath 표현식을 뜻한다. (xml은 eXtensible Markup Language의 줄임말로 데이터를 구조화하고 저장하는 데에 사용되는 마크업 언어이고, xpath는 xml 문서에서 특성 요소나 속성을 찾기 위한 경로 언어를 뜻한다.) Error based sql 인젝션에서 xml 데이터의 자리에는 주로 1,2,3 과 같은 단순한 값으로 이용이 된다. 예로 ‘1’은 xml 데이터를 나타내지 않으므로 유효한 xml 데이터는 아니지만, 함수 자체가 인수인 ‘1’을 xml 데이터로 처리하려고 시도하기 때문에 에러가 발생하게 된다. 따라서 이 부분은 함수가 유효하지 않은 xml 데이터로 사용이 되어 에러를 발생시키는 역할을 한다. xpath 자리에는 잘못된 xpath 표현식을 제공하면 오류가 발생하고, 이 오류 메시지를 통해 데이터베이스의 정보(테이블 이름, 칼럼 이름 등)를 유출할 수 있다.

concat 함수는 SQL의 함수로, 문자열을 합치는 데 사용이 되고 데이터베이스의 정보를 추출하기 위해서 사용된다. 에러 메시지를 통해서 데이터를 유출시키는 방법 중 하나로 extractvalue 함수와 함께 사용하여서 데이터베이스의 정보를 유출할 수 있다. 만약 select concat(‘Hello’, ‘ ‘, ‘World!’)라고 하면 이 쿼리는 “Hello World!”라는 문자열을 반환하게 된다. 컴퓨터의 ASCII 코드에서 백슬래시 문자는 숫자 92에 해당하고 0x는 숫자가 16진수로 표현되었음을 뜻하고, 5c는 16진수로 92를 나타낸다. 따라서 0x5c는 16진수로 백슬래시 문자를 나타내는 것이다. SQL 쿼리에서는 16진수 값을 문자열로 변환하여 사용할 수 있다. (이 외에도 콜론을 나타내는 0x3a, 수직 탭을 나타내는 0x0b 등 많은 문자들이 더 있다. 따라서,
SELECT concat(0x5c, ‘example’); 이라고 하면 \example 이라는 결과값을 반환하게 된다.
SELECT concat(0x5c, (SELECT database())); 라고 하면 database_name이 된다.

종합하면 SELECT extractvalue(1, concat(0x5c, (SELECT database()))); 와 같은 형식의 쿼리를 이용하여 데이터베이스의 정보를 유출시킬 수 있다.

가끔 flag 값이 출력되는 자릿수에 제한이 있어 flag 값의 길이보다 적은 자릿수가 제한이 되어 flag 값을 한 번에 모두 볼 수 없는 경우가 있을 수 있다. 이 경우에는 ‘substr’ 함수를 이용하여 문제를 해결할 수 있다. 문자열의 일부를 추출하는데 사용되는 substr 함수는 주어진 문자열에서 지정된 특정 위치부터 시작해서 지정된 특정 위치까지의 길이만큼의 부분 문자열을 반환할 수 있게 해준다. substr 함수는 substr(string, start, length)와 같은 구문으로 이루어지고 이때 string은 원본 문자열, start는 1부터 시작하는 부분 문자열을 추출하기 시작할 위치, length는 추출할 부분의 문자열 길이이다. 만약 length를 생략하게 된다면, 시작 위치부터 문자열의 끝까지 모든 문자를 반환하게 된다. MySQL에서 substr은 select substr(‘database systems’, x, y);의 형태로 이루어진다.

따라서 만약 ‘uid’가 ‘admin’인 부분의 ‘upw’의 값을 구하는 전체의 구문은 “ select upw from user where uid = ‘admin’; “ 와 같고, 값의 일부분만 추출하기 위해서는 “ select substr(upw, ~, ~) from user where uid = ‘admin’; “와 같이 이용할 수 있다. “ select substr(upw, ~) from user where uid = ‘admin’; “ 이라고 하면 ~ 부터 플래그 값의 문자 끝까지 추출하는 것이 가능해진다.

이제 문제 풀이를 진행해보겠다 !

이 문제는 Error Based SQL Injection과 관련된 문제라는 것을 알 수 있다.
우선 드림핵 사이트 로그인 후 문제 파일을 받고 서버를 생성한다.



코드를 분석해보기 전 간단한 페이지 작동 확인을 실행해보면 입력 칸에 작성한 문자열이
‘ { } ‘ 박스 부분에 출력이 된다는 것을 알 수 있다.

코드를 확인해보기 위해 다운로드 받았던 파일들을 열람해본다. deploy 파일 안에 들어있는 app.py 파일을 열어보면 다음과 같은 코드를 확인할 수 있다.

<코드 분석>
import os
운영체제와 상호작용하기 위한 모듈, 환경 변수를 가져오는 데 사용된다.
from flask import Flask, request
flask라는 프레임워크의 ‘Flask’ 클래스와 request 클래스를 가져온다. 이때 Flask는 객체 생성에 사용되고, request는 클라이언트 요청 데이터를 접근하는 데 사용된다.
from flask_mysqldb import MySQL
flask와 MySQL 데이터베이스를 연동하기 위한 확장 모듈이다.
app = Flask(name)
Flask 애플리케이션 객체 생성 name은 현재 모듈의 이름을 나타내고, Flask는 애플리케이션 리소스와 템플릿을 찾을 수 있다.
app.config['MYSQL_HOST'] = os.environ.get('MYSQL_HOST', 'localhost')
app.config['MYSQL_USER'] = os.environ.get('MYSQL_USER', 'user')
app.config['MYSQL_PASSWORD'] = os.environ.get('MYSQL_PASSWORD', 'pass')
app.config['MYSQL_DB'] = os.environ.get('MYSQL_DB', 'users')
app.config[…] 은 MySQL 데이터베이스 설정을 환경 변수에서 가져온다.

  • MYSQL_HOST : 데이터베이스 호스트 주소(기본값: localhost)
  • MYSQL_USER : 데이터베이스 사용자 이름(기본값: user)
  • MYSQL_PASSWORD: 데이터베이스 사용자 비밀번호(기본값: pass)
  • MYSQL_DB: 사용할 데이터베이스 이름(기본값: users)
    mysql = MySQL(app)
    Flask 애플리케이션과 MYSQP 데이터베이스를 연동하는 과정
    template ='''
    SELECT * FROM user WHERE uid='{uid}';

    ''' Template 는 HTML 템플릿 문자열로, 사용자에게 SQL 쿼리를 보여주고 uid를 입력할 수 있는 폼을 제공한다. @app.route('/', methods=['POST', 'GET']) def index(): uid = request.args.get('uid') if uid: try: cur = mysql.connection.cursor() cur.execute(f"SELECT * FROM user WHERE uid='{uid}';") return template.format(uid=uid) @app.route('/', methods=['POST', 'GET']) : POST과 GET 요청을 처리한다. def index() : route URL에 대한 요청을 처리하는 함수 uid = request.args.get('uid') : 요청 URL 파라미터에서 uid 값을 가져옴 if uid : 만약 uid가 존재한다면 try : 수행해라 cur = mysql.connection.cursor() : MySQL 데이터베이스 커서를 생성한다. cur.execute(f"SELECT * FROM user WHERE uid='{uid}';") : uid값에 해당하는 사용자 정보를 조회하는 SQL 쿼리 실행 return template.format(uid=uid) : 조회된 SQL커리를 포함하여 템플릿 반환 except Exception as e: return str(e) else: return template

예외가 발생한다면 예외 메시지를 반환하고, 값이 없다면 템플릿을 반환한다.
if name == 'main':
app.run(host='0.0.0.0')
해당 스크립트가 실행될 때, 서버가 시작되도록 보장하고 모듈로서의 역할을 잘 수행할 수 있게 함. 이 코드를 통해서 내가 입력한 값은 {uid} 부분으로 들어간다는 사실을 알 수 있다.

다운받은 파일 중 deploy 파일 안에 있는 init.sql 파일을 열면 다음과 같은 코드를 확인할 수 있다.

<코드 분석>
CREATE DATABASE IF NOT EXISTS users;
생성할 데이터베이스의 이름은 ‘users’이고 만약 ‘ussers’ 라는 이름의 데이터베이스가 존재하지 않으면 새로 생성한다.
GRANT ALL PRIVILEGES ON users.* TO 'dbuser'@'localhost' IDENTIFIED BY 'dbpass';
users 데이터베이스의 모든 테이블에 대해 권한을 부여하고, localhost에서만 접근 가능한 dbuser라는 사용자에게 권한을 부여하고, dbuser 사용자의 비밀번호를 ‘dbpass’로 설정한다.
USE users;
‘users;를 현재 세션에서 사용하도록 설정한다.
CREATE TABLE user(
idx int auto_increment primary key,
uid varchar(128) not null,
upw varchar(128) not null
);
‘user’라는 이름의 테이블을 생성하고, idx 라는 이름의 열을 정수형 auto_increment로 생성하여 primary key로 설정한다. uid라는 이름과 upw라는 이름을 가진 열을 가변 길이 문자열(varchar)로 생성하고 , 최대 128자까지 입력 가능하다. Not null은 열이 null 값을 가질 수 없음을 의미함.

INSERT INTO user(uid, upw) values('admin', 'DH{FLAG}');
INSERT INTO user(uid, upw) values('guest', 'guest');
INSERT INTO user(uid, upw) values('test', 'test');
user 테이블에 uid가 admin이고 upw가 DH{FLAG}인 행을 삽입하고, uid가 guest이고 upw가 guest인 행을 삽입하고, uid가 test이고 upw가 test인 행을 삽입한다. 이 부분을 통해 uid가 admin일 때 upw값이 FLAG라는 것을 알 수 있다. 즉, 데이터 베이스의 내부구조는
uid= admin, upw= FLAG
uid= guest, upw= guest
uid= test, upw= test 라는 것을 알 수 있다.
FLUSH PRIVILEGES;
이 부분을 통해 권한 테이블의 변경 내용을 디스크에 반영하고 메모리에 로드하여, 권한 변경 사항을 즉시 적용한다.

이제 데이터 베이스의 구조를 어느정도 파악했고, 구해야 하는 것이 어떤 것인지 알았으니 본격적으로 문제 풀이를 시작해보겠다.

입력창에 ' and extractvalue(1, concat(0x5c, database())); 를 입력하면 다음과 같은 메시지가 떠서 \users라는 데이터베이스 명을 얻을 수 있다.

최종적으로 알아야 하는 flag값은 ‘admin’의 ‘upw’이기 때문에 admin의 upw를 알기 위해서는 다음과 같은 쿼리를 작성해야 한다. ' and extractvalue(1, concat(0x5c, (select upw from user where uid = 'admin'))); 입력 후 submit를 누르면 다음과 같은 메시지가 뜬다.

… 을 보아 flag가 잘려서 보여지고 있다는 것을 알 수 있다. 이는 Flag값이 출력되는 자릿수에 제한이 있는데 값이 길어서 잘리는 것으로 예상되므로 substring() 을 이용하여 자릿수를 나누어 출력을 해야 한다. c3968c78840750168774ad951는 총 25자이므로 flag값이 출력되는 자릿수는 25자라는 것을 알 수 있다. 따라서 25자씩 끊어서 잘린 뒷부분을 볼 수 있다.
앞부분과의 연결성을 위해 substr 함수에 문자열이 20자부터 시작하게 입력한 후 코드를 재작성하여 다음과 같은 코드 ' and extractvalue(1, concat(0x5c, (select substr(upw,20) from user where uid = 'admin'))); 를 입력창에 입력하고 submit를 눌러보면

위와 같은 화면이 떠서 잘렸던 flag의 뒷부분이 보여지는 것을 알 수 있다.

따라서 flag 값은 : “ c3968c78840750168774ad951fc98bf788563c4d ”이다. Flag 입력 값의 양식에 맞춰 DH{c3968c78840750168774ad951fc98bf788563c4d}를 입력하면 정답이라는 것을 알 수 있다.

종합해서 정리하자면 이 문제는 sql 인젝션 중 error based sql injection과 관련된 문제이고 extractvalue() 함수를 이용하여 flag 값을 구할 수 있는 문제이다.

profile
이제 공부 막 시작했어요 ^_^ 파이팅

0개의 댓글