Natas Level 28 풀이
로그인
Username: natas28
URL: http://natas28.natas.labs.overthewire.org
이번엔 소스코드를 주지 않고, 검색창만 있다.
joke를 입력해보니 아래와 같은 조크가 나왔다.

지피티 말로는 이 개그랑 한 쌍으로 “I’d tell you a TCP joke, but I’d have to keep repeating it until you got it.” 도 있다고 한다. ack를 못 받으면 패킷이 누락된 걸로 판단하고 재전송하는 TCP 특징을 이용한 개그…
풀이
history를 보니,
입력한 값이 query 파라미터로 들어가는데, 바로 암호화되는 것 같다.
이 암호문이 서버에서 복호화되어 쿼리에 들어가고, GET 방식으로 search.php 페이지를 읽어와서 쿼리의 결과를 출력한다.
쿼리는 아마도
SELECT * FROM ??? WHERE query = ‘입력값’;
이런 느낌일텐데…싶어서 일단 ‘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 모드라 짐작할 수 있는 이유는, 아래와 같이 몇몇 입력값에 대해 암호화된 쿼리를 확인했을 때
공통적으로
“G+glEae6W/1XjA7vRm21nNyEco/c+J2TdR0Qp8dcjP” 가 붙기 때문이다.
이는 평문에서 고정된 SQL 쿼리 앞부분이 항상 같은 암호문 블록으로 암호화된다는 것을 의미한다.
ECB는 평문 블록을 독립적으로 암호화하므로, 같은 평문 블록은 항상 같은 암호문 블록을 생성한다.
반면 CBC 등 다른 운영 모드는 이전 블록이나 IV값을 이용해 암호화하기 때문에, 입력값이 바뀌면 전체 암호문 구조도 바뀐다.
따라서 이 문제는 ECB 모드가 사용되었을 가능성이 매우 높다…
또한 위에서 예상했던 대로 AES 알고리즘을 사용한다면 각 블록의 크기는 16바이트일 것이고, 쿼리 끝에 “=”이 붙는걸로 보아 base64 인코딩되었을 것이다(base64는 평문 길이가 3의 배수가 아니면 끝에 =이나 ==이 붙음).
이제 이 블록 구조를 더 파악해보기 위해 여러 입력값을 넣어봤는데,

알파벳 하나씩만 넣은 쿼리는 앞부분 뿐만 아니라 뒷부분 암호문도 같았다. 알파벳을 두 개, 세 개 넣은 쿼리는 조금씩 밀려서 앞부분만 같은 것 같다.
특수문자 ‘ 를 넣은 쿼리는 중간쯤 이후부터 xx를 넣은 쿼리와 같아지는데, 이스케이프 돼서 ' 로 처리되어 그런 것 같다.
실제로 확인해보면,
각 쿼리들을 base64 디코딩한 후 총 바이트 수를 계산하고, 16바이트별로 자르면 5개의 블록이 나오는데
알파벳 하나씩만 넣은 쿼리에 대해서는 세 번째 블록만 다르고
특수문자 ‘ 를 넣은 쿼리는 세 번째블록부터 바뀐다. 이스케이프 되는 게 확실하다…
' 로 쿼리에 들어가서 xx를 넣은 입력값과 글자수가 같아 뒷부분 4, 5번째 블록도 같은 것이다.
그렇다면
입력값 > 특수문자 이스케이프 > 평문 쿼리 생성 > AES 암호화 > base64 > URL 인코딩의 순서로 암호화되어 search.php의 파라미터로 붙는 것 같다.
이제 블록단위로 잘 조정해가며.. 특수문자 이스케이프 우회해서 인젝션 공격을 하면 되는데…
’ 를 넣었을 때 /’ 로 처리되어 바이트가 하나씩 밀려 3번째부터 블록이 깨지니까
이 깨진 블록을 의미없는 더미블록으로 끼워넣고, 이후에 sql인젝션이 서버에서 정상적으로 처리되게 할 블록이 삽입되도록 하면 된다.
정리하면,
블록 1,2(header) : 그대로 유지블록
블록 3 : 공백으로만 채운 블록으로 교체
블록 4 : SQL injection 블록들 조립해서 넣기
블록 5(footer) : 그대로 유지
이후 입력값 글자가 몇 개부터 블록 개수가 바뀌는지 조사해봤다.
위와 같이 공백을 하나씩 늘려가며 쿼리를 보냈을 때, 공백이 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 파라미터에 붙여 전송하면 된다.









